From c4804073617f7c9e03493ba849eacc277d017216 Mon Sep 17 00:00:00 2001 From: Steve Boyd Date: Fri, 4 Oct 2024 16:56:20 +1300 Subject: [PATCH] NEW Validate DBFields --- _config/model.yml | 6 + composer.json | 1 + src/Core/Validation/ConstraintValidator.php | 4 +- .../AbstractSymfonyFieldValidator.php | 48 ++++ .../FieldValidation/BigIntFieldValidator.php | 37 ++++ .../BooleanIntFieldValidator.php | 23 ++ .../CompositeFieldValidator.php | 33 +++ .../FieldValidation/DateFieldValidator.php | 35 +++ .../DatetimeFieldValidator.php | 23 ++ .../FieldValidation/DecimalFieldValidator.php | 61 ++++++ .../FieldValidation/EmailFieldValidator.php | 19 ++ .../FieldValidation/EnumFieldValidator.php | 27 +++ .../FieldValidationInterface.php | 14 ++ .../FieldValidation/FieldValidator.php | 44 ++++ .../FieldValidation/FieldValidatorsTrait.php | 143 ++++++++++++ .../FieldValidation/IntFieldValidator.php | 47 ++++ .../FieldValidation/IpFieldValidator.php | 30 +++ .../FieldValidation/LocaleFieldValidator.php | 22 ++ .../MultiEnumFieldValidator.php | 31 +++ .../FieldValidation/NumericFieldValidator.php | 49 +++++ .../FieldValidation/StringFieldValidator.php | 70 ++++++ .../FieldValidation/TimeFieldValidator.php | 23 ++ .../FieldValidation/UrlFieldValidator.php | 19 ++ src/Core/Validation/ValidationInterface.php | 10 + src/Core/Validation/ValidationResult.php | 43 +++- src/Forms/CompositeField.php | 4 +- src/Forms/EmailField.php | 28 +-- src/Forms/FieldGroup.php | 2 +- src/Forms/FormField.php | 37 +++- src/Forms/SelectionGroup_Item.php | 2 +- src/Forms/TextField.php | 31 +-- src/ORM/DataObject.php | 9 + src/ORM/FieldType/DBBigInt.php | 13 +- src/ORM/FieldType/DBBoolean.php | 22 +- src/ORM/FieldType/DBComposite.php | 5 + src/ORM/FieldType/DBDate.php | 65 ++---- src/ORM/FieldType/DBDatetime.php | 11 +- src/ORM/FieldType/DBDecimal.php | 15 ++ src/ORM/FieldType/DBEmail.php | 29 +++ src/ORM/FieldType/DBEnum.php | 5 + src/ORM/FieldType/DBField.php | 9 +- src/ORM/FieldType/DBInt.php | 45 +++- src/ORM/FieldType/DBIp.php | 13 ++ src/ORM/FieldType/DBLocale.php | 5 + src/ORM/FieldType/DBMultiEnum.php | 14 ++ src/ORM/FieldType/DBPolymorphicForeignKey.php | 6 + src/ORM/FieldType/DBString.php | 2 + src/ORM/FieldType/DBTime.php | 11 +- src/ORM/FieldType/DBUrl.php | 22 ++ src/ORM/FieldType/DBVarchar.php | 5 + .../BigIntFieldValidatorTest.php | 75 +++++++ .../BooleanIntFieldValidatorTest.php | 72 ++++++ .../CompositeFieldValidatorTest.php | 76 +++++++ .../DateFieldValidatorTest.php | 48 ++++ .../DatetimeFieldValidatorTest.php | 52 +++++ .../DecimalFieldValidatorTest.php | 138 ++++++++++++ .../EmailFieldValidatorTest.php | 37 ++++ .../EnumFieldValidatorTest.php | 54 +++++ .../FieldValidation/FieldValidatorTest.php | 43 ++++ .../FieldValidation/IntFieldValidatorTest.php | 76 +++++++ .../FieldValidation/IpFieldValidatorTest.php | 45 ++++ .../LocaleFieldValidatorTest.php | 61 ++++++ .../MultiEnumFieldValidatorTest.php | 84 +++++++ .../NumericFieldValidatorTest.php | 80 +++++++ .../StringFieldValidatorTest.php | 149 +++++++++++++ .../TimeFieldValidatorTest.php | 48 ++++ .../FieldValidation/UrlFieldValidatorTest.php | 45 ++++ tests/php/ORM/DBFieldValidatorsTest.php | 205 ++++++++++++++++++ tests/php/ORM/DBIntTest.php | 65 +++++- 69 files changed, 2584 insertions(+), 141 deletions(-) create mode 100644 src/Core/Validation/FieldValidation/AbstractSymfonyFieldValidator.php create mode 100644 src/Core/Validation/FieldValidation/BigIntFieldValidator.php create mode 100644 src/Core/Validation/FieldValidation/BooleanIntFieldValidator.php create mode 100644 src/Core/Validation/FieldValidation/CompositeFieldValidator.php create mode 100644 src/Core/Validation/FieldValidation/DateFieldValidator.php create mode 100644 src/Core/Validation/FieldValidation/DatetimeFieldValidator.php create mode 100644 src/Core/Validation/FieldValidation/DecimalFieldValidator.php create mode 100644 src/Core/Validation/FieldValidation/EmailFieldValidator.php create mode 100644 src/Core/Validation/FieldValidation/EnumFieldValidator.php create mode 100644 src/Core/Validation/FieldValidation/FieldValidationInterface.php create mode 100644 src/Core/Validation/FieldValidation/FieldValidator.php create mode 100644 src/Core/Validation/FieldValidation/FieldValidatorsTrait.php create mode 100644 src/Core/Validation/FieldValidation/IntFieldValidator.php create mode 100644 src/Core/Validation/FieldValidation/IpFieldValidator.php create mode 100644 src/Core/Validation/FieldValidation/LocaleFieldValidator.php create mode 100644 src/Core/Validation/FieldValidation/MultiEnumFieldValidator.php create mode 100644 src/Core/Validation/FieldValidation/NumericFieldValidator.php create mode 100644 src/Core/Validation/FieldValidation/StringFieldValidator.php create mode 100644 src/Core/Validation/FieldValidation/TimeFieldValidator.php create mode 100644 src/Core/Validation/FieldValidation/UrlFieldValidator.php create mode 100644 src/Core/Validation/ValidationInterface.php create mode 100644 src/ORM/FieldType/DBEmail.php create mode 100644 src/ORM/FieldType/DBIp.php create mode 100644 src/ORM/FieldType/DBUrl.php create mode 100644 tests/php/Core/Validation/FieldValidation/BigIntFieldValidatorTest.php create mode 100644 tests/php/Core/Validation/FieldValidation/BooleanIntFieldValidatorTest.php create mode 100644 tests/php/Core/Validation/FieldValidation/CompositeFieldValidatorTest.php create mode 100644 tests/php/Core/Validation/FieldValidation/DateFieldValidatorTest.php create mode 100644 tests/php/Core/Validation/FieldValidation/DatetimeFieldValidatorTest.php create mode 100644 tests/php/Core/Validation/FieldValidation/DecimalFieldValidatorTest.php create mode 100644 tests/php/Core/Validation/FieldValidation/EmailFieldValidatorTest.php create mode 100644 tests/php/Core/Validation/FieldValidation/EnumFieldValidatorTest.php create mode 100644 tests/php/Core/Validation/FieldValidation/FieldValidatorTest.php create mode 100644 tests/php/Core/Validation/FieldValidation/IntFieldValidatorTest.php create mode 100644 tests/php/Core/Validation/FieldValidation/IpFieldValidatorTest.php create mode 100644 tests/php/Core/Validation/FieldValidation/LocaleFieldValidatorTest.php create mode 100644 tests/php/Core/Validation/FieldValidation/MultiEnumFieldValidatorTest.php create mode 100644 tests/php/Core/Validation/FieldValidation/NumericFieldValidatorTest.php create mode 100644 tests/php/Core/Validation/FieldValidation/StringFieldValidatorTest.php create mode 100644 tests/php/Core/Validation/FieldValidation/TimeFieldValidatorTest.php create mode 100644 tests/php/Core/Validation/FieldValidation/UrlFieldValidatorTest.php create mode 100644 tests/php/ORM/DBFieldValidatorsTest.php diff --git a/_config/model.yml b/_config/model.yml index 4046feddb..ca4d8e193 100644 --- a/_config/model.yml +++ b/_config/model.yml @@ -20,6 +20,8 @@ SilverStripe\Core\Injector\Injector: class: SilverStripe\ORM\FieldType\DBDecimal Double: class: SilverStripe\ORM\FieldType\DBDouble + Email: + class: SilverStripe\ORM\FieldType\DBEmail Enum: class: SilverStripe\ORM\FieldType\DBEnum Float: @@ -36,6 +38,8 @@ SilverStripe\Core\Injector\Injector: class: SilverStripe\ORM\FieldType\DBHTMLVarchar Int: class: SilverStripe\ORM\FieldType\DBInt + IP: + class: SilverStripe\ORM\FieldType\DBIp BigInt: class: SilverStripe\ORM\FieldType\DBBigInt Locale: @@ -58,6 +62,8 @@ SilverStripe\Core\Injector\Injector: class: SilverStripe\ORM\FieldType\DBText Time: class: SilverStripe\ORM\FieldType\DBTime + URL: + class: SilverStripe\ORM\FieldType\DBUrl Varchar: class: SilverStripe\ORM\FieldType\DBVarchar Year: diff --git a/composer.json b/composer.json index f4ac18e47..e51556301 100644 --- a/composer.json +++ b/composer.json @@ -47,6 +47,7 @@ "symfony/dom-crawler": "^7.0", "symfony/filesystem": "^7.0", "symfony/http-foundation": "^7.0", + "symfony/intl": "^7.0", "symfony/mailer": "^7.0", "symfony/mime": "^7.0", "symfony/translation": "^7.0", diff --git a/src/Core/Validation/ConstraintValidator.php b/src/Core/Validation/ConstraintValidator.php index 9a6568998..941a18a8d 100644 --- a/src/Core/Validation/ConstraintValidator.php +++ b/src/Core/Validation/ConstraintValidator.php @@ -35,9 +35,9 @@ class ConstraintValidator /** @var ConstraintViolationInterface $violation */ foreach ($violations as $violation) { if ($fieldName) { - $result->addFieldError($fieldName, $violation->getMessage()); + $result->addFieldError($fieldName, $violation->getMessage(), value: $value); } else { - $result->addError($violation->getMessage()); + $result->addError($violation->getMessage(), value: $value); } } diff --git a/src/Core/Validation/FieldValidation/AbstractSymfonyFieldValidator.php b/src/Core/Validation/FieldValidation/AbstractSymfonyFieldValidator.php new file mode 100644 index 000000000..195bd0eb5 --- /dev/null +++ b/src/Core/Validation/FieldValidation/AbstractSymfonyFieldValidator.php @@ -0,0 +1,48 @@ +isValid()) { + return $result; + } + $constraintClass = $this->getConstraintClass(); + $args = [ + ...$this->getContraintNamedArgs(), + 'message' => $this->getMessage(), + ]; + $constraint = new $constraintClass(...$args); + $validationResult = ConstraintValidator::validate($this->value, $constraint, $this->name); + return $result->combineAnd($validationResult); + } + + /** + * The symfony constraint class to use + */ + abstract protected function getConstraintClass(): string; + + /** + * The named args to pass to the constraint + * Defined named args as assoc array keys + */ + protected function getContraintNamedArgs(): array + { + return []; + } + + /** + * The message to use when the value is invalid + */ + abstract protected function getMessage(): string; +} diff --git a/src/Core/Validation/FieldValidation/BigIntFieldValidator.php b/src/Core/Validation/FieldValidation/BigIntFieldValidator.php new file mode 100644 index 000000000..f345f9c86 --- /dev/null +++ b/src/Core/Validation/FieldValidation/BigIntFieldValidator.php @@ -0,0 +1,37 @@ +value !== 1 && $this->value !== 0) { + $message = _t(__CLASS__ . '.INVALID', 'Invalid value'); + $result->addFieldError($this->name, $message, value: $this->value); + } + return $result; + } +} diff --git a/src/Core/Validation/FieldValidation/CompositeFieldValidator.php b/src/Core/Validation/FieldValidation/CompositeFieldValidator.php new file mode 100644 index 000000000..e080af2d7 --- /dev/null +++ b/src/Core/Validation/FieldValidation/CompositeFieldValidator.php @@ -0,0 +1,33 @@ +value as $child) { + $result->combineAnd($child->validate()); + } + return $result; + } +} diff --git a/src/Core/Validation/FieldValidation/DateFieldValidator.php b/src/Core/Validation/FieldValidation/DateFieldValidator.php new file mode 100644 index 000000000..cec0d1c3f --- /dev/null +++ b/src/Core/Validation/FieldValidation/DateFieldValidator.php @@ -0,0 +1,35 @@ +getFormat(), $this->value ?? ''); + if ($date === false || $date['error_count'] > 0 || $date['warning_count'] > 0) { + $result->addError($this->name, $this->getMessage()); + } + return $result; + } + + protected function getFormat(): string + { + return 'Y-m-d'; + } + + protected function getMessage(): string + { + return _t(__CLASS__ . '.INVALID', 'Invalid date'); + } +} diff --git a/src/Core/Validation/FieldValidation/DatetimeFieldValidator.php b/src/Core/Validation/FieldValidation/DatetimeFieldValidator.php new file mode 100644 index 000000000..855f8fb56 --- /dev/null +++ b/src/Core/Validation/FieldValidation/DatetimeFieldValidator.php @@ -0,0 +1,23 @@ +wholeSize = $wholeSize; + $this->decimalSize = $decimalSize; + } + + protected function validateValue(): ValidationResult + { + $result = parent::validateValue(); + if (!$result->isValid()) { + return $result; + } + // Example of how digits are stored in the database + // Decimal(5,2) is allowed a total of 5 digits, and will always round to 2 decimal places + // This means it has a maximum 3 digits before the decimal point + // + // Valid + // 123.99 + // 999.99 + // -999.99 + // 123.999 - will round to 124.00 + // + // Not valid + // 1234.9 - 4 digits the before the decimal point + // 999.999 - would be rounted to 10000000.00 which exceeds the 9 digits + + // Convert to absolute value - any the minus sign is not counted + $absValue = abs($this->value); + // Round to the decimal size which is what the database will do + $rounded = round($absValue, $this->decimalSize); + // Get formatted as a string, which will right pad with zeros to the decimal size + $rounded = number_format($rounded, $this->decimalSize, thousands_separator: ''); + // Count this number of digits - the minus 1 is for the decimal point + $digitCount = strlen((string) $rounded) - 1; + if ($digitCount > $this->wholeSize) { + $message = _t(__CLASS__ . '.TOOLARGE', 'Number is too large'); + $result->addFieldError($this->name, $message, value: $this->value); + } + return $result; + } +} diff --git a/src/Core/Validation/FieldValidation/EmailFieldValidator.php b/src/Core/Validation/FieldValidation/EmailFieldValidator.php new file mode 100644 index 000000000..acd9ee737 --- /dev/null +++ b/src/Core/Validation/FieldValidation/EmailFieldValidator.php @@ -0,0 +1,19 @@ +allowedValues = $allowedValues; + } + + protected function validateValue(): ValidationResult + { + $result = ValidationResult::create(); + if (!in_array($this->value, $this->allowedValues, true)) { + $message = _t(__CLASS__ . '.NOTALLOWED', 'Not an allowed value'); + $result->addFieldError($this->name, $message, value: $this->value); + } + return $result; + } +} diff --git a/src/Core/Validation/FieldValidation/FieldValidationInterface.php b/src/Core/Validation/FieldValidation/FieldValidationInterface.php new file mode 100644 index 000000000..14bb13862 --- /dev/null +++ b/src/Core/Validation/FieldValidation/FieldValidationInterface.php @@ -0,0 +1,14 @@ +name = $name; + $this->value = $value; + $this->skipIfNull = $skipIfNull; + } + + /** + * Validate the value + */ + public function validate(): ValidationResult + { + $result = ValidationResult::create(); + if ($this->value === null && $this->skipIfNull) { + return $result; + } + $validationResult = $this->validateValue($result); + if (!$validationResult->isValid()) { + $result->combineAnd($validationResult); + } + return $result; + } + + /** + * Inner validatation method that that is implemented by subclasses + */ + abstract protected function validateValue(): ValidationResult; +} diff --git a/src/Core/Validation/FieldValidation/FieldValidatorsTrait.php b/src/Core/Validation/FieldValidation/FieldValidatorsTrait.php new file mode 100644 index 000000000..469fb7168 --- /dev/null +++ b/src/Core/Validation/FieldValidation/FieldValidatorsTrait.php @@ -0,0 +1,143 @@ + [null, 'getMyArg'], + * c) MyFieldValidator::class => null, + * + * a) Will create a FieldValidator and pass the name and value of the field as args to the constructor + * b) Will create a FieldValidator and pass the name, value, make a pass additional args, calling each + * non-null value on the field e.g. it will skip the first arg and call $field->getMyArg() for the second arg + * c) Will disable a previously set FieldValidator. This is useful to disable a FieldValidator that was set + * on a parent class + * + * You may only have a single instance of a FieldValidator class per field + */ + private static array $field_validators = []; + + /** + * Used by FieldValidator to skip validation if the field is null + */ + protected bool $skipValidationIfNull = false; + + /** + * Get whether this field should skip validation if it is null + * There is intentionally no setter for this + */ + public function getSkipValidationIfNull(): bool + { + return $this->skipValidationIfNull; + } + + /** + * Get the value of this field for field validation + * Override this method in your class to return the value you want to validate + * If it's different from what's normally returned in getValue(); + */ + public function getValueForValidation(): mixed + { + return $this->getValue(); + } + + /** + * Validate this field + */ + public function validate(): ValidationResult + { + $result = ValidationResult::create(); + $fieldValidators = $this->getFieldValidators(); + foreach ($fieldValidators as $fieldValidator) { + $validationResult = $fieldValidator->validate(); + if (!$validationResult->isValid()) { + $result->combineAnd($validationResult); + } + } + return $result; + } + + /** + * Get FieldValidators based on `field_validators` configuration + */ + private function getFieldValidators(): array + { + $fieldValidators = []; + // Used to disable a validator that was previously set with an int index + $disabledClasses = []; + $interface = FieldValidationInterface::class; + // temporary check, will make FormField implement FieldValidationInterface in a future PR + $tmp = FormField::class; + if (!is_a($this, $interface) && !is_a($this, $tmp)) { + $class = get_class($this); + throw new RuntimeException("Class $class does not implement interface $interface"); + } + /** @var FieldValidationInterface|Configurable $this */ + $name = $this->getName(); + $value = $this->getValueForValidation(); + $skipIfNull = $this->getSkipValidationIfNull(); + // Field name is required for FieldValidators when called ValidationResult::addFieldMessage() + if ($name === '') { + throw new RuntimeException('Field name is blank'); + } + $classes = []; + $config = $this->config()->get('field_validators'); + foreach ($config as $indexOrClass => $classOrArgCallsOrDisable) { + $class = ''; + $argCalls = []; + $disable = false; + if (is_int($indexOrClass)) { + $class = $classOrArgCallsOrDisable; + } else { + $class = $indexOrClass; + $argCalls = $classOrArgCallsOrDisable; + $disable = $classOrArgCallsOrDisable === null; + } + if ($disable) { + $disabledClasses[$class] = true; + continue; + } else { + if (isset($disabledClasses[$class])) { + unset($disabledClasses[$class]); + } + } + if (!is_a($class, FieldValidator::class, true)) { + throw new RuntimeException("Class $class is not a FieldValidator"); + } + if (!is_array($argCalls)) { + throw new RuntimeException("argCalls for FieldValidator $class is not an array"); + } + $classes[$class] = $argCalls; + } + foreach (array_keys($disabledClasses) as $class) { + unset($classes[$class]); + } + foreach ($classes as $class => $argCalls) { + $args = [$name, $value, $skipIfNull]; + foreach ($argCalls as $i => $argCall) { + if (!is_string($argCall) && !is_null($argCall)) { + throw new RuntimeException("argCall $i for FieldValidator $class is not a string or null"); + } + if ($argCall) { + $args[] = call_user_func([$this, $argCall]); + } else { + $args[] = null; + } + } + $fieldValidators[$class] = Injector::inst()->createWithArgs($class, $args); + } + return array_values($fieldValidators); + } +} diff --git a/src/Core/Validation/FieldValidation/IntFieldValidator.php b/src/Core/Validation/FieldValidation/IntFieldValidator.php new file mode 100644 index 000000000..51ee8946c --- /dev/null +++ b/src/Core/Validation/FieldValidation/IntFieldValidator.php @@ -0,0 +1,47 @@ +value)) { + $message = _t(__CLASS__ . '.NOTINT', 'Not an integer'); + $result->addFieldError($this->name, $message, value: $this->value); + } + return $result; + } +} diff --git a/src/Core/Validation/FieldValidation/IpFieldValidator.php b/src/Core/Validation/FieldValidation/IpFieldValidator.php new file mode 100644 index 000000000..789eb8fcf --- /dev/null +++ b/src/Core/Validation/FieldValidation/IpFieldValidator.php @@ -0,0 +1,30 @@ + Constraints\Ip::ALL, + ]; + } + + protected function getMessage(): string + { + return _t(__CLASS__ . '.INVALID', 'Invalid IP address'); + } +} diff --git a/src/Core/Validation/FieldValidation/LocaleFieldValidator.php b/src/Core/Validation/FieldValidation/LocaleFieldValidator.php new file mode 100644 index 000000000..accdf8a7c --- /dev/null +++ b/src/Core/Validation/FieldValidation/LocaleFieldValidator.php @@ -0,0 +1,22 @@ +value as $value) { + if (!in_array($value, $this->allowedValues, true)) { + $message = _t(__CLASS__ . '.NOTALLOWED', 'Not an allowed value'); + $result->addFieldError($this->name, $message, value: $value); + break; + } + } + return $result; + } +} diff --git a/src/Core/Validation/FieldValidation/NumericFieldValidator.php b/src/Core/Validation/FieldValidation/NumericFieldValidator.php new file mode 100644 index 000000000..54c1c9d1f --- /dev/null +++ b/src/Core/Validation/FieldValidation/NumericFieldValidator.php @@ -0,0 +1,49 @@ +minValue = $minValue; + $this->maxValue = $maxValue; + parent::__construct($name, $value, $skipIfNull); + } + + protected function validateValue(): ValidationResult + { + $result = ValidationResult::create(); + if (!is_numeric($this->value) || is_string($this->value)) { + // Must be a numeric value, though not as a numeric string + $message = _t(__CLASS__ . '.NOTNUMERIC', 'Must be a number'); + $result->addFieldError($this->name, $message, value: $this->value); + return $result; + } elseif (isset($this->minValue) && $this->value < $this->minValue) { + $message = _t(__CLASS__ . '.TOOSMALL', 'Value is too small'); + $result->addFieldError($this->name, $message, value: $this->value); + } elseif (isset($this->maxValue) && $this->value > $this->maxValue) { + $message = _t(__CLASS__ . '.TOOLARGE', 'Value is too large'); + $result->addFieldError($this->name, $message, value: $this->value); + } + return $result; + } +} diff --git a/src/Core/Validation/FieldValidation/StringFieldValidator.php b/src/Core/Validation/FieldValidation/StringFieldValidator.php new file mode 100644 index 000000000..09bac79ef --- /dev/null +++ b/src/Core/Validation/FieldValidation/StringFieldValidator.php @@ -0,0 +1,70 @@ +minLength = $minLength; + $this->maxLength = $maxLength; + } + + protected function validateValue(): ValidationResult + { + $result = ValidationResult::create(); + if (!is_string($this->value)) { + $message = _t(__CLASS__ . '.INVALID', 'Must be a string'); + $result->addFieldError($this->name, $message, value: $this->value); + return $result; + } + // Blank strings are valid, even if there's a minLength requirement + if ($this->value === '') { + return $result; + } + $len = mb_strlen($this->value); + if (!is_null($this->minLength) && $len < $this->minLength) { + $message = _t( + __CLASS__ . '.TOOSHORT', + 'Must have at least {minLength} characters', + ['minLength' => $this->minLength] + ); + $result->addFieldError($this->name, $message, value: $this->value); + } + if (!is_null($this->maxLength) && $len > $this->maxLength) { + $message = _t( + __CLASS__ . '.TOOLONG', + 'Can not have more than {maxLength} characters', + ['maxLength' => $this->maxLength] + ); + $result->addFieldError($this->name, $message, value: $this->value); + } + return $result; + } +} diff --git a/src/Core/Validation/FieldValidation/TimeFieldValidator.php b/src/Core/Validation/FieldValidation/TimeFieldValidator.php new file mode 100644 index 000000000..93874ddbf --- /dev/null +++ b/src/Core/Validation/FieldValidation/TimeFieldValidator.php @@ -0,0 +1,23 @@ +addFieldError(null, $message, $messageType, $code, $cast); + public function addError( + $message, + $messageType = ValidationResult::TYPE_ERROR, + $code = null, + $cast = ValidationResult::CAST_TEXT, + $value = ValidationResult::VALUE_UNSET, + ) { + return $this->addFieldError(null, $message, $messageType, $code, $cast, $value); } /** @@ -89,6 +100,7 @@ class ValidationResult * This can be usedful for ensuring no duplicate messages * @param string|bool $cast Cast type; One of the CAST_ constant definitions. * Bool values will be treated as plain text flag. + * @param mixed $value The value that failed validation * @return $this */ public function addFieldError( @@ -96,10 +108,11 @@ class ValidationResult $message, $messageType = ValidationResult::TYPE_ERROR, $code = null, - $cast = ValidationResult::CAST_TEXT + $cast = ValidationResult::CAST_TEXT, + $value = ValidationResult::VALUE_UNSET, ) { $this->isValid = false; - return $this->addFieldMessage($fieldName, $message, $messageType, $code, $cast); + return $this->addFieldMessage($fieldName, $message, $messageType, $code, $cast, $value); } /** @@ -112,11 +125,17 @@ class ValidationResult * This can be usedful for ensuring no duplicate messages * @param string|bool $cast Cast type; One of the CAST_ constant definitions. * Bool values will be treated as plain text flag. + * @param mixed $value The value that failed validation * @return $this */ - public function addMessage($message, $messageType = ValidationResult::TYPE_ERROR, $code = null, $cast = ValidationResult::CAST_TEXT) - { - return $this->addFieldMessage(null, $message, $messageType, $code, $cast); + public function addMessage( + $message, + $messageType = ValidationResult::TYPE_ERROR, + $code = null, + $cast = ValidationResult::CAST_TEXT, + $value = ValidationResult::VALUE_UNSET, + ) { + return $this->addFieldMessage(null, $message, $messageType, $code, $cast, $value); } /** @@ -130,6 +149,7 @@ class ValidationResult * This can be usedful for ensuring no duplicate messages * @param string|bool $cast Cast type; One of the CAST_ constant definitions. * Bool values will be treated as plain text flag. + * @param mixed $value The value that failed validation * @return $this */ public function addFieldMessage( @@ -137,7 +157,8 @@ class ValidationResult $message, $messageType = ValidationResult::TYPE_ERROR, $code = null, - $cast = ValidationResult::CAST_TEXT + $cast = ValidationResult::CAST_TEXT, + $value = ValidationResult::VALUE_UNSET, ) { if ($code && is_numeric($code)) { throw new InvalidArgumentException("Don't use a numeric code '$code'. Use a string."); @@ -151,7 +172,9 @@ class ValidationResult 'messageType' => $messageType, 'messageCast' => $cast, ]; - + if ($value !== ValidationResult::VALUE_UNSET) { + $metadata['value'] = $value; + } if ($code) { $this->messages[$code] = $metadata; } else { diff --git a/src/Forms/CompositeField.php b/src/Forms/CompositeField.php index 7c1cab23f..57ea04cfb 100644 --- a/src/Forms/CompositeField.php +++ b/src/Forms/CompositeField.php @@ -119,10 +119,8 @@ class CompositeField extends FormField * Returns the name (ID) for the element. * If the CompositeField doesn't have a name, but we still want the ID/name to be set. * This code generates the ID from the nested children. - * - * @return String $name */ - public function getName() + public function getName(): string { if ($this->name) { return $this->name; diff --git a/src/Forms/EmailField.php b/src/Forms/EmailField.php index 4426d67f8..a348284e0 100644 --- a/src/Forms/EmailField.php +++ b/src/Forms/EmailField.php @@ -2,14 +2,17 @@ namespace SilverStripe\Forms; -use SilverStripe\Core\Validation\ConstraintValidator; -use Symfony\Component\Validator\Constraints\Email as EmailConstraint; +use SilverStripe\Core\Validation\FieldValidation\EmailValidator; /** * Text input field with validation for correct email format according to the relevant RFC. */ class EmailField extends TextField { + private static array $field_validators = [ + EmailValidator::class, + ]; + protected $inputType = 'email'; public function Type() @@ -17,27 +20,6 @@ class EmailField extends TextField return 'email text'; } - /** - * Validates for RFC compliant email addresses. - * - * @param Validator $validator - */ - public function validate($validator) - { - $this->value = trim($this->value ?? ''); - - $message = _t('SilverStripe\\Forms\\EmailField.VALIDATION', 'Please enter an email address'); - $result = ConstraintValidator::validate( - $this->value, - new EmailConstraint(message: $message, mode: EmailConstraint::VALIDATION_MODE_STRICT), - $this->getName() - ); - $validator->getResult()->combineAnd($result); - $isValid = $result->isValid(); - - return $this->extendValidationResult($isValid, $validator); - } - public function getSchemaValidation() { $rules = parent::getSchemaValidation(); diff --git a/src/Forms/FieldGroup.php b/src/Forms/FieldGroup.php index 9a0d6c675..f7ced1903 100644 --- a/src/Forms/FieldGroup.php +++ b/src/Forms/FieldGroup.php @@ -106,7 +106,7 @@ class FieldGroup extends CompositeField * In some cases the FieldGroup doesn't have a title, but we still want * the ID / name to be set. This code, generates the ID from the nested children */ - public function getName() + public function getName(): string { if ($this->name) { return $this->name; diff --git a/src/Forms/FormField.php b/src/Forms/FormField.php index 0d210436b..3ef4b6bf4 100644 --- a/src/Forms/FormField.php +++ b/src/Forms/FormField.php @@ -15,6 +15,7 @@ use SilverStripe\Core\Validation\ValidationResult; use SilverStripe\View\AttributesHTML; use SilverStripe\View\SSViewer; use SilverStripe\Model\ModelData; +use SilverStripe\Core\Validation\FieldValidation\FieldValidatorsTrait; /** * Represents a field in a form. @@ -44,6 +45,7 @@ class FormField extends RequestHandler { use AttributesHTML; use FormMessage; + use FieldValidatorsTrait; /** @see $schemaDataType */ const SCHEMA_DATA_TYPE_STRING = 'String'; @@ -424,12 +426,10 @@ class FormField extends RequestHandler /** * Returns the field name. - * - * @return string */ - public function getName() + public function getName(): string { - return $this->name; + return $this->name ?? ''; } /** @@ -443,12 +443,20 @@ class FormField extends RequestHandler } /** - * Returns the field value. + * Alias of getValue() * * @see FormField::setSubmittedValue() * @return mixed */ public function Value() + { + return $this->getValue(); + } + + /** + * Returns the field value. + */ + public function getValue(): mixed { return $this->value; } @@ -1231,15 +1239,28 @@ class FormField extends RequestHandler } /** - * Abstract method each {@link FormField} subclass must implement, determines whether the field - * is valid or not based on the value. + * Subclasses can define an existing FieldValidatorClass to validate the FormField value + * They may also override this method to provide custom validation logic * * @param Validator $validator * @return bool */ public function validate($validator) { - return $this->extendValidationResult(true, $validator); + $isValid = true; + $result = ValidationResult::create(); + $fieldValidators = $this->getFieldValidators(); + foreach ($fieldValidators as $fieldValidator) { + $validationResult = $fieldValidator->validate(); + if (!$validationResult->isValid()) { + $result->combineAnd($validationResult); + } + } + if (!$result->isValid()) { + $isValid = false; + $validator->getResult()->combineAnd($result); + } + return $this->extendValidationResult($isValid, $validator); } /** diff --git a/src/Forms/SelectionGroup_Item.php b/src/Forms/SelectionGroup_Item.php index 55d021859..ab23c0cc8 100644 --- a/src/Forms/SelectionGroup_Item.php +++ b/src/Forms/SelectionGroup_Item.php @@ -43,7 +43,7 @@ class SelectionGroup_Item extends CompositeField return $this; } - function getValue() + function getValue(): mixed { return $this->value; } diff --git a/src/Forms/TextField.php b/src/Forms/TextField.php index 49aa40b2c..dda142333 100644 --- a/src/Forms/TextField.php +++ b/src/Forms/TextField.php @@ -2,6 +2,8 @@ namespace SilverStripe\Forms; +use SilverStripe\Core\Validation\FieldValidation\StringFieldValidator; + /** * Text input field. */ @@ -14,6 +16,10 @@ class TextField extends FormField implements TippableFieldInterface protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_TEXT; + private static array $field_validators = [ + StringFieldValidator::class => [null, 'getMaxLength'], + ]; + /** * @var Tip|null A tip to render beside the input */ @@ -117,31 +123,6 @@ class TextField extends FormField implements TippableFieldInterface return $data; } - /** - * Validate this field - * - * @param Validator $validator - * @return bool - */ - 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( - $this->name, - _t( - 'SilverStripe\\Forms\\TextField.VALIDATEMAXLENGTH', - 'The value for {name} must not exceed {maxLength} characters in length', - ['name' => $name, 'maxLength' => $this->maxLength] - ), - "validation" - ); - $result = false; - } - return $this->extendValidationResult($result, $validator); - } - public function getSchemaValidation() { $rules = parent::getSchemaValidation(); diff --git a/src/ORM/DataObject.php b/src/ORM/DataObject.php index 2b6bed1da..8968d59c3 100644 --- a/src/ORM/DataObject.php +++ b/src/ORM/DataObject.php @@ -1230,6 +1230,15 @@ class DataObject extends ModelData implements DataObjectInterface, i18nEntityPro public function validate() { $result = ValidationResult::create(); + // Call DBField::validate() on every DBField + $specs = static::getSchema()->fieldSpecs(static::class); + foreach (array_keys($specs) as $fieldName) { + $dbField = $this->dbObject($fieldName); + $validationResult = $dbField->validate(); + if (!$validationResult->isValid()) { + $result->combineAnd($validationResult); + } + } $this->extend('updateValidate', $result); return $result; } diff --git a/src/ORM/FieldType/DBBigInt.php b/src/ORM/FieldType/DBBigInt.php index c92c2da69..9abb70c55 100644 --- a/src/ORM/FieldType/DBBigInt.php +++ b/src/ORM/FieldType/DBBigInt.php @@ -2,18 +2,24 @@ namespace SilverStripe\ORM\FieldType; +use SilverStripe\Core\Validation\FieldValidation\IntFieldValidator; +use SilverStripe\Core\Validation\FieldValidation\BigIntFieldValidator; use SilverStripe\ORM\DB; /** * Represents a signed 8 byte integer field. Do note PHP running as 32-bit might not work with Bigint properly, as it * would convert the value to a float when queried from the database since the value is a 64-bit one. * - * @package framework - * @subpackage model - * @see Int + * BigInt is always signed i.e. can be negative + * Their range is -9223372036854775808 to 9223372036854775807 */ class DBBigInt extends DBInt { + private static array $field_validators = [ + // Remove parent validator and add BigIntValidator instead + IntFieldValidator::class => null, + BigIntFieldValidator::class, + ]; public function requireField(): void { @@ -24,7 +30,6 @@ class DBBigInt extends DBInt 'default' => $this->defaultVal, 'arrayValue' => $this->arrayValue ]; - $values = ['type' => 'bigint', 'parts' => $parts]; DB::require_field($this->tableName, $this->name, $values); } diff --git a/src/ORM/FieldType/DBBoolean.php b/src/ORM/FieldType/DBBoolean.php index c4d4c1c48..daefbab2d 100644 --- a/src/ORM/FieldType/DBBoolean.php +++ b/src/ORM/FieldType/DBBoolean.php @@ -2,6 +2,7 @@ namespace SilverStripe\ORM\FieldType; +use SilverStripe\Core\Validation\FieldValidation\BooleanIntFieldValidator; use SilverStripe\Forms\CheckboxField; use SilverStripe\Forms\DropdownField; use SilverStripe\Forms\FormField; @@ -9,10 +10,15 @@ use SilverStripe\ORM\DB; use SilverStripe\Model\ModelData; /** - * Represents a boolean field. + * Represents a boolean field + * Values are stored as a tinyint i.e. 1 or 0 and NOT as true or false */ class DBBoolean extends DBField { + private static array $field_validators = [ + BooleanIntFieldValidator::class, + ]; + public function __construct(?string $name = null, bool|int $defaultVal = 0) { $this->defaultVal = ($defaultVal) ? 1 : 0; @@ -34,6 +40,13 @@ class DBBoolean extends DBField DB::require_field($this->tableName, $this->name, $values); } + public function setValue(mixed $value, null|array|ModelData $record = null, bool $markChanged = true): static + { + parent::setValue($value); + $this->value = $this->convertBooleanLikeValueToTinyInt($value); + return $this; + } + public function Nice(): string { return ($this->value) ? _t(__CLASS__ . '.YESANSWER', 'Yes') : _t(__CLASS__ . '.NOANSWER', 'No'); @@ -83,6 +96,11 @@ class DBBoolean extends DBField } public function prepValueForDB(mixed $value): array|int|null + { + return $this->convertBooleanLikeValueToTinyInt($value); + } + + private function convertBooleanLikeValueToTinyInt(mixed $value): int { if (is_bool($value)) { return $value ? 1 : 0; @@ -94,9 +112,11 @@ class DBBoolean extends DBField switch (strtolower($value ?? '')) { case 'false': case 'f': + case '0': return 0; case 'true': case 't': + case '1': return 1; } } diff --git a/src/ORM/FieldType/DBComposite.php b/src/ORM/FieldType/DBComposite.php index 7060417ea..feaba9018 100644 --- a/src/ORM/FieldType/DBComposite.php +++ b/src/ORM/FieldType/DBComposite.php @@ -8,6 +8,7 @@ use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DB; use SilverStripe\ORM\Queries\SQLSelect; use SilverStripe\Model\ModelData; +use SilverStripe\Core\Validation\FieldValidation\CompositeFieldValidator; /** * Extend this class when designing a {@link DBField} that doesn't have a 1-1 mapping with a database field. @@ -25,6 +26,10 @@ use SilverStripe\Model\ModelData; */ abstract class DBComposite extends DBField { + private static array $field_validators = [ + CompositeFieldValidator::class, + ]; + /** * Similar to {@link DataObject::$db}, * holds an array of composite field names. diff --git a/src/ORM/FieldType/DBDate.php b/src/ORM/FieldType/DBDate.php index d8a271d78..fc80269ee 100644 --- a/src/ORM/FieldType/DBDate.php +++ b/src/ORM/FieldType/DBDate.php @@ -12,6 +12,7 @@ use SilverStripe\ORM\DB; use SilverStripe\Security\Member; use SilverStripe\Security\Security; use SilverStripe\Model\ModelData; +use SilverStripe\Core\Validation\FieldValidation\DateFieldValidator; /** * Represents a date field. @@ -33,6 +34,7 @@ class DBDate extends DBField { /** * Standard ISO format string for date in CLDR standard format + * This is equivalent to php date format "Y-m-d" e.g. 2024-08-31 */ public const ISO_DATE = 'y-MM-dd'; @@ -42,13 +44,16 @@ class DBDate extends DBField */ public const ISO_LOCALE = 'en_US'; + private static array $field_validators = [ + DateFieldValidator::class, + ]; + + protected bool $skipValidationIfNull = true; + public function setValue(mixed $value, null|array|ModelData $record = null, bool $markChanged = true): static { - $value = $this->parseDate($value); - if ($value === false) { - throw new InvalidArgumentException( - "Invalid date: '$value'. Use " . DBDate::ISO_DATE . " to prevent this error." - ); + if ($value !== null) { + $value = $this->parseDate($value); } $this->value = $value; return $this; @@ -58,15 +63,10 @@ class DBDate extends DBField * Parse timestamp or iso8601-ish date into standard iso8601 format * * @param mixed $value - * @return string|null|false Formatted date, null if empty but valid, or false if invalid + * @return mixed Formatted date, or the original value if it couldn't be parsed */ protected function parseDate(mixed $value): string|null|false { - // Skip empty values - if (empty($value) && !is_numeric($value)) { - return null; - } - // Determine value to parse if (is_array($value)) { $source = $value; // parse array @@ -74,19 +74,18 @@ class DBDate extends DBField $source = $value; // parse timestamp } else { // Convert US date -> iso, fix y2k, etc - $value = $this->fixInputDate($value); - if (is_null($value)) { - return null; - } - $source = strtotime($value ?? ''); // convert string to timestamp + $fixedValue = $this->fixInputDate($value); + // convert string to timestamp + $source = strtotime($fixedValue ?? ''); } - if ($value === false) { - return false; + if (!$source) { + // Unable to parse date, keep as is so that the validator can catch it later + return $value; } - // Format as iso8601 $formatter = $this->getInternalFormatter(); - return $formatter->format($source); + $ret = $formatter->format($source); + return $ret; } /** @@ -560,20 +559,12 @@ class DBDate extends DBField */ protected function fixInputDate($value) { - // split [$year, $month, $day, $time] = $this->explodeDateString($value); - - if ((int)$year === 0 && (int)$month === 0 && (int)$day === 0) { - return null; + if (!checkdate((int) $month, (int) $day, (int) $year)) { + // Keep invalid dates as they are so that the validator can catch them later + return $value; } - // Validate date - if (!checkdate($month ?? 0, $day ?? 0, $year ?? 0)) { - throw new InvalidArgumentException( - "Invalid date: '$value'. Use " . DBDate::ISO_DATE . " to prevent this error." - ); - } - - // Convert to y-m-d + // Convert to Y-m-d return sprintf('%d-%02d-%02d%s', $year, $month, $day, $time); } @@ -591,11 +582,8 @@ class DBDate extends DBField $value ?? '', $matches )) { - throw new InvalidArgumentException( - "Invalid date: '$value'. Use " . DBDate::ISO_DATE . " to prevent this error." - ); + return [0, 0, 0, '']; } - $parts = [ $matches['first'], $matches['second'], @@ -605,11 +593,6 @@ class DBDate extends DBField if ($parts[0] < 1000 && $parts[2] > 1000) { $parts = array_reverse($parts ?? []); } - if ($parts[0] < 1000 && (int)$parts[0] !== 0) { - throw new InvalidArgumentException( - "Invalid date: '$value'. Use " . DBDate::ISO_DATE . " to prevent this error." - ); - } $parts[] = $matches['time']; return $parts; } diff --git a/src/ORM/FieldType/DBDatetime.php b/src/ORM/FieldType/DBDatetime.php index 287b64690..4ae0f373b 100644 --- a/src/ORM/FieldType/DBDatetime.php +++ b/src/ORM/FieldType/DBDatetime.php @@ -13,6 +13,8 @@ use SilverStripe\Security\Member; use SilverStripe\Security\Security; use SilverStripe\View\TemplateGlobalProvider; use SilverStripe\Model\ModelData; +use SilverStripe\Core\Validation\FieldValidation\DatetimeFieldValidator; +use SilverStripe\Core\Validation\FieldValidation\DateFieldValidator; /** * Represents a date-time field. @@ -39,6 +41,7 @@ class DBDatetime extends DBDate implements TemplateGlobalProvider /** * Standard ISO format string for date and time in CLDR standard format, * with a whitespace separating date and time (common database representation, e.g. in MySQL). + * This is equivalent to php date format "Y-m-d H:i:s" e.g. 2024-08-31 09:30:00 */ public const ISO_DATETIME = 'y-MM-dd HH:mm:ss'; @@ -48,10 +51,16 @@ class DBDatetime extends DBDate implements TemplateGlobalProvider */ public const ISO_DATETIME_NORMALISED = 'y-MM-dd\'T\'HH:mm:ss'; + private static array $field_validators = [ + DatetimeFieldValidator::class, + // disable parent validator + DateFieldValidator::class => null, + ]; + /** * Flag idicating if this field is considered immutable * when this is enabled setting the value of this field will return a new field instance - * instead updatin the old one + * instead updating the old one */ protected bool $immutable = false; diff --git a/src/ORM/FieldType/DBDecimal.php b/src/ORM/FieldType/DBDecimal.php index deab1bcd0..54270ad4a 100644 --- a/src/ORM/FieldType/DBDecimal.php +++ b/src/ORM/FieldType/DBDecimal.php @@ -2,6 +2,7 @@ namespace SilverStripe\ORM\FieldType; +use SilverStripe\Core\Validation\FieldValidation\DecimalFieldValidator; use SilverStripe\Forms\FormField; use SilverStripe\Forms\NumericField; use SilverStripe\ORM\DB; @@ -12,6 +13,10 @@ use SilverStripe\Model\ModelData; */ class DBDecimal extends DBField { + private static array $field_validators = [ + DecimalFieldValidator::class => ['getWholeSize', 'getDecimalSize'], + ]; + /** * Whole number size */ @@ -50,6 +55,16 @@ class DBDecimal extends DBField return floor($this->value ?? 0.0); } + public function getWholeSize(): int + { + return $this->wholeSize; + } + + public function getDecimalSize(): int + { + return $this->decimalSize; + } + public function requireField(): void { $parts = [ diff --git a/src/ORM/FieldType/DBEmail.php b/src/ORM/FieldType/DBEmail.php new file mode 100644 index 000000000..12eb2f5b6 --- /dev/null +++ b/src/ORM/FieldType/DBEmail.php @@ -0,0 +1,29 @@ +name, $title); + $field->setMaxLength($this->getSize()); + + // Allow the user to select if it's null instead of automatically assuming empty string is + if (!$this->getNullifyEmpty()) { + return NullableField::create($field); + } + return $field; + } +} diff --git a/src/ORM/FieldType/DBEnum.php b/src/ORM/FieldType/DBEnum.php index e71e6d17d..dcfde02dc 100644 --- a/src/ORM/FieldType/DBEnum.php +++ b/src/ORM/FieldType/DBEnum.php @@ -3,6 +3,7 @@ namespace SilverStripe\ORM\FieldType; use SilverStripe\Core\Config\Config; +use SilverStripe\Core\Validation\FieldValidation\EnumFieldValidator; use SilverStripe\Forms\DropdownField; use SilverStripe\Forms\FormField; use SilverStripe\Forms\SelectField; @@ -17,6 +18,10 @@ use SilverStripe\ORM\DB; */ class DBEnum extends DBString { + private static array $field_validators = [ + EnumFieldValidator::class => ['getEnum'], + ]; + /** * List of enum values */ diff --git a/src/ORM/FieldType/DBField.php b/src/ORM/FieldType/DBField.php index 38efb5758..98bb7a8b9 100644 --- a/src/ORM/FieldType/DBField.php +++ b/src/ORM/FieldType/DBField.php @@ -10,6 +10,8 @@ use SilverStripe\Forms\TextField; use SilverStripe\ORM\Filters\SearchFilter; use SilverStripe\ORM\Queries\SQLSelect; use SilverStripe\Model\ModelData; +use SilverStripe\Core\Validation\FieldValidation\FieldValidatorsTrait; +use SilverStripe\Core\Validation\FieldValidation\FieldValidationInterface; /** * Single field in the database. @@ -41,8 +43,9 @@ use SilverStripe\Model\ModelData; * } * */ -abstract class DBField extends ModelData implements DBIndexable +abstract class DBField extends ModelData implements DBIndexable, FieldValidationInterface { + use FieldValidatorsTrait; /** * Raw value of this field @@ -99,6 +102,8 @@ abstract class DBField extends ModelData implements DBIndexable 'ProcessedRAW' => 'HTMLFragment', ]; + private static array $field_validators = []; + /** * Default value in the database. * Might be overridden on DataObject-level, but still useful for setting defaults on @@ -161,7 +166,7 @@ abstract class DBField extends ModelData implements DBIndexable * * If you try an alter the name a warning will be thrown. */ - public function setName(?string $name): static + public function setName(string $name): static { if ($this->name && $this->name !== $name) { user_error("DBField::setName() shouldn't be called once a DBField already has a name." diff --git a/src/ORM/FieldType/DBInt.php b/src/ORM/FieldType/DBInt.php index 0f46ddf54..7524919a7 100644 --- a/src/ORM/FieldType/DBInt.php +++ b/src/ORM/FieldType/DBInt.php @@ -2,32 +2,59 @@ namespace SilverStripe\ORM\FieldType; +use SilverStripe\Core\Validation\FieldValidation\IntFieldValidator; use SilverStripe\Forms\FormField; use SilverStripe\Forms\NumericField; use SilverStripe\Model\List\ArrayList; use SilverStripe\ORM\DB; use SilverStripe\Model\List\SS_List; use SilverStripe\Model\ArrayData; +use SilverStripe\Model\ModelData; /** - * Represents a signed 32 bit integer field. + * Represents a signed 32 bit integer field + * + * Ints are always signed i.e. they can be negative + * Their range is -2147483648 to 2147483647 */ class DBInt extends DBField { + private static array $field_validators = [ + IntFieldValidator::class + ]; + + /** + * Raw value of this field + * + * Set to 0 by default, which is a change of the default value of null for DBField + */ + protected mixed $value = 0; + public function __construct(?string $name = null, int $defaultVal = 0) { - $this->defaultVal = is_int($defaultVal) ? $defaultVal : 0; - + $this->defaultVal = $defaultVal; parent::__construct($name); } - /** - * Ensure int values are always returned. - * This is for mis-configured databases that return strings. - */ - public function getValue(): ?int + public function getField($fieldName): mixed { - return (int) $this->value; + return $this->value; + } + + public function setValue(mixed $value, null|array|ModelData $record = null, bool $markChanged = true): static + { + if (is_null($value)) { + // Convert null to 0 so that it will pass validation + // Will be converted to 0 in prepValueForDB(), which is called after validation + // Methods such as DataObject::dbObject() can set this to null e.g. when a value has + // not been explicity set on a new record. + $value = 0; + } elseif (is_string($value) && preg_match('/^-?\d+$/', $value)) { + // Cast int like strings as ints + $value = (int) $value; + } + $this->value = $value; + return $this; } /** diff --git a/src/ORM/FieldType/DBIp.php b/src/ORM/FieldType/DBIp.php new file mode 100644 index 000000000..ecd28a0dd --- /dev/null +++ b/src/ORM/FieldType/DBIp.php @@ -0,0 +1,13 @@ + null, + // enable multi enum field validator + MultiEnumFieldValidator::class => ['getEnum'], + ]; + public function __construct($name = null, $enum = null, $default = null) { // MultiEnum needs to take care of its own defaults @@ -34,6 +43,11 @@ class DBMultiEnum extends DBEnum } } + public function getValueForValidation(): array + { + return explode(',', $this->value); + } + public function requireField(): void { $charset = Config::inst()->get(MySQLDatabase::class, 'charset'); diff --git a/src/ORM/FieldType/DBPolymorphicForeignKey.php b/src/ORM/FieldType/DBPolymorphicForeignKey.php index 62b792606..8fad8ad43 100644 --- a/src/ORM/FieldType/DBPolymorphicForeignKey.php +++ b/src/ORM/FieldType/DBPolymorphicForeignKey.php @@ -5,12 +5,18 @@ namespace SilverStripe\ORM\FieldType; use SilverStripe\Forms\FormField; use SilverStripe\ORM\DataObject; use SilverStripe\Model\ModelData; +use SilverStripe\Core\Validation\FieldValidation\CompositeFieldValidator; /** * A special ForeignKey class that handles relations with arbitrary class types */ class DBPolymorphicForeignKey extends DBComposite { + private static array $field_validators = [ + // Disable parent field validator + CompositeFieldValidator::class => null, + ]; + private static bool $index = true; private static array $composite_db = [ diff --git a/src/ORM/FieldType/DBString.php b/src/ORM/FieldType/DBString.php index 99d597aa9..7c91e2bd1 100644 --- a/src/ORM/FieldType/DBString.php +++ b/src/ORM/FieldType/DBString.php @@ -16,6 +16,8 @@ abstract class DBString extends DBField 'Plain' => 'Text', ]; + protected bool $skipValidationIfNull = true; + /** * Set the default value for "nullify empty" * diff --git a/src/ORM/FieldType/DBTime.php b/src/ORM/FieldType/DBTime.php index fac285be1..7915336b1 100644 --- a/src/ORM/FieldType/DBTime.php +++ b/src/ORM/FieldType/DBTime.php @@ -11,6 +11,7 @@ use SilverStripe\ORM\DB; use SilverStripe\Security\Member; use SilverStripe\Security\Security; use SilverStripe\Model\ModelData; +use SilverStripe\Core\Validation\FieldValidation\TimeFieldValidator; /** * Represents a column in the database with the type 'Time'. @@ -26,17 +27,17 @@ class DBTime extends DBField { /** * Standard ISO format string for time in CLDR standard format + * This is equivalent to php date format "H:i:s" e.g. 09:30:00 */ public const ISO_TIME = 'HH:mm:ss'; + private static array $field_validators = [ + TimeFieldValidator::class, + ]; + public function setValue(mixed $value, null|array|ModelData $record = null, bool $markChanged = true): static { $value = $this->parseTime($value); - if ($value === false) { - throw new InvalidArgumentException( - 'Invalid date passed. Use ' . $this->getISOFormat() . ' to prevent this error.' - ); - } $this->value = $value; return $this; } diff --git a/src/ORM/FieldType/DBUrl.php b/src/ORM/FieldType/DBUrl.php new file mode 100644 index 000000000..ab9435c65 --- /dev/null +++ b/src/ORM/FieldType/DBUrl.php @@ -0,0 +1,22 @@ +name, $title); + $field->setMaxLength($this->getSize()); + return $field; + } +} diff --git a/src/ORM/FieldType/DBVarchar.php b/src/ORM/FieldType/DBVarchar.php index 3081ad34b..b0cbbc017 100644 --- a/src/ORM/FieldType/DBVarchar.php +++ b/src/ORM/FieldType/DBVarchar.php @@ -8,6 +8,7 @@ use SilverStripe\Forms\NullableField; use SilverStripe\Forms\TextField; use SilverStripe\ORM\Connect\MySQLDatabase; use SilverStripe\ORM\DB; +use SilverStripe\Core\Validation\FieldValidation\StringFieldValidator; /** * Class Varchar represents a variable-length string of up to 255 characters, designed to store raw text @@ -18,6 +19,10 @@ use SilverStripe\ORM\DB; */ class DBVarchar extends DBString { + private static array $field_validators = [ + StringFieldValidator::class => [null, 'getSize'], + ]; + private static array $casting = [ 'Initial' => 'Text', 'URL' => 'Text', diff --git a/tests/php/Core/Validation/FieldValidation/BigIntFieldValidatorTest.php b/tests/php/Core/Validation/FieldValidation/BigIntFieldValidatorTest.php new file mode 100644 index 000000000..f760113e2 --- /dev/null +++ b/tests/php/Core/Validation/FieldValidation/BigIntFieldValidatorTest.php @@ -0,0 +1,75 @@ + [ + 'value' => 123, + 'expected' => true, + ], + 'valid-zero' => [ + 'value' => 0, + 'expected' => true, + ], + 'valid-negative-int' => [ + 'value' => -123, + 'expected' => true, + ], + 'valid-max-int' => [ + 'value' => 9223372036854775807, + 'expected' => true, + ], + 'valid-min-int' => [ + 'value' => '-9223372036854775808', + 'expected' => true, + ], + // Note: cannot test out of range values as they casting them to int + // will change the value to PHP_INT_MIN/PHP_INT_MAX + 'invalid-string-int' => [ + 'value' => '123', + 'expected' => false, + ], + 'invalid-float' => [ + 'value' => 123.45, + 'expected' => false, + ], + 'invalid-array' => [ + 'value' => [123], + 'expected' => false, + ], + 'invalid-null' => [ + 'value' => null, + 'expected' => false, + ], + 'invalid-true' => [ + 'value' => true, + 'expected' => false, + ], + 'invalid-false' => [ + 'value' => false, + 'expected' => false, + ], + ]; + } + + #[DataProvider('provideValidate')] + public function testValidate(mixed $value, bool $expected): void + { + // On 64-bit systems, -9223372036854775808 will end up as a float + // however it works correctly when cast to an int + if ($value === '-9223372036854775808') { + $value = (int) $value; + } + $validator = new BigIntFieldValidator('MyField', $value, false); + $result = $validator->validate(); + $this->assertSame($expected, $result->isValid()); + } +} diff --git a/tests/php/Core/Validation/FieldValidation/BooleanIntFieldValidatorTest.php b/tests/php/Core/Validation/FieldValidation/BooleanIntFieldValidatorTest.php new file mode 100644 index 000000000..008cc77de --- /dev/null +++ b/tests/php/Core/Validation/FieldValidation/BooleanIntFieldValidatorTest.php @@ -0,0 +1,72 @@ + [ + 'value' => 1, + 'expected' => true, + ], + 'valid-int-0' => [ + 'value' => 0, + 'expected' => true, + ], + 'invvalid-true' => [ + 'value' => true, + 'expected' => false, + ], + 'invalid-false' => [ + 'value' => false, + 'expected' => false, + ], + 'invalid-string-1' => [ + 'value' => '1', + 'expected' => false, + ], + 'invalid-string-0' => [ + 'value' => '0', + 'expected' => false, + ], + 'invalid-string-true' => [ + 'value' => 'true', + 'expected' => false, + ], + 'invalid-string-false' => [ + 'value' => 'false', + 'expected' => false, + ], + 'invalid-null' => [ + 'value' => null, + 'expected' => false, + ], + 'invalid-string' => [ + 'value' => 'abc', + 'expected' => false, + ], + 'invalid-int' => [ + 'value' => 123, + 'expected' => false, + ], + 'invalid-array' => [ + 'value' => [], + 'expected' => false, + ], + ]; + } + + #[DataProvider('provideValidate')] + public function testValidate(mixed $value, bool $expected): void + { + $validator = new BooleanIntFieldValidator('MyField', $value, false); + $result = $validator->validate(); + $this->assertSame($expected, $result->isValid()); + } +} diff --git a/tests/php/Core/Validation/FieldValidation/CompositeFieldValidatorTest.php b/tests/php/Core/Validation/FieldValidation/CompositeFieldValidatorTest.php new file mode 100644 index 000000000..be372ed2c --- /dev/null +++ b/tests/php/Core/Validation/FieldValidation/CompositeFieldValidatorTest.php @@ -0,0 +1,76 @@ + [ + 'valueBoolean' => true, + 'valueString' => 'fish', + 'exception' => null, + 'expected' => true, + ], + 'exception-not-iterable' => [ + 'valueBoolean' => true, + 'valueString' => 'not-iterable', + 'exception' => InvalidArgumentException::class, + 'expected' => true, + ], + 'exception-not-field-validator' => [ + 'valueBoolean' => true, + 'valueString' => 'no-field-validation', + 'exception' => InvalidArgumentException::class, + 'expected' => true, + ], + 'invalid-bool-field' => [ + 'valueBoolean' => 'dog', + 'valueString' => 'fish', + 'exception' => null, + 'expected' => false, + ], + 'invalid-string-field' => [ + 'valueBoolean' => true, + 'valueString' => 456.789, + 'exception' => null, + 'expected' => false, + ], + ]; + } + + #[DataProvider('provideValidate')] + public function testValidate(mixed $valueBoolean, mixed $valueString, ?string $exception, bool $expected): void + { + if ($exception) { + $this->expectException($exception); + } + $booleanField = new DBBoolean('IntField'); + $booleanField->setValue($valueBoolean); + if ($exception && $valueString === 'no-field-validation') { + $stringField = new stdClass(); + } else { + $stringField = new DBVarchar('StringField'); + $stringField->setValue($valueString); + } + if ($exception && $valueString === 'not-iterable') { + $iterable = 'banana'; + } else { + $iterable = [$booleanField, $stringField]; + } + $validator = new CompositeFieldValidator('MyField', $iterable, false); + $result = $validator->validate(); + if (!$exception) { + $this->assertSame($expected, $result->isValid()); + } + } +} diff --git a/tests/php/Core/Validation/FieldValidation/DateFieldValidatorTest.php b/tests/php/Core/Validation/FieldValidation/DateFieldValidatorTest.php new file mode 100644 index 000000000..4160da1a5 --- /dev/null +++ b/tests/php/Core/Validation/FieldValidation/DateFieldValidatorTest.php @@ -0,0 +1,48 @@ + [ + 'value' => '2020-09-15', + 'expected' => true, + ], + 'invalid' => [ + 'value' => '2020-02-30', + 'expected' => false, + ], + 'invalid-wrong-format' => [ + 'value' => '15-09-2020', + 'expected' => false, + ], + 'invalid-date-time' => [ + 'value' => '2020-09-15 13:34:56', + 'expected' => false, + ], + 'invalid-time' => [ + 'value' => '13:34:56', + 'expected' => false, + ], + 'invalid-null' => [ + 'value' => null, + 'expected' => false, + ], + ]; + } + + #[DataProvider('provideValidate')] + public function testValidate(mixed $value, bool $expected): void + { + $validator = new DateFieldValidator('MyField', $value, false); + $result = $validator->validate(); + $this->assertSame($expected, $result->isValid()); + } +} diff --git a/tests/php/Core/Validation/FieldValidation/DatetimeFieldValidatorTest.php b/tests/php/Core/Validation/FieldValidation/DatetimeFieldValidatorTest.php new file mode 100644 index 000000000..28ebe657f --- /dev/null +++ b/tests/php/Core/Validation/FieldValidation/DatetimeFieldValidatorTest.php @@ -0,0 +1,52 @@ + [ + 'value' => '2020-09-15 13:34:56', + 'expected' => true, + ], + 'invalid-date' => [ + 'value' => '2020-02-30 13:34:56', + 'expected' => false, + ], + 'invalid-time' => [ + 'value' => '2020-02-15 13:99:56', + 'expected' => false, + ], + 'invalid-wrong-format' => [ + 'value' => '15-09-2020 13:34:56', + 'expected' => false, + ], + 'invalid-date-only' => [ + 'value' => '2020-09-15', + 'expected' => false, + ], + 'invalid-time-only' => [ + 'value' => '13:34:56', + 'expected' => false, + ], + 'invalid-null' => [ + 'value' => null, + 'expected' => false, + ], + ]; + } + + #[DataProvider('provideValidate')] + public function testValidate(mixed $value, bool $expected): void + { + $validator = new DatetimeFieldValidator('MyField', $value, false); + $result = $validator->validate(); + $this->assertSame($expected, $result->isValid()); + } +} diff --git a/tests/php/Core/Validation/FieldValidation/DecimalFieldValidatorTest.php b/tests/php/Core/Validation/FieldValidation/DecimalFieldValidatorTest.php new file mode 100644 index 000000000..e1efff8ef --- /dev/null +++ b/tests/php/Core/Validation/FieldValidation/DecimalFieldValidatorTest.php @@ -0,0 +1,138 @@ + [ + 'value' => 123.45, + 'wholeSize' => 5, + 'decimalSize' => 2, + 'expected' => true, + ], + 'valid-negative' => [ + 'value' => -123.45, + 'wholeSize' => 5, + 'decimalSize' => 2, + 'expected' => true, + ], + 'valid-zero' => [ + 'value' => 0, + 'wholeSize' => 5, + 'decimalSize' => 2, + 'expected' => true, + ], + 'valid-rounded-dp' => [ + 'value' => 123.456, + 'wholeSize' => 5, + 'decimalSize' => 2, + 'expected' => true, + ], + 'valid-rounded-up' => [ + 'value' => 123.999, + 'wholeSize' => 5, + 'decimalSize' => 2, + 'expected' => true, + ], + 'valid-int' => [ + 'value' => 123, + 'wholeSize' => 5, + 'decimalSize' => 2, + 'expected' => true, + ], + 'valid-negative-int' => [ + 'value' => -123, + 'wholeSize' => 5, + 'decimalSize' => 2, + 'expected' => true, + ], + 'valid-max' => [ + 'value' => 999.99, + 'wholeSize' => 5, + 'decimalSize' => 2, + 'expected' => true, + ], + 'valid-max-negative' => [ + 'value' => -999.99, + 'wholeSize' => 5, + 'decimalSize' => 2, + 'expected' => true, + ], + 'invalid-rounded-to-6-digts' => [ + 'value' => 999.999, + 'wholeSize' => 5, + 'decimalSize' => 2, + 'expected' => false, + ], + 'invalid-too-long' => [ + 'value' => 1234.56, + 'wholeSize' => 5, + 'decimalSize' => 2, + 'expected' => false, + ], + 'invalid-too-long-3dp' => [ + 'value' => 123.456, + 'wholeSize' => 5, + 'decimalSize' => 3, + 'expected' => false, + ], + 'invalid-too-long-1dp' => [ + 'value' => 123.4, + 'wholeSize' => 5, + 'decimalSize' => 3, + 'expected' => false, + ], + 'invalid-too-long-int' => [ + 'value' => 123, + 'wholeSize' => 5, + 'decimalSize' => 3, + 'expected' => false, + ], + 'invalid-string' => [ + 'value' => '123.45', + 'wholeSize' => 5, + 'decimalSize' => 2, + 'expected' => false, + ], + 'invalid-null' => [ + 'value' => null, + 'wholeSize' => 5, + 'decimalSize' => 2, + 'expected' => false, + ], + 'invalid-true' => [ + 'value' => true, + 'wholeSize' => 5, + 'decimalSize' => 2, + 'expected' => false, + ], + 'invalid-false' => [ + 'value' => false, + 'wholeSize' => 5, + 'decimalSize' => 2, + 'expected' => false, + ], + 'invalid-array' => [ + 'value' => [123.45], + 'wholeSize' => 5, + 'decimalSize' => 2, + 'expected' => false, + ], + ]; + } + + #[DataProvider('provideValidate')] + public function testValidate(mixed $value, int $wholeSize, int $decimalSize, bool $expected): void + { + $validator = new DecimalFieldValidator('MyField', $value, false, $wholeSize, $decimalSize); + $result = $validator->validate(); + $this->assertSame($expected, $result->isValid()); + } +} diff --git a/tests/php/Core/Validation/FieldValidation/EmailFieldValidatorTest.php b/tests/php/Core/Validation/FieldValidation/EmailFieldValidatorTest.php new file mode 100644 index 000000000..22b6e21f6 --- /dev/null +++ b/tests/php/Core/Validation/FieldValidation/EmailFieldValidatorTest.php @@ -0,0 +1,37 @@ + [ + 'value' => 'test@example.com', + 'expected' => true, + ], + 'invalid' => [ + 'value' => 'fish', + 'expected' => false, + ], + 'invalid-null' => [ + 'value' => null, + 'expected' => false, + ], + ]; + } + + #[DataProvider('provideValidate')] + public function testValidate(mixed $value, bool $expected): void + { + $validator = new EmailFieldValidator('MyField', $value, false); + $result = $validator->validate(); + $this->assertSame($expected, $result->isValid()); + } +} diff --git a/tests/php/Core/Validation/FieldValidation/EnumFieldValidatorTest.php b/tests/php/Core/Validation/FieldValidation/EnumFieldValidatorTest.php new file mode 100644 index 000000000..b41f49a03 --- /dev/null +++ b/tests/php/Core/Validation/FieldValidation/EnumFieldValidatorTest.php @@ -0,0 +1,54 @@ + [ + 'value' => 'cat', + 'allowedValues' => ['cat', 'dog'], + 'expected' => true, + ], + 'valid-int' => [ + 'value' => 123, + 'allowedValues' => [123, 456], + 'expected' => true, + ], + 'invalid' => [ + 'value' => 'fish', + 'allowedValues' => ['cat', 'dog'], + 'expected' => false, + ], + 'invalid-none' => [ + 'value' => '', + 'allowedValues' => ['cat', 'dog'], + 'expected' => false, + ], + 'invalid-null' => [ + 'value' => null, + 'allowedValues' => ['cat', 'dog'], + 'expected' => false, + ], + 'invalid-strict' => [ + 'value' => '123', + 'allowedValues' => [123, 456], + 'expected' => false, + ], + ]; + } + + #[DataProvider('provideValidate')] + public function testValidate(mixed $value, array $allowedValues, bool $expected): void + { + $validator = new EnumFieldValidator('MyField', $value, false, $allowedValues); + $result = $validator->validate(); + $this->assertSame($expected, $result->isValid()); + } +} diff --git a/tests/php/Core/Validation/FieldValidation/FieldValidatorTest.php b/tests/php/Core/Validation/FieldValidation/FieldValidatorTest.php new file mode 100644 index 000000000..4992a3c81 --- /dev/null +++ b/tests/php/Core/Validation/FieldValidation/FieldValidatorTest.php @@ -0,0 +1,43 @@ + [ + 'skipIfNull' => true, + 'expected' => true, + ], + 'not-skip' => [ + 'skipIfNull' => false, + 'expected' => false, + ], + ]; + } + + #[DataProvider('provideSkipIfNull')] + public function testSkipIfNull(bool $skipIfNull, bool $expected): void + { + $value = null; + $validator = new class ('MyField', $value, $skipIfNull) extends FieldValidator { + protected function validateValue(): ValidationResult + { + $result = ValidationResult::create(); + if ($this->value === null) { + $result->addFieldError('MyField', 'Disaster'); + } + return $result; + } + }; + $result = $validator->validate(); + $this->assertSame($expected, $result->isValid()); + } +} diff --git a/tests/php/Core/Validation/FieldValidation/IntFieldValidatorTest.php b/tests/php/Core/Validation/FieldValidation/IntFieldValidatorTest.php new file mode 100644 index 000000000..f64727abc --- /dev/null +++ b/tests/php/Core/Validation/FieldValidation/IntFieldValidatorTest.php @@ -0,0 +1,76 @@ + [ + 'value' => 123, + 'expected' => true, + ], + 'valid-zero' => [ + 'value' => 0, + 'expected' => true, + ], + 'valid-negative-int' => [ + 'value' => -123, + 'expected' => true, + ], + 'valid-max-int' => [ + 'value' => 2147483647, + 'expected' => true, + ], + 'valid-min-int' => [ + 'value' => -2147483648, + 'expected' => true, + ], + 'invalid-out-of-bounds' => [ + 'value' => 2147483648, + 'expected' => false, + ], + 'invalid-out-of-negative-bounds' => [ + 'value' => -2147483649, + 'expected' => false, + ], + 'invalid-string-int' => [ + 'value' => '123', + 'expected' => false, + ], + 'invalid-float' => [ + 'value' => 123.45, + 'expected' => false, + ], + 'invalid-array' => [ + 'value' => [123], + 'expected' => false, + ], + 'invalid-null' => [ + 'value' => null, + 'expected' => false, + ], + 'invalid-true' => [ + 'value' => true, + 'expected' => false, + ], + 'invalid-false' => [ + 'value' => false, + 'expected' => false, + ], + ]; + } + + #[DataProvider('provideValidate')] + public function testValidate(mixed $value, bool $expected): void + { + $validator = new IntFieldValidator('MyField', $value, false); + $result = $validator->validate(); + $this->assertSame($expected, $result->isValid()); + } +} diff --git a/tests/php/Core/Validation/FieldValidation/IpFieldValidatorTest.php b/tests/php/Core/Validation/FieldValidation/IpFieldValidatorTest.php new file mode 100644 index 000000000..a4aed7657 --- /dev/null +++ b/tests/php/Core/Validation/FieldValidation/IpFieldValidatorTest.php @@ -0,0 +1,45 @@ + [ + 'value' => '127.0.0.1', + 'expected' => true, + ], + 'valid-ipv6' => [ + 'value' => '0:0:0:0:0:0:0:1', + 'expected' => true, + ], + 'valid-ipv6-short' => [ + 'value' => '::1', + 'expected' => true, + ], + 'invalid' => [ + 'value' => '12345', + 'expected' => false, + ], + 'invalid-null' => [ + 'value' => null, + 'expected' => false, + ], + ]; + } + + #[DataProvider('provideValidate')] + public function testValidate(mixed $value, bool $expected): void + { + $validator = new IpFieldValidator('MyField', $value, false); + $result = $validator->validate(); + $this->assertSame($expected, $result->isValid()); + } +} diff --git a/tests/php/Core/Validation/FieldValidation/LocaleFieldValidatorTest.php b/tests/php/Core/Validation/FieldValidation/LocaleFieldValidatorTest.php new file mode 100644 index 000000000..6f9824f0d --- /dev/null +++ b/tests/php/Core/Validation/FieldValidation/LocaleFieldValidatorTest.php @@ -0,0 +1,61 @@ + [ + 'value' => 'de_DE', + 'expected' => true, + ], + 'valid-dash' => [ + 'value' => 'de-DE', + 'expected' => true, + ], + 'valid-short' => [ + 'value' => 'de', + 'expected' => true, + ], + 'invalid' => [ + 'value' => 'zz_ZZ', + 'expected' => false, + ], + 'invalid-dash' => [ + 'value' => 'zz-ZZ', + 'expected' => false, + ], + 'invalid-short' => [ + 'value' => 'zz', + 'expected' => false, + ], + 'invalid-dashes' => [ + 'value' => '-----', + 'expected' => false, + ], + 'invalid-donut' => [ + 'value' => 'donut', + 'expected' => false, + ], + 'invalid-null' => [ + 'value' => null, + 'expected' => false, + ], + ]; + } + + #[DataProvider('provideValidate')] + public function testValidate(mixed $value, bool $expected): void + { + $validator = new LocaleFieldValidator('MyField', $value, false); + $result = $validator->validate(); + $this->assertSame($expected, $result->isValid()); + } +} diff --git a/tests/php/Core/Validation/FieldValidation/MultiEnumFieldValidatorTest.php b/tests/php/Core/Validation/FieldValidation/MultiEnumFieldValidatorTest.php new file mode 100644 index 000000000..971cdfe7f --- /dev/null +++ b/tests/php/Core/Validation/FieldValidation/MultiEnumFieldValidatorTest.php @@ -0,0 +1,84 @@ + [ + 'value' => ['cat'], + 'allowedValues' => ['cat', 'dog'], + 'exception' => false, + 'expected' => true, + ], + 'valid-multi-string' => [ + 'value' => ['cat', 'dog'], + 'allowedValues' => ['cat', 'dog'], + 'exception' => false, + 'expected' => true, + ], + 'valid-none' => [ + 'value' => [], + 'allowedValues' => ['cat', 'dog'], + 'exception' => false, + 'expected' => true, + ], + 'valid-int' => [ + 'value' => [123], + 'allowedValues' => [123, 456], + 'exception' => false, + 'expected' => true, + ], + 'exception-not-array' => [ + 'value' => 'cat,dog', + 'allowedValues' => ['cat', 'dog'], + 'exception' => true, + 'expected' => false, + ], + 'invalid' => [ + 'value' => ['fish'], + 'allowedValues' => ['cat', 'dog'], + 'exception' => false, + 'expected' => false, + ], + 'invalid-null' => [ + 'value' => [null], + 'allowedValues' => ['cat', 'dog'], + 'exception' => false, + 'expected' => false, + ], + 'invalid-multi' => [ + 'value' => ['dog', 'fish'], + 'allowedValues' => ['cat', 'dog'], + 'exception' => false, + 'expected' => false, + ], + 'invalid-strict' => [ + 'value' => ['123'], + 'allowedValues' => [123, 456], + 'exception' => false, + 'expected' => false, + ], + ]; + } + + #[DataProvider('provideValidate')] + public function testValidate(mixed $value, array $allowedValues, bool $exception, bool $expected): void + { + if ($exception) { + $this->expectException(InvalidArgumentException::class); + } + $validator = new MultiEnumFieldValidator('MyField', $value, false, $allowedValues); + $result = $validator->validate(); + if (!$exception) { + $this->assertSame($expected, $result->isValid()); + } + } +} diff --git a/tests/php/Core/Validation/FieldValidation/NumericFieldValidatorTest.php b/tests/php/Core/Validation/FieldValidation/NumericFieldValidatorTest.php new file mode 100644 index 000000000..e2662c2a1 --- /dev/null +++ b/tests/php/Core/Validation/FieldValidation/NumericFieldValidatorTest.php @@ -0,0 +1,80 @@ + [ + 'value' => 123, + 'expected' => true, + ], + 'valid-zero' => [ + 'value' => 0, + 'expected' => true, + ], + 'valid-negative-int' => [ + 'value' => -123, + 'expected' => true, + ], + 'valid-float' => [ + 'value' => 123.45, + 'expected' => true, + ], + 'valid-negative-float' => [ + 'value' => -123.45, + 'expected' => true, + ], + 'valid-max-int' => [ + 'value' => PHP_INT_MAX, + 'expected' => true, + ], + 'valid-min-int' => [ + 'value' => PHP_INT_MIN, + 'expected' => true, + ], + 'valid-max-float' => [ + 'value' => PHP_FLOAT_MAX, + 'expected' => true, + ], + 'valid-min-float' => [ + 'value' => PHP_FLOAT_MIN, + 'expected' => true, + ], + 'invalid-string' => [ + 'value' => '123', + 'expected' => false, + ], + 'invalid-array' => [ + 'value' => [123], + 'expected' => false, + ], + 'invalid-null' => [ + 'value' => null, + 'expected' => false, + ], + 'invalid-true' => [ + 'value' => true, + 'expected' => false, + ], + 'invalid-false' => [ + 'value' => false, + 'expected' => false, + ], + ]; + } + + #[DataProvider('provideValidate')] + public function testValidate(mixed $value, bool $expected): void + { + $validator = new NumericFieldValidator('MyField', $value, false); + $result = $validator->validate(); + $this->assertSame($expected, $result->isValid()); + } +} diff --git a/tests/php/Core/Validation/FieldValidation/StringFieldValidatorTest.php b/tests/php/Core/Validation/FieldValidation/StringFieldValidatorTest.php new file mode 100644 index 000000000..b1f481b33 --- /dev/null +++ b/tests/php/Core/Validation/FieldValidation/StringFieldValidatorTest.php @@ -0,0 +1,149 @@ + [ + 'value' => 'fish', + 'minLength' => null, + 'maxLength' => null, + 'exception' => false, + 'expected' => true, + ], + 'valid-blank' => [ + 'value' => '', + 'minLength' => null, + 'maxLength' => null, + 'exception' => false, + 'expected' => true, + ], + 'valid-blank-when-min' => [ + 'value' => '', + 'minLength' => 5, + 'maxLength' => null, + 'exception' => false, + 'expected' => true, + ], + 'valid-max' => [ + 'value' => 'fish', + 'minLength' => 0, + 'maxLength' => 4, + 'exception' => false, + 'expected' => true, + ], + 'valid-less-than-max-null-min' => [ + 'value' => 'fish', + 'minLength' => null, + 'maxLength' => 4, + 'exception' => false, + 'expected' => true, + ], + 'valid-less-than-max-unicode' => [ + 'value' => '☕☕☕☕', + 'minLength' => 0, + 'maxLength' => 4, + 'exception' => false, + 'expected' => true, + ], + 'exception-negative-min' => [ + 'value' => 'fish', + 'minLength' => -1, + 'maxLength' => null, + 'exception' => true, + 'expected' => false, + ], + 'invalid-below-min' => [ + 'value' => 'fish', + 'minLength' => 5, + 'maxLength' => null, + 'exception' => false, + 'expected' => false, + ], + 'invalid-below-min-unicode' => [ + 'value' => '☕☕☕☕', + 'minLength' => 5, + 'maxLength' => null, + 'exception' => false, + 'expected' => false, + ], + 'invalid-above-min' => [ + 'value' => 'fish', + 'minLength' => 0, + 'maxLength' => 3, + 'exception' => false, + 'expected' => false, + ], + 'invalid-above-min-unicode' => [ + 'value' => '☕☕☕☕', + 'minLength' => 0, + 'maxLength' => 3, + 'exception' => false, + 'expected' => false, + ], + 'invalid-int' => [ + 'value' => 123, + 'minLength' => null, + 'maxLength' => null, + 'exception' => false, + 'expected' => false, + ], + 'invalid-float' => [ + 'value' => 123.56, + 'minLength' => null, + 'maxLength' => null, + 'exception' => false, + 'expected' => false, + ], + 'invalid-true' => [ + 'value' => true, + 'minLength' => null, + 'maxLength' => null, + 'exception' => false, + 'expected' => false, + ], + 'invalid-false' => [ + 'value' => false, + 'minLength' => null, + 'maxLength' => null, + 'exception' => false, + 'expected' => false, + ], + 'invalid-null' => [ + 'value' => null, + 'minLength' => null, + 'maxLength' => null, + 'exception' => false, + 'expected' => false, + ], + 'invalid-array' => [ + 'value' => ['fish'], + 'minLength' => null, + 'maxLength' => null, + 'exception' => false, + 'expected' => false, + ], + ]; + } + + #[DataProvider('provideValidate')] + public function testValidate(mixed $value, ?int $minLength, ?int $maxLength, bool $exception, bool $expected): void + { + if ($exception) { + $this->expectException(InvalidArgumentException::class); + } + $validator = new StringFieldValidator('MyField', $value, false, $minLength, $maxLength); + $result = $validator->validate(); + if (!$exception) { + $this->assertSame($expected, $result->isValid()); + } + } +} diff --git a/tests/php/Core/Validation/FieldValidation/TimeFieldValidatorTest.php b/tests/php/Core/Validation/FieldValidation/TimeFieldValidatorTest.php new file mode 100644 index 000000000..dc22bb8f4 --- /dev/null +++ b/tests/php/Core/Validation/FieldValidation/TimeFieldValidatorTest.php @@ -0,0 +1,48 @@ + [ + 'value' => '13:34:56', + 'expected' => true, + ], + 'invalid' => [ + 'value' => '13:99:56', + 'expected' => false, + ], + 'invalid-wrong-format' => [ + 'value' => '13-34-56', + 'expected' => false, + ], + 'invalid-date-time' => [ + 'value' => '2020-09-15 13:34:56', + 'expected' => false, + ], + 'invalid-date' => [ + 'value' => '2020-09-15', + 'expected' => false, + ], + 'invalid-null' => [ + 'value' => null, + 'expected' => false, + ], + ]; + } + + #[DataProvider('provideValidate')] + public function testValidate(mixed $value, bool $expected): void + { + $validator = new TimeFieldValidator('MyField', $value, false); + $result = $validator->validate(); + $this->assertSame($expected, $result->isValid()); + } +} diff --git a/tests/php/Core/Validation/FieldValidation/UrlFieldValidatorTest.php b/tests/php/Core/Validation/FieldValidation/UrlFieldValidatorTest.php new file mode 100644 index 000000000..86bd18dff --- /dev/null +++ b/tests/php/Core/Validation/FieldValidation/UrlFieldValidatorTest.php @@ -0,0 +1,45 @@ + [ + 'value' => 'https://www.example.com', + 'expected' => true, + ], + 'valid-http' => [ + 'value' => 'https://www.example.com', + 'expected' => true, + ], + 'invalid-ftp' => [ + 'value' => 'ftp://www.example.com', + 'expected' => false, + ], + 'invalid-no-scheme' => [ + 'value' => 'www.example.com', + 'expected' => false, + ], + 'invalid-null' => [ + 'value' => null, + 'expected' => false, + ], + ]; + } + + #[DataProvider('provideValidate')] + public function testValidate(mixed $value, bool $expected): void + { + $validator = new UrlFieldValidator('MyField', $value, false); + $result = $validator->validate(); + $this->assertSame($expected, $result->isValid()); + } +} diff --git a/tests/php/ORM/DBFieldValidatorsTest.php b/tests/php/ORM/DBFieldValidatorsTest.php new file mode 100644 index 000000000..27a068ee6 --- /dev/null +++ b/tests/php/ORM/DBFieldValidatorsTest.php @@ -0,0 +1,205 @@ + [ + 'class' => DBBigInt::class, + 'expected' => [ + BigIntValidator::class, + ], + ], + 'DBBoolean' => [ + 'class' => DBBoolean::class, + 'expected' => [ + ], + ], + 'DBClassName' => [ + 'class' => DBClassName::class, + 'expected' => [ + ], + ], + 'DBComposite' => [ + 'class' => DBComposite::class, + 'expected' => [ + ], + ], + 'DBCurrency' => [ + 'class' => DBCurrency::class, + 'expected' => [ + ], + ], + 'DBDate' => [ + 'class' => DBDate::class, + 'expected' => [ + ], + ], + 'DBDatetime' => [ + 'class' => DBDatetime::class, + 'expected' => [ + ], + ], + 'DBDecimal' => [ + 'class' => DBDecimal::class, + 'expected' => [ + ], + ], + 'DBEmail' => [ + 'class' => DBEmail::class, + 'expected' => [ + ], + ], + 'DBFloat' => [ + 'class' => DBFloat::class, + 'expected' => [ + ], + ], + 'DBForeignKey' => [ + 'class' => DBForeignKey::class, + 'expected' => [ + ], + ], + 'DBHTMLText' => [ + 'class' => DBHTMLText::class, + 'expected' => [ + ], + ], + 'DBHTMLVarchar' => [ + 'class' => DBHTMLVarchar::class, + 'expected' => [ + ], + ], + 'DBIndexable' => [ + 'class' => DBIndexable::class, + 'expected' => [ + ], + ], + 'DBInt' => [ + 'class' => DBInt::class, + 'expected' => [ + IntValidator::class, + ], + ], + 'DBIp' => [ + 'class' => DBIp::class, + 'expected' => [ + ], + ], + 'DBLocale' => [ + 'class' => DBLocale::class, + 'expected' => [ + ], + ], + 'DBMoney' => [ + 'class' => DBMoney::class, + 'expected' => [ + ], + ], + 'DBMultiEnum' => [ + 'class' => DBMultiEnum::class, + 'expected' => [ + ], + ], + 'DBPercentage' => [ + 'class' => DBPercentage::class, + 'expected' => [ + ], + ], + 'DBPolymorphicForeignKey' => [ + 'class' => DBPolymorphicForeignKey::class, + 'expected' => [ + ], + ], + 'DBPolymorhicRelationAwareForiegnKey' => [ + 'class' => DBPolymorphicRelationAwareForeignKey::class, + 'expected' => [ + ], + ], + 'DBPrimaryKey' => [ + 'class' => DBPrimaryKey::class, + 'expected' => [ + ], + ], + 'DBString' => [ + 'class' => DBString::class, + 'expected' => [ + ], + ], + 'DBText' => [ + 'class' => DBText::class, + 'expected' => [ + ], + ], + 'DBTime' => [ + 'class' => DBTime::class, + 'expected' => [ + ], + ], + 'DBUrl' => [ + 'class' => DBUrl::class, + 'expected' => [ + ], + ], + 'DBVarchar' => [ + 'class' => DBVarchar::class, + 'expected' => [ + ], + ], + 'DBYear' => [ + 'class' => DBYear::class, + 'expected' => [ + ], + ], + ]; + } + + #[DataProvider('provideFieldValidatorConfig')] + public function testFieldValidatorConfig(string $class, array $expected): void + { + $method = new ReflectionMethod($class, 'getFieldValidators'); + $method->setAccessible(true); + $obj = new $class('MyField'); + $fieldValidators = $method->invoke($obj); + $actual = array_map('get_class', $fieldValidators); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/php/ORM/DBIntTest.php b/tests/php/ORM/DBIntTest.php index 554d80232..02e51986e 100644 --- a/tests/php/ORM/DBIntTest.php +++ b/tests/php/ORM/DBIntTest.php @@ -4,15 +4,70 @@ namespace SilverStripe\ORM\Tests; use SilverStripe\Dev\SapphireTest; use SilverStripe\ORM\FieldType\DBInt; +use PHPUnit\Framework\Attributes\DataProvider; class DBIntTest extends SapphireTest { - public function testGetValueCastToInt() + public function testDefaultValue(): void + { + $field = new DBInt('MyField'); + $this->assertSame(0, $field->getValue()); + } + + public static function provideSetGetValue(): array + { + return [ + 'int' => [ + 'value' => 3, + 'expected' => 3, + ], + 'string-int' => [ + 'value' => '3', + 'expected' => 3, + ], + 'string' => [ + 'value' => 'fish', + 'expected' => 'fish', + ], + 'array' => [ + 'value' => [], + 'expected' => [], + ], + 'null' => [ + 'value' => null, + 'expected' => 0, + ], + ]; + } + + #[DataProvider('provideSetGetValue')] + public function testSetGetValue(mixed $value, mixed $expected): void { $field = DBInt::create('MyField'); - $field->setValue(3); - $this->assertSame(3, $field->getValue()); - $field->setValue('3'); - $this->assertSame(3, $field->getValue()); + $field->setValue($value); + $this->assertSame($expected, $field->getValue()); + } + + public static function provideValidate(): array + { + return [ + 'valid' => [ + 'value' => 123, + 'expected' => true, + ], + 'invalid' => [ + 'value' => 'abc', + 'expected' => false, + ], + ]; + } + + #[DataProvider('provideValidate')] + public function testValidate(mixed $value, bool $expected): void + { + $field = new DBInt('MyField'); + $field->setValue($value); + $result = $field->validate(); + $this->assertSame($expected, $result->isValid()); } }