mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-09-01 07:17:48 +02:00
Merge branch '5.0' into 5
This commit is contained in:
commit
5206945532
@ -6,12 +6,15 @@ use SilverStripe\Control\Middleware\HTTPMiddlewareAware;
|
||||
use SilverStripe\Core\Application;
|
||||
use SilverStripe\Core\Environment;
|
||||
use SilverStripe\Core\Kernel;
|
||||
use SilverStripe\Core\Manifest\Module;
|
||||
use SilverStripe\Core\Startup\FlushDiscoverer;
|
||||
use SilverStripe\Core\Startup\CompositeFlushDiscoverer;
|
||||
use SilverStripe\Core\Startup\CallbackFlushDiscoverer;
|
||||
use SilverStripe\Core\Startup\RequestFlushDiscoverer;
|
||||
use SilverStripe\Core\Startup\ScheduledFlushDiscoverer;
|
||||
use SilverStripe\Core\Startup\DeployFlushDiscoverer;
|
||||
use SilverStripe\Dev\Deprecation;
|
||||
use SilverStripe\GraphQL\TypeCreator;
|
||||
|
||||
class HTTPApplication implements Application
|
||||
{
|
||||
@ -130,6 +133,10 @@ class HTTPApplication implements Application
|
||||
return $this->callMiddleware($request, function ($request) use ($callback, $flush) {
|
||||
// Pre-request boot
|
||||
$this->getKernel()->boot($flush);
|
||||
|
||||
// This is the earliest point we can do this and guarantee it's hit exactly once per request.
|
||||
$this->warnAboutDeprecatedSetups();
|
||||
|
||||
return call_user_func($callback, $request);
|
||||
});
|
||||
} catch (HTTPResponse_Exception $ex) {
|
||||
@ -138,4 +145,48 @@ class HTTPApplication implements Application
|
||||
$this->getKernel()->shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger deprecation notices for legacy configuration which is deprecated but
|
||||
* doesn't have deprecation notices directly on the relevant API
|
||||
*
|
||||
* Don't remove this method even if it's just a no-op - we'll reuse this mechanism
|
||||
* in the future as needed.
|
||||
*/
|
||||
private function warnAboutDeprecatedSetups()
|
||||
{
|
||||
// TypeCreator is a class unique to GraphQL v3 - we use it in other areas to detect
|
||||
// which version is being used.
|
||||
if (class_exists(TypeCreator::class)) {
|
||||
Deprecation::notice(
|
||||
'4.13.0',
|
||||
'silverstripe/graphql 3.x is deprecated. Upgrade to 4.x instead.'
|
||||
. ' See https://docs.silverstripe.org/en/4/upgrading/upgrading_to_graphql_4/',
|
||||
Deprecation::SCOPE_GLOBAL
|
||||
);
|
||||
}
|
||||
|
||||
// The alternate_public_dir config property is deprecated, but because it's
|
||||
// always fetched it'll throw a deprecation warning whether you've set it or not.
|
||||
// There are also multiple mechanisms which can result in this bad configuration.
|
||||
if (PUBLIC_DIR !== 'public' || Director::publicDir() !== PUBLIC_DIR) {
|
||||
Deprecation::notice(
|
||||
'4.13.0',
|
||||
'Use of a public webroot other than "public" is deprecated.'
|
||||
. ' See https://docs.silverstripe.org/en/4/changelogs/4.1.0#public-folder/',
|
||||
Deprecation::SCOPE_GLOBAL
|
||||
);
|
||||
}
|
||||
|
||||
// This change of defaults has no other deprecation notice being emitted currently.
|
||||
$project = new Module(BASE_PATH, BASE_PATH);
|
||||
if ($project->getResourcesDir() === '') {
|
||||
Deprecation::notice(
|
||||
'4.13.0',
|
||||
'The RESOURCES_DIR constant will change to "_resources" by default.'
|
||||
. ' See https://docs.silverstripe.org/en/5/changelogs/5.0.0/#api-general',
|
||||
Deprecation::SCOPE_GLOBAL
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -188,7 +188,7 @@ abstract class BaseKernel implements Kernel
|
||||
$this->getIncludeTests(),
|
||||
$flush
|
||||
);
|
||||
|
||||
|
||||
// Flush config
|
||||
if ($flush) {
|
||||
$config = $this->getConfigLoader()->getManifest();
|
||||
@ -265,7 +265,7 @@ abstract class BaseKernel implements Kernel
|
||||
|
||||
abstract public function boot($flush = false);
|
||||
|
||||
abstract public function isFlushed();
|
||||
abstract public function isFlushed(): ?bool;
|
||||
|
||||
/**
|
||||
* Check if there's a legacy _ss_environment.php file
|
||||
|
@ -289,14 +289,14 @@ class ClassInfo
|
||||
*/
|
||||
public static function classes_for_file($filePath)
|
||||
{
|
||||
$absFilePath = Director::getAbsFile($filePath);
|
||||
$absFilePath = Convert::slashes(Director::getAbsFile($filePath));
|
||||
$classManifest = ClassLoader::inst()->getManifest();
|
||||
$classes = $classManifest->getClasses();
|
||||
$classNames = $classManifest->getClassNames();
|
||||
|
||||
$matchedClasses = [];
|
||||
foreach ($classes as $lowerClass => $compareFilePath) {
|
||||
if (strcasecmp($absFilePath ?? '', $compareFilePath ?? '') === 0) {
|
||||
if (strcasecmp($absFilePath, Convert::slashes($compareFilePath ?? '')) === 0) {
|
||||
$matchedClasses[$lowerClass] = $classNames[$lowerClass];
|
||||
}
|
||||
}
|
||||
@ -312,14 +312,14 @@ class ClassInfo
|
||||
*/
|
||||
public static function classes_for_folder($folderPath)
|
||||
{
|
||||
$absFolderPath = Director::getAbsFile($folderPath);
|
||||
$absFolderPath = Convert::slashes(Director::getAbsFile($folderPath));
|
||||
$classManifest = ClassLoader::inst()->getManifest();
|
||||
$classes = $classManifest->getClasses();
|
||||
$classNames = $classManifest->getClassNames();
|
||||
|
||||
$matchedClasses = [];
|
||||
foreach ($classes as $lowerClass => $compareFilePath) {
|
||||
if (stripos($compareFilePath ?? '', $absFolderPath ?? '') === 0) {
|
||||
if (stripos(Convert::slashes($compareFilePath ?? ''), $absFolderPath) === 0) {
|
||||
$matchedClasses[$lowerClass] = $classNames[$lowerClass];
|
||||
}
|
||||
}
|
||||
|
@ -15,11 +15,8 @@ class CoreKernel extends BaseKernel
|
||||
|
||||
/**
|
||||
* Indicates whether the Kernel has been flushed on boot
|
||||
* Uninitialised before boot
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $flush;
|
||||
private ?bool $flush = null;
|
||||
|
||||
/**
|
||||
* @param false $flush
|
||||
@ -217,12 +214,7 @@ class CoreKernel extends BaseKernel
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the Kernel has been flushed on boot
|
||||
*
|
||||
* @return bool|null null if the kernel hasn't been booted yet
|
||||
*/
|
||||
public function isFlushed()
|
||||
public function isFlushed(): ?bool
|
||||
{
|
||||
return $this->flush;
|
||||
}
|
||||
|
@ -227,6 +227,7 @@ trait CustomMethods
|
||||
*
|
||||
* @param object $object
|
||||
* @return array
|
||||
* @deprecated 4.13.0 Will be replaced by findMethodsFrom() in CMS 5
|
||||
*/
|
||||
protected function findMethodsFrom($object)
|
||||
{
|
||||
|
@ -16,11 +16,9 @@ class DatabaselessKernel extends BaseKernel
|
||||
{
|
||||
/**
|
||||
* Indicates whether the Kernel has been flushed on boot
|
||||
* Uninitialised before boot
|
||||
*
|
||||
* @var bool
|
||||
* Null before boot
|
||||
*/
|
||||
private $flush;
|
||||
private ?bool $flush = null;
|
||||
|
||||
/**
|
||||
* Allows disabling of the configured error handling.
|
||||
@ -53,10 +51,7 @@ class DatabaselessKernel extends BaseKernel
|
||||
$this->setBooted(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isFlushed()
|
||||
public function isFlushed(): ?bool
|
||||
{
|
||||
return $this->flush;
|
||||
}
|
||||
|
@ -132,4 +132,11 @@ interface Kernel
|
||||
* @return $this
|
||||
*/
|
||||
public function setEnvironment($environment);
|
||||
|
||||
/**
|
||||
* Returns whether the Kernel has been flushed on boot
|
||||
*
|
||||
* @return bool|null null if the kernel hasn't been booted yet
|
||||
*/
|
||||
public function isFlushed(): ?bool;
|
||||
}
|
||||
|
@ -10,19 +10,13 @@ use SilverStripe\Core\Injector\InjectorLoader;
|
||||
use SilverStripe\Core\Manifest\Module;
|
||||
|
||||
/**
|
||||
* Handles raising an notice when accessing a deprecated method
|
||||
* Handles raising an notice when accessing a deprecated method, class, configuration, or behaviour.
|
||||
*
|
||||
* A pattern used in SilverStripe when deprecating a method is to add something like
|
||||
* user_error('This method is deprecated', E_USER_NOTICE);
|
||||
* to the method
|
||||
*
|
||||
* However sometimes we want to mark that a method will be deprecated in some future version and shouldn't be used in
|
||||
* Sometimes we want to mark that a method will be deprecated in some future version and shouldn't be used in
|
||||
* new code, but not forbid in the current version - for instance when that method is still heavily used in framework
|
||||
* or cms.
|
||||
*
|
||||
* This class abstracts the above pattern and adds a way to do that.
|
||||
*
|
||||
* Each call to notice passes a version that the notice will be valid from.
|
||||
* See https://docs.silverstripe.org/en/contributing/release_process/#deprecation
|
||||
*/
|
||||
class Deprecation
|
||||
{
|
||||
@ -114,6 +108,12 @@ class Deprecation
|
||||
$level = 1;
|
||||
}
|
||||
$newLevel = $level;
|
||||
// handle closures inside withNoReplacement()
|
||||
if (self::$insideWithNoReplacement
|
||||
&& substr($backtrace[$newLevel]['function'], -strlen('{closure}')) === '{closure}'
|
||||
) {
|
||||
$newLevel = $newLevel + 2;
|
||||
}
|
||||
// handle call_user_func
|
||||
if ($level === 4 && strpos($backtrace[2]['function'] ?? '', 'call_user_func') !== false) {
|
||||
$newLevel = 5;
|
||||
@ -190,10 +190,10 @@ class Deprecation
|
||||
// Do not add to self::$userErrorMessageBuffer, as the backtrace is too expensive
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Getting a backtrace is slow, so we only do it if we need it
|
||||
$backtrace = null;
|
||||
|
||||
|
||||
// Get the calling scope
|
||||
if ($scope == Deprecation::SCOPE_METHOD) {
|
||||
$backtrace = debug_backtrace(0);
|
||||
@ -204,7 +204,7 @@ class Deprecation
|
||||
} else {
|
||||
$caller = false;
|
||||
}
|
||||
|
||||
|
||||
if (substr($string, -1) != '.') {
|
||||
$string .= ".";
|
||||
}
|
||||
|
@ -545,6 +545,6 @@ class CompositeField extends FormField
|
||||
/** @var FormField $child */
|
||||
$valid = ($child && $child->validate($validator) && $valid);
|
||||
}
|
||||
return $valid;
|
||||
return $this->extendValidationResult($valid, $validator);
|
||||
}
|
||||
}
|
||||
|
@ -416,7 +416,7 @@ class ConfirmedPasswordField extends FormField
|
||||
|
||||
// if field isn't visible, don't validate
|
||||
if (!$this->isSaveable()) {
|
||||
return true;
|
||||
return $this->extendValidationResult(true, $validator);
|
||||
}
|
||||
|
||||
$this->getPasswordField()->setValue($this->value);
|
||||
@ -431,7 +431,7 @@ class ConfirmedPasswordField extends FormField
|
||||
"validation"
|
||||
);
|
||||
|
||||
return false;
|
||||
return $this->extendValidationResult(false, $validator);
|
||||
}
|
||||
|
||||
if (!$this->canBeEmpty) {
|
||||
@ -443,7 +443,7 @@ class ConfirmedPasswordField extends FormField
|
||||
"validation"
|
||||
);
|
||||
|
||||
return false;
|
||||
return $this->extendValidationResult(false, $validator);
|
||||
}
|
||||
}
|
||||
|
||||
@ -483,7 +483,7 @@ class ConfirmedPasswordField extends FormField
|
||||
"validation"
|
||||
);
|
||||
|
||||
return false;
|
||||
return $this->extendValidationResult(false, $validator);
|
||||
}
|
||||
}
|
||||
|
||||
@ -498,7 +498,7 @@ class ConfirmedPasswordField extends FormField
|
||||
"validation"
|
||||
);
|
||||
|
||||
return false;
|
||||
return $this->extendValidationResult(false, $validator);
|
||||
}
|
||||
}
|
||||
|
||||
@ -513,7 +513,7 @@ class ConfirmedPasswordField extends FormField
|
||||
),
|
||||
"validation"
|
||||
);
|
||||
return false;
|
||||
return $this->extendValidationResult(false, $validator);
|
||||
}
|
||||
|
||||
// Check this password is valid for the current user
|
||||
@ -527,7 +527,7 @@ class ConfirmedPasswordField extends FormField
|
||||
),
|
||||
"validation"
|
||||
);
|
||||
return false;
|
||||
return $this->extendValidationResult(false, $validator);
|
||||
}
|
||||
|
||||
// With a valid user and password, check the password is correct
|
||||
@ -543,12 +543,12 @@ class ConfirmedPasswordField extends FormField
|
||||
),
|
||||
"validation"
|
||||
);
|
||||
return false;
|
||||
return $this->extendValidationResult(false, $validator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return $this->extendValidationResult(true, $validator);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -58,6 +58,7 @@ class CurrencyField extends TextField
|
||||
|
||||
public function validate($validator)
|
||||
{
|
||||
$result = true;
|
||||
$currencySymbol = preg_quote(DBCurrency::config()->uninherited('currency_symbol') ?? '');
|
||||
$regex = '/^\s*(\-?' . $currencySymbol . '?|' . $currencySymbol . '\-?)?(\d{1,3}(\,\d{3})*|(\d+))(\.\d{2})?\s*$/';
|
||||
if (!empty($this->value) && !preg_match($regex ?? '', $this->value ?? '')) {
|
||||
@ -66,9 +67,10 @@ class CurrencyField extends TextField
|
||||
_t('SilverStripe\\Forms\\Form.VALIDCURRENCY', "Please enter a valid currency"),
|
||||
"validation"
|
||||
);
|
||||
return false;
|
||||
$result = false;
|
||||
}
|
||||
return true;
|
||||
|
||||
return $this->extendValidationResult($result, $validator);
|
||||
}
|
||||
|
||||
public function getSchemaValidation()
|
||||
|
@ -369,7 +369,7 @@ class DateField extends TextField
|
||||
{
|
||||
// Don't validate empty fields
|
||||
if (empty($this->rawValue)) {
|
||||
return true;
|
||||
return $this->extendValidationResult(true, $validator);
|
||||
}
|
||||
|
||||
// We submitted a value, but it couldn't be parsed
|
||||
@ -382,7 +382,7 @@ class DateField extends TextField
|
||||
['format' => $this->getDateFormat()]
|
||||
)
|
||||
);
|
||||
return false;
|
||||
return $this->extendValidationResult(false, $validator);
|
||||
}
|
||||
|
||||
// Check min date
|
||||
@ -406,7 +406,7 @@ class DateField extends TextField
|
||||
ValidationResult::TYPE_ERROR,
|
||||
ValidationResult::CAST_HTML
|
||||
);
|
||||
return false;
|
||||
return $this->extendValidationResult(false, $validator);
|
||||
}
|
||||
}
|
||||
|
||||
@ -431,11 +431,11 @@ class DateField extends TextField
|
||||
ValidationResult::TYPE_ERROR,
|
||||
ValidationResult::CAST_HTML
|
||||
);
|
||||
return false;
|
||||
return $this->extendValidationResult(false, $validator);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return $this->extendValidationResult(true, $validator);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -565,7 +565,7 @@ class DatetimeField extends TextField
|
||||
{
|
||||
// Don't validate empty fields
|
||||
if (empty($this->rawValue)) {
|
||||
return true;
|
||||
return $this->extendValidationResult(true, $validator);
|
||||
}
|
||||
|
||||
// We submitted a value, but it couldn't be parsed
|
||||
@ -578,7 +578,7 @@ class DatetimeField extends TextField
|
||||
['format' => $this->getDatetimeFormat()]
|
||||
)
|
||||
);
|
||||
return false;
|
||||
return $this->extendValidationResult(false, $validator);
|
||||
}
|
||||
|
||||
// Check min date (in server timezone)
|
||||
@ -602,7 +602,7 @@ class DatetimeField extends TextField
|
||||
ValidationResult::TYPE_ERROR,
|
||||
ValidationResult::CAST_HTML
|
||||
);
|
||||
return false;
|
||||
return $this->extendValidationResult(false, $validator);
|
||||
}
|
||||
}
|
||||
|
||||
@ -627,11 +627,11 @@ class DatetimeField extends TextField
|
||||
ValidationResult::TYPE_ERROR,
|
||||
ValidationResult::CAST_HTML
|
||||
);
|
||||
return false;
|
||||
return $this->extendValidationResult(false, $validator);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return $this->extendValidationResult(true, $validator);
|
||||
}
|
||||
|
||||
public function performReadonlyTransformation()
|
||||
|
@ -29,6 +29,7 @@ class EmailField extends TextField
|
||||
*/
|
||||
public function validate($validator)
|
||||
{
|
||||
$result = true;
|
||||
$this->value = trim($this->value ?? '');
|
||||
|
||||
$pattern = '^[a-z0-9!#$%&\'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$';
|
||||
@ -43,10 +44,10 @@ class EmailField extends TextField
|
||||
'validation'
|
||||
);
|
||||
|
||||
return false;
|
||||
$result = false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return $this->extendValidationResult($result, $validator);
|
||||
}
|
||||
|
||||
public function getSchemaValidation()
|
||||
|
@ -187,7 +187,7 @@ class FileField extends FormField implements FileHandleField
|
||||
$fieldName = preg_replace('#\[(.*?)\]$#', '', $this->name ?? '');
|
||||
|
||||
if (!isset($_FILES[$fieldName])) {
|
||||
return true;
|
||||
return $this->extendValidationResult(true, $validator);
|
||||
}
|
||||
|
||||
if ($isMultiFileUpload) {
|
||||
@ -204,11 +204,12 @@ class FileField extends FormField implements FileHandleField
|
||||
$isValid = false;
|
||||
}
|
||||
}
|
||||
return $isValid;
|
||||
return $this->extendValidationResult($isValid, $validator);
|
||||
}
|
||||
|
||||
// regular single-file upload
|
||||
return $this->validateFileData($validator, $_FILES[$this->name]);
|
||||
$result = $this->validateFileData($validator, $_FILES[$this->name]);
|
||||
return $this->extendValidationResult($result, $validator);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1219,18 +1219,29 @@ class FormField extends RequestHandler
|
||||
return strtolower(preg_replace('/Field$/', '', $type->getShortName() ?? '') ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to call an extension hook which allows the result of validate() calls to be adjusted
|
||||
*
|
||||
* @param bool $result
|
||||
* @param Validator $validator
|
||||
* @return bool
|
||||
*/
|
||||
protected function extendValidationResult(bool $result, Validator $validator): bool
|
||||
{
|
||||
$this->extend('updateValidationResult', $result, $validator);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract method each {@link FormField} subclass must implement, determines whether the field
|
||||
* is valid or not based on the value.
|
||||
*
|
||||
* @todo Make this abstract.
|
||||
*
|
||||
* @param Validator $validator
|
||||
* @return bool
|
||||
*/
|
||||
public function validate($validator)
|
||||
{
|
||||
return true;
|
||||
return $this->extendValidationResult(true, $validator);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -668,8 +668,8 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler
|
||||
return $gridField
|
||||
->getConfig()
|
||||
->getComponentByType(GridFieldPaginator::class)
|
||||
->getTemplateParameters($gridField)
|
||||
->toMap()['NumPages'];
|
||||
?->getTemplateParameters($gridField)
|
||||
?->toMap()['NumPages'] ?? 1;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -73,7 +73,7 @@ class LookupField extends MultiSelectField
|
||||
*/
|
||||
public function validate($validator)
|
||||
{
|
||||
return true;
|
||||
return $this->extendValidationResult(true, $validator);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -334,11 +334,12 @@ class MoneyField extends FormField
|
||||
['currency' => $currency]
|
||||
)
|
||||
);
|
||||
return false;
|
||||
return $this->extendValidationResult(false, $validator);
|
||||
}
|
||||
|
||||
// Field-specific validation
|
||||
return $this->fieldAmount->validate($validator) && $this->fieldCurrency->validate($validator);
|
||||
$result = $this->fieldAmount->validate($validator) && $this->fieldCurrency->validate($validator);
|
||||
return $this->extendValidationResult($result, $validator);
|
||||
}
|
||||
|
||||
public function setForm($form)
|
||||
|
@ -247,21 +247,23 @@ abstract class MultiSelectField extends SelectField
|
||||
return true;
|
||||
}
|
||||
);
|
||||
if (empty($invalidValues)) {
|
||||
return true;
|
||||
|
||||
$result = true;
|
||||
if (!empty($invalidValues)) {
|
||||
$result = false;
|
||||
// List invalid items
|
||||
$validator->validationError(
|
||||
$this->getName(),
|
||||
_t(
|
||||
'SilverStripe\\Forms\\MultiSelectField.SOURCE_VALIDATION',
|
||||
"Please select values within the list provided. Invalid option(s) {value} given",
|
||||
['value' => implode(',', $invalidValues)]
|
||||
),
|
||||
"validation"
|
||||
);
|
||||
}
|
||||
|
||||
// List invalid items
|
||||
$validator->validationError(
|
||||
$this->getName(),
|
||||
_t(
|
||||
'SilverStripe\\Forms\\MultiSelectField.SOURCE_VALIDATION',
|
||||
"Please select values within the list provided. Invalid option(s) {value} given",
|
||||
['value' => implode(',', $invalidValues)]
|
||||
),
|
||||
"validation"
|
||||
);
|
||||
return false;
|
||||
return $this->extendValidationResult($result, $validator);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -191,20 +191,21 @@ class NumericField extends TextField
|
||||
*/
|
||||
public function validate($validator)
|
||||
{
|
||||
$result = true;
|
||||
// false signifies invalid value due to failed parse()
|
||||
if ($this->value !== false) {
|
||||
return true;
|
||||
if ($this->value === false) {
|
||||
$validator->validationError(
|
||||
$this->name,
|
||||
_t(
|
||||
'SilverStripe\\Forms\\NumericField.VALIDATION',
|
||||
"'{value}' is not a number, only numbers can be accepted for this field",
|
||||
['value' => $this->originalValue]
|
||||
)
|
||||
);
|
||||
$result = false;
|
||||
}
|
||||
|
||||
$validator->validationError(
|
||||
$this->name,
|
||||
_t(
|
||||
'SilverStripe\\Forms\\NumericField.VALIDATION',
|
||||
"'{value}' is not a number, only numbers can be accepted for this field",
|
||||
['value' => $this->originalValue]
|
||||
)
|
||||
);
|
||||
return false;
|
||||
return $this->extendValidationResult($result, $validator);
|
||||
}
|
||||
|
||||
public function getSchemaValidation()
|
||||
|
@ -140,7 +140,7 @@ class OptionsetField extends SingleSelectField
|
||||
public function validate($validator)
|
||||
{
|
||||
if (!$this->Value()) {
|
||||
return true;
|
||||
return $this->extendValidationResult(true, $validator);
|
||||
}
|
||||
|
||||
return parent::validate($validator);
|
||||
|
@ -44,7 +44,7 @@ class SingleLookupField extends SingleSelectField
|
||||
*/
|
||||
public function validate($validator)
|
||||
{
|
||||
return true;
|
||||
return $this->extendValidationResult(true, $validator);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -137,13 +137,13 @@ abstract class SingleSelectField extends SelectField
|
||||
// Use selection rules to check which are valid
|
||||
foreach ($validValues as $formValue) {
|
||||
if ($this->isSelectedValue($formValue, $selected)) {
|
||||
return true;
|
||||
return $this->extendValidationResult(true, $validator);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ($this->getHasEmptyDefault() || !$validValues || in_array('', $validValues ?? [])) {
|
||||
// Check empty value
|
||||
return true;
|
||||
return $this->extendValidationResult(true, $validator);
|
||||
}
|
||||
$selected = '(none)';
|
||||
}
|
||||
@ -158,7 +158,7 @@ abstract class SingleSelectField extends SelectField
|
||||
),
|
||||
"validation"
|
||||
);
|
||||
return false;
|
||||
return $this->extendValidationResult(false, $validator);
|
||||
}
|
||||
|
||||
public function castedCopy($classOrCopy)
|
||||
|
@ -125,6 +125,7 @@ class TextField extends FormField implements TippableFieldInterface
|
||||
*/
|
||||
public function validate($validator)
|
||||
{
|
||||
$result = true;
|
||||
if (!is_null($this->maxLength) && mb_strlen($this->value ?? '') > $this->maxLength) {
|
||||
$name = strip_tags($this->Title() ? $this->Title() : $this->getName());
|
||||
$validator->validationError(
|
||||
@ -136,9 +137,9 @@ class TextField extends FormField implements TippableFieldInterface
|
||||
),
|
||||
"validation"
|
||||
);
|
||||
return false;
|
||||
$result = false;
|
||||
}
|
||||
return true;
|
||||
return $this->extendValidationResult($result, $validator);
|
||||
}
|
||||
|
||||
public function getSchemaValidation()
|
||||
|
@ -324,9 +324,10 @@ class TimeField extends TextField
|
||||
{
|
||||
// Don't validate empty fields
|
||||
if (empty($this->rawValue)) {
|
||||
return true;
|
||||
return $this->extendValidationResult(true, $validator);
|
||||
}
|
||||
|
||||
$result = true;
|
||||
// We submitted a value, but it couldn't be parsed
|
||||
if (empty($this->value)) {
|
||||
$validator->validationError(
|
||||
@ -337,9 +338,11 @@ class TimeField extends TextField
|
||||
['format' => $this->getTimeFormat()]
|
||||
)
|
||||
);
|
||||
return false;
|
||||
$result = false;
|
||||
}
|
||||
return true;
|
||||
|
||||
$this->extendValidationResult($result, $validator);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -46,7 +46,11 @@ class DBBoolean extends DBField
|
||||
{
|
||||
$fieldName = $this->name;
|
||||
if ($fieldName) {
|
||||
$dataObject->setField($fieldName, $this->value ? 1 : 0);
|
||||
if ($this->value instanceof DBField) {
|
||||
$this->value->saveInto($dataObject);
|
||||
} else {
|
||||
$dataObject->__set($fieldName, $this->value ? 1 : 0);
|
||||
}
|
||||
} else {
|
||||
$class = static::class;
|
||||
throw new \RuntimeException("DBField::saveInto() Called on a nameless '$class' object");
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace SilverStripe\ORM\FieldType;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\DB;
|
||||
@ -220,8 +221,12 @@ abstract class DBComposite extends DBField
|
||||
{
|
||||
foreach ($this->compositeDatabaseFields() as $field => $spec) {
|
||||
// Save into record
|
||||
$key = $this->getName() . $field;
|
||||
$dataObject->setField($key, $this->getField($field));
|
||||
if ($this->value instanceof DBField) {
|
||||
$this->value->saveInto($dataObject);
|
||||
} else {
|
||||
$key = $this->getName() . $field;
|
||||
$dataObject->__set($key, $this->getField($field));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -270,11 +275,11 @@ abstract class DBComposite extends DBField
|
||||
{
|
||||
$this->objCacheClear();
|
||||
|
||||
// Non-db fields get assigned as normal properties
|
||||
if (!$this->hasField($field)) {
|
||||
parent::setField($field, $value);
|
||||
|
||||
return $this;
|
||||
throw new InvalidArgumentException(implode(' ', [
|
||||
"Field $field does not exist.",
|
||||
'If this was accessed via a dynamic property then call setDynamicData() instead.'
|
||||
]));
|
||||
}
|
||||
|
||||
// Set changed
|
||||
|
@ -88,8 +88,12 @@ class DBDecimal extends DBField
|
||||
$fieldName = $this->name;
|
||||
|
||||
if ($fieldName) {
|
||||
$value = (float) preg_replace('/[^0-9.\-\+]/', '', $this->value ?? '');
|
||||
$dataObject->setField($fieldName, $value);
|
||||
if ($this->value instanceof DBField) {
|
||||
$this->value->saveInto($dataObject);
|
||||
} else {
|
||||
$value = (float) preg_replace('/[^0-9.\-\+]/', '', $this->value ?? '');
|
||||
$dataObject->__set($fieldName, $value);
|
||||
}
|
||||
} else {
|
||||
throw new \UnexpectedValueException(
|
||||
"DBField::saveInto() Called on a nameless '" . static::class . "' object"
|
||||
|
@ -542,7 +542,11 @@ abstract class DBField extends ViewableData implements DBIndexable
|
||||
"DBField::saveInto() Called on a nameless '" . static::class . "' object"
|
||||
);
|
||||
}
|
||||
$dataObject->setField($fieldName, $this->value);
|
||||
if ($this->value instanceof self) {
|
||||
$this->value->saveInto($dataObject);
|
||||
} else {
|
||||
$dataObject->__set($fieldName, $this->value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -45,7 +45,7 @@ class DBPercentage extends DBDecimal
|
||||
|
||||
$fieldName = $this->name;
|
||||
if ($fieldName && $dataObject->$fieldName > 1.0) {
|
||||
$dataObject->setField($fieldName, 1.0);
|
||||
$dataObject->__set($fieldName, 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -394,14 +394,7 @@ class StandardRelatedDataService implements RelatedDataService
|
||||
*/
|
||||
private function prepareClassNameLiteral(string $value): string
|
||||
{
|
||||
$c = chr(92);
|
||||
$escaped = str_replace($c ?? '', "{$c}{$c}", $value ?? '');
|
||||
// postgres
|
||||
if (stripos(get_class(DB::get_conn()), 'postgres') !== false) {
|
||||
return "E'{$escaped}'";
|
||||
}
|
||||
// mysql
|
||||
return "'{$escaped}'";
|
||||
return DB::get_conn()->quoteString($value);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -73,7 +73,7 @@ class ViewableData implements IteratorAggregate
|
||||
/**
|
||||
* Acts as a PHP 8.2+ compliant replacement for dynamic properties
|
||||
*/
|
||||
private array $data = [];
|
||||
private array $dynamicData = [];
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
@ -203,7 +203,7 @@ class ViewableData implements IteratorAggregate
|
||||
*/
|
||||
public function hasField($field)
|
||||
{
|
||||
return property_exists($this, $field) || isset($this->data[$field]);
|
||||
return property_exists($this, $field) || $this->hasDynamicData($field);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -217,7 +217,7 @@ class ViewableData implements IteratorAggregate
|
||||
if ($this->isAccessibleProperty($field)) {
|
||||
return $this->$field;
|
||||
}
|
||||
return $this->data[$field];
|
||||
return $this->getDynamicData($field);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -236,10 +236,25 @@ class ViewableData implements IteratorAggregate
|
||||
if ($this->isAccessibleProperty($field)) {
|
||||
$this->$field = $value;
|
||||
}
|
||||
$this->data[$field] = $value;
|
||||
return $this->setDynamicData($field, $value);
|
||||
}
|
||||
|
||||
public function getDynamicData(string $field): mixed
|
||||
{
|
||||
return $this->hasDynamicData($field) ? $this->dynamicData[$field] : null;
|
||||
}
|
||||
|
||||
public function setDynamicData(string $field, mixed $value): static
|
||||
{
|
||||
$this->dynamicData[$field] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasDynamicData(string $field): bool
|
||||
{
|
||||
return array_key_exists($field, $this->dynamicData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a method exists for the current class which isn't private.
|
||||
* Also returns true for private methods if $this is ViewableData (not a subclass)
|
||||
|
@ -24,6 +24,7 @@ class Sources implements Resettable
|
||||
*
|
||||
* @config
|
||||
* @var array
|
||||
* @deprecated 4.0.0 Use SilverStripe\Core\Manifest\ModuleManifest.module_priority instead
|
||||
*/
|
||||
private static $module_priority = [];
|
||||
|
||||
@ -35,7 +36,6 @@ class Sources implements Resettable
|
||||
public function getSortedModules()
|
||||
{
|
||||
$sortedModules = [];
|
||||
|
||||
foreach (ModuleLoader::inst()->getManifest()->getModules() as $module) {
|
||||
$sortedModules[$module->getName()] = $module->getPath();
|
||||
};
|
||||
|
@ -457,7 +457,7 @@ class i18nTextCollector
|
||||
$this->getWriter()->write(
|
||||
$entities,
|
||||
$this->defaultLocale,
|
||||
$this->baseSavePath . '/' . $module->getRelativePath()
|
||||
$this->baseSavePath . DIRECTORY_SEPARATOR . $module->getRelativePath()
|
||||
);
|
||||
return $this;
|
||||
}
|
||||
@ -511,25 +511,25 @@ class i18nTextCollector
|
||||
$modulePath = $module->getPath();
|
||||
|
||||
// Search all .ss files in themes
|
||||
if (stripos($module->getRelativePath() ?? '', 'themes/') === 0) {
|
||||
if (stripos($module->getRelativePath() ?? '', 'themes' . DIRECTORY_SEPARATOR) === 0) {
|
||||
return $this->getFilesRecursive($modulePath, null, 'ss');
|
||||
}
|
||||
|
||||
// If non-standard module structure, search all root files
|
||||
if (!is_dir("{$modulePath}/code") && !is_dir("{$modulePath}/src")) {
|
||||
if (!is_dir($modulePath . DIRECTORY_SEPARATOR . 'code') && !is_dir($modulePath . DIRECTORY_SEPARATOR . 'src')) {
|
||||
return $this->getFilesRecursive($modulePath);
|
||||
}
|
||||
|
||||
// Get code files
|
||||
if (is_dir("{$modulePath}/src")) {
|
||||
$files = $this->getFilesRecursive("{$modulePath}/src", null, 'php');
|
||||
if (is_dir($modulePath . DIRECTORY_SEPARATOR . 'src')) {
|
||||
$files = $this->getFilesRecursive($modulePath . DIRECTORY_SEPARATOR . 'src', null, 'php');
|
||||
} else {
|
||||
$files = $this->getFilesRecursive("{$modulePath}/code", null, 'php');
|
||||
$files = $this->getFilesRecursive($modulePath . DIRECTORY_SEPARATOR . 'code', null, 'php');
|
||||
}
|
||||
|
||||
// Search for templates in this module
|
||||
if (is_dir("{$modulePath}/templates")) {
|
||||
$templateFiles = $this->getFilesRecursive("{$modulePath}/templates", null, 'ss');
|
||||
if (is_dir($modulePath . DIRECTORY_SEPARATOR . 'templates')) {
|
||||
$templateFiles = $this->getFilesRecursive($modulePath . DIRECTORY_SEPARATOR . 'templates', null, 'ss');
|
||||
} else {
|
||||
$templateFiles = $this->getFilesRecursive($modulePath, null, 'ss');
|
||||
}
|
||||
@ -957,7 +957,7 @@ class i18nTextCollector
|
||||
$fileList = [];
|
||||
}
|
||||
// Skip ignored folders
|
||||
if (is_file("{$folder}/_manifest_exclude") || preg_match($folderExclude ?? '', $folder ?? '')) {
|
||||
if (is_file($folder . DIRECTORY_SEPARATOR . '_manifest_exclude') || preg_match($folderExclude ?? '', $folder ?? '')) {
|
||||
return $fileList;
|
||||
}
|
||||
|
||||
|
@ -151,6 +151,22 @@ class DeprecationTest extends SapphireTest
|
||||
Deprecation::outputNotices();
|
||||
}
|
||||
|
||||
public function testNoticeWithNoReplacementTrue()
|
||||
{
|
||||
$message = implode(' ', [
|
||||
'SilverStripe\Dev\Tests\DeprecationTest->testNoticeWithNoReplacementTrue is deprecated.',
|
||||
'My message.',
|
||||
'Called from PHPUnit\Framework\TestCase->runTest.'
|
||||
]);
|
||||
$this->expectDeprecation();
|
||||
$this->expectDeprecationMessage($message);
|
||||
Deprecation::enable(true);
|
||||
Deprecation::withNoReplacement(function () {
|
||||
Deprecation::notice('123', 'My message.');
|
||||
});
|
||||
Deprecation::outputNotices();
|
||||
}
|
||||
|
||||
public function testClassWithNoReplacement()
|
||||
{
|
||||
$message = implode(' ', [
|
||||
|
@ -2,21 +2,40 @@
|
||||
|
||||
namespace SilverStripe\Forms\Tests;
|
||||
|
||||
use Exception;
|
||||
use LogicException;
|
||||
use ReflectionClass;
|
||||
use SilverStripe\Core\ClassInfo;
|
||||
use SilverStripe\Core\Config\Config;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\Forms\CompositeField;
|
||||
use SilverStripe\Forms\FieldGroup;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\Form;
|
||||
use SilverStripe\Forms\FormField;
|
||||
use SilverStripe\Forms\GridField\GridField;
|
||||
use SilverStripe\Forms\GridField\GridField_FormAction;
|
||||
use SilverStripe\Forms\GridField\GridState;
|
||||
use SilverStripe\Forms\NullableField;
|
||||
use SilverStripe\Forms\PopoverField;
|
||||
use SilverStripe\Forms\PrintableTransformation_TabSet;
|
||||
use SilverStripe\Forms\RequiredFields;
|
||||
use SilverStripe\Forms\SelectionGroup;
|
||||
use SilverStripe\Forms\SelectionGroup_Item;
|
||||
use SilverStripe\Forms\Tab;
|
||||
use SilverStripe\Forms\Tests\FormFieldTest\FieldValidationExtension;
|
||||
use SilverStripe\Forms\Tests\FormFieldTest\TestExtension;
|
||||
use SilverStripe\Forms\TextField;
|
||||
use SilverStripe\Forms\Tip;
|
||||
use SilverStripe\Forms\ToggleCompositeField;
|
||||
use SilverStripe\Forms\TreeDropdownField;
|
||||
use SilverStripe\Forms\TreeDropdownField_Readonly;
|
||||
use SilverStripe\ORM\ValidationResult;
|
||||
use SilverStripe\Security\Group;
|
||||
use SilverStripe\Security\Permission;
|
||||
use SilverStripe\Security\PermissionCheckboxSetField;
|
||||
use SilverStripe\Security\PermissionCheckboxSetField_Readonly;
|
||||
|
||||
class FormFieldTest extends SapphireTest
|
||||
{
|
||||
@ -24,6 +43,7 @@ class FormFieldTest extends SapphireTest
|
||||
protected static $required_extensions = [
|
||||
FormField::class => [
|
||||
TestExtension::class,
|
||||
FieldValidationExtension::class,
|
||||
],
|
||||
];
|
||||
|
||||
@ -455,6 +475,146 @@ class FormFieldTest extends SapphireTest
|
||||
);
|
||||
}
|
||||
|
||||
public function testValidationExtensionHooks()
|
||||
{
|
||||
/** @var TextField|FieldValidationExtension $field */
|
||||
$field = new TextField('Test');
|
||||
$field->setMaxLength(5);
|
||||
$field->setValue('IAmLongerThan5Characters');
|
||||
$result = $field->validate(new RequiredFields('Test'));
|
||||
$this->assertFalse($result);
|
||||
|
||||
// Call extension method in FieldValidationExtension
|
||||
$field->setExcludeFromValidation(true);
|
||||
$result = $field->validate(new RequiredFields('Test'));
|
||||
$this->assertTrue($result);
|
||||
|
||||
// Call extension methods in FieldValidationExtension
|
||||
$field->setValue('1234');
|
||||
$field->setExcludeFromValidation(false);
|
||||
$field->setTriggerTestValidationError(true);
|
||||
|
||||
// Ensure messages set via updateValidationResult() propagate through to form fields after validation
|
||||
$form = new Form(null, 'TestForm', new FieldList($field), new FieldList(), new RequiredFields());
|
||||
$form->validationResult();
|
||||
$schema = $field->getSchemaState();
|
||||
$this->assertEquals(
|
||||
'A test error message',
|
||||
$schema['message']['value']
|
||||
);
|
||||
}
|
||||
|
||||
public function testValidationExtensionHooksAreCalledOnFormFieldSubclasses()
|
||||
{
|
||||
// Can't use a dataProvider for this as dataProviders are fetched very early by phpunit,
|
||||
// and the ClassManifest isn't ready then
|
||||
$formFieldClasses = ClassInfo::subclassesFor(FormField::class, false);
|
||||
foreach ($formFieldClasses as $formFieldClass) {
|
||||
$reflection = new ReflectionClass($formFieldClass);
|
||||
// Skip abstract classes, like MultiSelectField, and fields that only exist for unit tests
|
||||
if ($reflection->isAbstract() || is_a($formFieldClass, TestOnly::class, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create appropriate constructor arguments for the form field class. These don't have to be offer realistic
|
||||
// data, they just need to ensure we can construct the field and call ->validate() on it
|
||||
switch ($formFieldClass) {
|
||||
//
|
||||
// Fields in framework with specific argument requirements
|
||||
//
|
||||
case NullableField::class:
|
||||
case CompositeField::class:
|
||||
case FieldGroup::class:
|
||||
case PopoverField::class:
|
||||
$args = [TextField::create('Test2')];
|
||||
break;
|
||||
case SelectionGroup_Item::class:
|
||||
$args = ['Test', [TextField::create('Test2')]];
|
||||
break;
|
||||
case ToggleCompositeField::class:
|
||||
$args = ['Test', 'Test', TextField::create('Test2')];
|
||||
break;
|
||||
case PrintableTransformation_TabSet::class:
|
||||
$args = [Tab::create('TestTab', 'Testtab', TextField::create('Test2'))];
|
||||
break;
|
||||
case TreeDropdownField::class:
|
||||
case TreeDropdownField_Readonly::class:
|
||||
$args = ['Test', 'Test', Group::class];
|
||||
break;
|
||||
case PermissionCheckboxSetField::class:
|
||||
case PermissionCheckboxSetField_Readonly::class:
|
||||
$args = ['Test', 'Test', Permission::class, 'Test'];
|
||||
break;
|
||||
case SelectionGroup::class:
|
||||
$args = ['Test', []];
|
||||
break;
|
||||
case GridField_FormAction::class:
|
||||
$args = [GridField::create('GF'), 'Test', 'Test label', 'Test action name', []];
|
||||
break;
|
||||
case GridState::class:
|
||||
$args = [GridField::create('GF')];
|
||||
break;
|
||||
//
|
||||
// Fields from other modules included in the kitchensink recipe
|
||||
//
|
||||
case \SilverStripe\Blog\Admin\GridFieldFormAction::class:
|
||||
$args = [GridField::create('GF'), 'Test', 'Test label', 'Test action name', []];
|
||||
break;
|
||||
case \SilverStripe\Blog\Forms\BlogAdminSidebar::class:
|
||||
$args = [TextField::create('Test2')];
|
||||
break;
|
||||
case \SilverStripe\CKANRegistry\Forms\PresentedOptionsField::class:
|
||||
$args = ['Test', \SilverStripe\CKANRegistry\Model\Resource::create()];
|
||||
break;
|
||||
case \SilverStripe\DocumentConverter\SettingsField::class:
|
||||
$args = [];
|
||||
break;
|
||||
case \DNADesign\Elemental\Forms\ElementalAreaField::class:
|
||||
$args = ['Test', \DNADesign\Elemental\Models\ElementalArea::create(), []];
|
||||
break;
|
||||
case \SilverStripe\MFA\FormField\RegisteredMFAMethodListField::class:
|
||||
$args = ['Test', 'Test label', 1];
|
||||
break;
|
||||
case \SilverStripe\Subsites\Forms\SubsitesTreeDropdownField::class:
|
||||
$args = ['Test', 'Test', Group::class];
|
||||
break;
|
||||
case \SilverStripe\UserForms\FormField\UserFormsCompositeField::class:
|
||||
case \SilverStripe\UserForms\FormField\UserFormsGroupField::class:
|
||||
case \SilverStripe\UserForms\FormField\UserFormsStepField::class:
|
||||
$args = [TextField::create('Test2')];
|
||||
break;
|
||||
case \Symbiote\AdvancedWorkflow\FormFields\WorkflowField::class:
|
||||
$args = ['Test', 'Test label', \Symbiote\AdvancedWorkflow\DataObjects\WorkflowDefinition::create()];
|
||||
break;
|
||||
//
|
||||
// Default arguments, this covers most simple form fields
|
||||
//
|
||||
default:
|
||||
$args = ['Test', 'Test label'];
|
||||
}
|
||||
|
||||
// Assert that extendValidationResult is called once each time ->validate() is called
|
||||
$mock = $this->getMockBuilder($formFieldClass)
|
||||
->setConstructorArgs($args)
|
||||
->onlyMethods(['extendValidationResult'])
|
||||
->getMock();
|
||||
$mock->expects($invocationRule = $this->once())
|
||||
->method('extendValidationResult')
|
||||
->will($this->returnValue(true));
|
||||
|
||||
$isValid = $mock->validate(new RequiredFields());
|
||||
$this->assertTrue($isValid, "$formFieldClass should be valid");
|
||||
|
||||
// This block is not essential and only exists to make test debugging easier - without this,
|
||||
// the error message on failure is generic and doesn't include the class name that failed
|
||||
try {
|
||||
$invocationRule->verify();
|
||||
} catch (Exception $e) {
|
||||
$this->fail("Expectation failed for '$formFieldClass' class: {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testHasClass()
|
||||
{
|
||||
$field = new FormField('Test');
|
||||
|
38
tests/php/Forms/FormFieldTest/FieldValidationExtension.php
Normal file
38
tests/php/Forms/FormFieldTest/FieldValidationExtension.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\Forms\Tests\FormFieldTest;
|
||||
|
||||
use SilverStripe\Core\Extension;
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\Forms\Validator;
|
||||
|
||||
class FieldValidationExtension extends Extension implements TestOnly
|
||||
{
|
||||
protected bool $excludeFromValidation = false;
|
||||
|
||||
protected bool $triggerTestValidationError = false;
|
||||
|
||||
public function updateValidationResult(bool &$result, Validator $validator)
|
||||
{
|
||||
if ($this->excludeFromValidation) {
|
||||
$result = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->triggerTestValidationError) {
|
||||
$result = false;
|
||||
$validator->validationError($this->owner->getName(), 'A test error message');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public function setExcludeFromValidation(bool $exclude)
|
||||
{
|
||||
$this->excludeFromValidation = $exclude;
|
||||
}
|
||||
|
||||
public function setTriggerTestValidationError(bool $triggerTestValidationError)
|
||||
{
|
||||
$this->triggerTestValidationError = $triggerTestValidationError;
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ namespace SilverStripe\ORM\Tests;
|
||||
use SilverStripe\ORM\FieldType\DBMoney;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class DBCompositeTest extends SapphireTest
|
||||
{
|
||||
@ -108,4 +109,15 @@ class DBCompositeTest extends SapphireTest
|
||||
$this->assertEquals('DBCompositeTest_SubclassedDBFieldObject', $object2->dbObject('OtherMoney')->getTable());
|
||||
$this->assertEquals('DBCompositeTest_SubclassedDBFieldObject', $object2->dbObject('OverriddenMoney')->getTable());
|
||||
}
|
||||
|
||||
public function testSetFieldDynamicPropertyException()
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage(implode(' ', [
|
||||
'Field abc does not exist.',
|
||||
'If this was accessed via a dynamic property then call setDynamicData() instead.'
|
||||
]));
|
||||
$object = new DBCompositeTest\TestObject();
|
||||
$object->MyMoney->abc = 'def';
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ use SilverStripe\ORM\FieldType\DBTime;
|
||||
use SilverStripe\ORM\FieldType\DBVarchar;
|
||||
use SilverStripe\ORM\FieldType\DBText;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\ORM\FieldType\DBField;
|
||||
use SilverStripe\ORM\FieldType\DBYear;
|
||||
|
||||
/**
|
||||
@ -34,6 +35,9 @@ use SilverStripe\ORM\FieldType\DBYear;
|
||||
*/
|
||||
class DBFieldTest extends SapphireTest
|
||||
{
|
||||
protected static $extra_dataobjects = [
|
||||
DBFieldTest\TestDataObject::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* Test the nullValue() method on DBField.
|
||||
@ -322,4 +326,73 @@ class DBFieldTest extends SapphireTest
|
||||
$this->assertEquals('<P>ÅÄÖ</P>', DBHTMLText::create_field('HTMLFragment', '<p>åäö</p>')->UpperCase());
|
||||
$this->assertEquals('<p>åäö</p>', DBHTMLText::create_field('HTMLFragment', '<p>ÅÄÖ</p>')->LowerCase());
|
||||
}
|
||||
|
||||
public function testSaveInto()
|
||||
{
|
||||
$obj = new DBFieldTest\TestDataObject();
|
||||
/** @var DBField $field */
|
||||
$field = $obj->dbObject('Title');
|
||||
$field->setValue('New Value');
|
||||
$field->saveInto($obj);
|
||||
|
||||
$this->assertEquals('New Value', $obj->getField('Title'));
|
||||
$this->assertEquals(1, $field->saveIntoCalledCount);
|
||||
$this->assertEquals(1, $obj->setFieldCalledCount);
|
||||
}
|
||||
|
||||
public function testSaveIntoNoRecursion()
|
||||
{
|
||||
$obj = new DBFieldTest\TestDataObject();
|
||||
/** @var DBField $field */
|
||||
$field = $obj->dbObject('Title');
|
||||
$value = new DBFieldTest\TestDbField('Title');
|
||||
$value->setValue('New Value');
|
||||
$field->setValue($value);
|
||||
$field->saveInto($obj);
|
||||
|
||||
$this->assertEquals('New Value', $obj->getField('Title'));
|
||||
$this->assertEquals(1, $field->saveIntoCalledCount);
|
||||
$this->assertEquals(1, $obj->setFieldCalledCount);
|
||||
}
|
||||
|
||||
public function testSaveIntoAsProperty()
|
||||
{
|
||||
$obj = new DBFieldTest\TestDataObject();
|
||||
/** @var DBField $field */
|
||||
$field = $obj->dbObject('Title');
|
||||
$field->setValue('New Value');
|
||||
$obj->Title = $field;
|
||||
|
||||
$this->assertEquals('New Value', $obj->getField('Title'));
|
||||
$this->assertEquals(1, $field->saveIntoCalledCount);
|
||||
// Called twice because $obj->setField($field) => $field->saveInto() => $obj->setField('New Value')
|
||||
$this->assertEquals(2, $obj->setFieldCalledCount);
|
||||
}
|
||||
|
||||
public function testSaveIntoNoRecursionAsProperty()
|
||||
{
|
||||
$obj = new DBFieldTest\TestDataObject();
|
||||
/** @var DBField $field */
|
||||
$field = $obj->dbObject('Title');
|
||||
$value = new DBFieldTest\TestDbField('Title');
|
||||
$value->setValue('New Value');
|
||||
$field->setValue($value);
|
||||
$obj->Title = $field;
|
||||
|
||||
$this->assertEquals('New Value', $obj->getField('Title'));
|
||||
$this->assertEquals(1, $field->saveIntoCalledCount);
|
||||
// Called twice because $obj->setField($field) => $field->saveInto() => $obj->setField('New Value')
|
||||
$this->assertEquals(2, $obj->setFieldCalledCount);
|
||||
}
|
||||
|
||||
public function testSaveIntoRespectsSetters()
|
||||
{
|
||||
$obj = new DBFieldTest\TestDataObject();
|
||||
/** @var DBField $field */
|
||||
$field = $obj->dbObject('MyTestField');
|
||||
$field->setValue('New Value');
|
||||
$obj->MyTestField = $field;
|
||||
|
||||
$this->assertEquals('new value', $obj->getField('MyTestField'));
|
||||
}
|
||||
}
|
||||
|
29
tests/php/ORM/DBFieldTest/TestDataObject.php
Normal file
29
tests/php/ORM/DBFieldTest/TestDataObject.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\ORM\Tests\DBFieldTest;
|
||||
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class TestDataObject extends DataObject implements TestOnly
|
||||
{
|
||||
private static $table_name = 'DBFieldTest_TestDataObject';
|
||||
|
||||
private static $db = [
|
||||
'Title' => TestDbField::class,
|
||||
'MyTestField' => TestDbField::class,
|
||||
];
|
||||
|
||||
public $setFieldCalledCount = 0;
|
||||
|
||||
public function setField($fieldName, $val)
|
||||
{
|
||||
$this->setFieldCalledCount++;
|
||||
return parent::setField($fieldName, $val);
|
||||
}
|
||||
|
||||
public function setMyTestField($val)
|
||||
{
|
||||
return $this->setField('MyTestField', strtolower($val));
|
||||
}
|
||||
}
|
42
tests/php/ORM/DBFieldTest/TestDbField.php
Normal file
42
tests/php/ORM/DBFieldTest/TestDbField.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\ORM\Tests\DBFieldTest;
|
||||
|
||||
use SilverStripe\Core\Config\Config;
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\ORM\DB;
|
||||
use SilverStripe\ORM\FieldType\DBField;
|
||||
|
||||
class TestDbField extends DBField implements TestOnly
|
||||
{
|
||||
public function requireField()
|
||||
{
|
||||
// Basically the same as DBVarchar but we don't want to test with DBVarchar in case something
|
||||
// changes in that class eventually.
|
||||
$charset = Config::inst()->get(MySQLDatabase::class, 'charset');
|
||||
$collation = Config::inst()->get(MySQLDatabase::class, 'collation');
|
||||
|
||||
$parts = [
|
||||
'datatype' => 'varchar',
|
||||
'precision' => 255,
|
||||
'character set' => $charset,
|
||||
'collate' => $collation,
|
||||
'arrayValue' => $this->arrayValue
|
||||
];
|
||||
|
||||
$values = [
|
||||
'type' => 'varchar',
|
||||
'parts' => $parts
|
||||
];
|
||||
|
||||
DB::require_field($this->tableName, $this->name, $values);
|
||||
}
|
||||
|
||||
public $saveIntoCalledCount = 0;
|
||||
|
||||
public function saveInto($dataObject)
|
||||
{
|
||||
$this->saveIntoCalledCount++;
|
||||
return parent::saveInto($dataObject);
|
||||
}
|
||||
}
|
@ -1958,8 +1958,6 @@ class DataListTest extends SapphireTest
|
||||
/**
|
||||
* Test passing scalar values to sort()
|
||||
*
|
||||
* Explicity tests that sort(null) will wipe any existing sort on a DataList
|
||||
*
|
||||
* @dataProvider provideSortScalarValues
|
||||
*/
|
||||
public function testSortScalarValues(mixed $emtpyValue, string $type): void
|
||||
@ -1967,16 +1965,14 @@ class DataListTest extends SapphireTest
|
||||
$this->assertSame(['Subteam 1'], Team::get()->limit(1)->column('Title'));
|
||||
$list = Team::get()->sort('Title DESC');
|
||||
$this->assertSame(['Team 3'], $list->limit(1)->column('Title'));
|
||||
if ($type !== 'wipes-existing') {
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
}
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
if ($type === 'invalid-scalar') {
|
||||
$this->expectExceptionMessage('sort() arguments must either be a string, an array, or null');
|
||||
}
|
||||
if ($type === 'empty-scalar') {
|
||||
$this->expectExceptionMessage('Invalid sort parameter');
|
||||
}
|
||||
// $type === 'wipes-existing' is valid
|
||||
|
||||
$list = $list->sort($emtpyValue);
|
||||
$this->assertSame(['Subteam 1'], $list->limit(1)->column('Title'));
|
||||
}
|
||||
@ -1984,7 +1980,6 @@ class DataListTest extends SapphireTest
|
||||
public function provideSortScalarValues(): array
|
||||
{
|
||||
return [
|
||||
[null, 'wipes-existing'],
|
||||
['', 'empty-scalar'],
|
||||
[[], 'empty-scalar'],
|
||||
[false, 'invalid-scalar'],
|
||||
@ -1994,6 +1989,27 @@ class DataListTest extends SapphireTest
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicity tests that sort(null) will wipe any existing sort on a DataList
|
||||
*/
|
||||
public function testSortNull(): void
|
||||
{
|
||||
$list = Team::get()->sort('Title DESC');
|
||||
$query = $list->dataQuery()->getFinalisedQuery();
|
||||
$this->assertSame(
|
||||
['"DataObjectTest_Team"."Title"' => 'DESC'],
|
||||
$query->getOrderBy(),
|
||||
'Calling sort on a DataList sets an Orderby on the underlying query.'
|
||||
);
|
||||
|
||||
$list = $list->sort(null);
|
||||
$query = $list->dataQuery()->getFinalisedQuery();
|
||||
$this->assertEmpty(
|
||||
$query->getOrderBy(),
|
||||
'Calling sort with null on a DataList unsets the orderby on the underlying query.'
|
||||
);
|
||||
}
|
||||
|
||||
public function testShuffle()
|
||||
{
|
||||
$list = Team::get()->shuffle();
|
||||
|
@ -66,6 +66,7 @@ class DataObjectTest extends SapphireTest
|
||||
DataObjectTest\TreeNode::class,
|
||||
DataObjectTest\OverriddenDataObject::class,
|
||||
DataObjectTest\InjectedDataObject::class,
|
||||
DataObjectTest\SettersAndGetters::class,
|
||||
];
|
||||
|
||||
protected function setUp(): void
|
||||
@ -2667,4 +2668,14 @@ class DataObjectTest extends SapphireTest
|
||||
$vals = ['25.25', '50.00', '75.00', '100.50'];
|
||||
$this->assertSame(array_combine($vals ?? [], $vals ?? []), $obj->dbObject('MyEnumWithDots')->enumValues());
|
||||
}
|
||||
|
||||
public function testSettersAndGettersAreRespected()
|
||||
{
|
||||
$obj = new DataObjectTest\SettersAndGetters();
|
||||
$obj->MyTestField = 'Some Value';
|
||||
// Setter overrides it with all lower case
|
||||
$this->assertSame('some value', $obj->getField('MyTestField'));
|
||||
// Getter overrides it with all upper case
|
||||
$this->assertSame('SOME VALUE', $obj->MyTestField);
|
||||
}
|
||||
}
|
||||
|
25
tests/php/ORM/DataObjectTest/SettersAndGetters.php
Normal file
25
tests/php/ORM/DataObjectTest/SettersAndGetters.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\ORM\Tests\DataObjectTest;
|
||||
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class SettersAndGetters extends DataObject implements TestOnly
|
||||
{
|
||||
private static $table_name = 'DataObjectTest_SettersAndGetters';
|
||||
|
||||
private static $db = [
|
||||
'MyTestField' => 'Varchar(255)',
|
||||
];
|
||||
|
||||
public function setMyTestField($val)
|
||||
{
|
||||
$this->setField('MyTestField', strtolower($val));
|
||||
}
|
||||
|
||||
public function getMyTestField()
|
||||
{
|
||||
return strtoupper($this->getField('MyTestField'));
|
||||
}
|
||||
}
|
@ -1831,7 +1831,7 @@ class MemberTest extends FunctionalTest
|
||||
$result = Member::mapInCMSGroups($groups);
|
||||
$this->assertInstanceOf(Map::class, $result);
|
||||
|
||||
$this->assertSame($expectedUsers, $result->keys());
|
||||
$this->assertEqualsCanonicalizing($expectedUsers, $result->keys());
|
||||
}
|
||||
|
||||
public function provideMapInCMSGroups()
|
||||
|
@ -267,4 +267,15 @@ class ViewableDataTest extends SapphireTest
|
||||
$output = $reflectionMethod->invokeArgs(new ViewableData(), ['objCache']);
|
||||
$this->assertTrue($output, 'Property should be accessible');
|
||||
}
|
||||
|
||||
public function testDynamicData()
|
||||
{
|
||||
$obj = (object) ['SomeField' => [1, 2, 3]];
|
||||
$viewableData = new ViewableData();
|
||||
$this->assertFalse($viewableData->hasDynamicData('abc'));
|
||||
$viewableData->setDynamicData('abc', $obj);
|
||||
$this->assertTrue($viewableData->hasDynamicData('abc'));
|
||||
$this->assertSame($obj, $viewableData->getDynamicData('abc'));
|
||||
$this->assertSame($obj, $viewableData->abc);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user