Skip to content

Commit

Permalink
Merge pull request #11408 from creative-commoners/pulls/6/field-valid…
Browse files Browse the repository at this point in the history
…ators

NEW Validate DBFields
  • Loading branch information
GuySartorelli authored Nov 6, 2024
2 parents 6b33b5a + ec45390 commit 3f863b2
Show file tree
Hide file tree
Showing 92 changed files with 3,747 additions and 122 deletions.
6 changes: 6 additions & 0 deletions _config/model.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,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",
Expand Down
65 changes: 65 additions & 0 deletions src/Core/Validation/FieldValidation/BigIntFieldValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

namespace SilverStripe\Core\Validation\FieldValidation;

use RunTimeException;
use SilverStripe\Core\Validation\FieldValidation\IntFieldValidator;
use SilverStripe\Core\Validation\ValidationResult;

/**
* A field validator for 64-bit integers
* Will throw a RunTimeException if used on a 32-bit system
*/
class BigIntFieldValidator extends IntFieldValidator
{
/**
* The minimum value for a signed 64-bit integer.
* Defined as string instead of int otherwise will end up as a float
* on 64-bit systems
*
* When this is cast to an int in IntFieldValidator::__construct()
* it will be properly cast to an int
*/
protected const MIN_INT = '-9223372036854775808';

/**
* The maximum value for a signed 64-bit integer.
*/
protected const MAX_INT = '9223372036854775807';

public function __construct(
string $name,
mixed $value,
?int $minValue = null,
?int $maxValue = null
) {
if (is_null($minValue) || is_null($maxValue)) {
$bits = strlen(decbin(~0));
if ($bits === 32) {
throw new RunTimeException('Cannot use BigIntFieldValidator on a 32-bit system');
}
}
$this->minValue = $minValue;
$this->maxValue = $maxValue;
parent::__construct($name, $value, $minValue, $maxValue);
}

protected function validateValue(): ValidationResult
{
$result = ValidationResult::create();
// Validate string values that are too large or too small
// Only testing for string values here as that's all bccomp can take as arguments
// int values that are too large or too small will be cast to float
// on 64-bit systems and will fail the validation in IntFieldValidator
if (is_string($this->value)) {
if (!is_null($this->minValue) && bccomp($this->value, static::MIN_INT) === -1) {
$result->addFieldError($this->name, $this->getTooSmallMessage());
}
if (!is_null($this->maxValue) && bccomp($this->value, static::MAX_INT) === 1) {
$result->addFieldError($this->name, $this->getTooLargeMessage());
}
}
$result->combineAnd(parent::validateValue());
return $result;
}
}
22 changes: 22 additions & 0 deletions src/Core/Validation/FieldValidation/BooleanFieldValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace SilverStripe\Core\Validation\FieldValidation;

use SilverStripe\Core\Validation\ValidationResult;
use SilverStripe\Core\Validation\FieldValidation\FieldValidator;

/**
* Validates that a value is a boolean
*/
class BooleanFieldValidator extends FieldValidator
{
protected function validateValue(): ValidationResult
{
$result = ValidationResult::create();
if (!is_bool($this->value)) {
$message = _t(__CLASS__ . '.INVALID', 'Invalid value');
$result->addFieldError($this->name, $message);
}
return $result;
}
}
39 changes: 39 additions & 0 deletions src/Core/Validation/FieldValidation/CompositeFieldValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

namespace SilverStripe\Core\Validation\FieldValidation;

use InvalidArgumentException;
use SilverStripe\Core\Validation\ValidationResult;
use SilverStripe\Core\Validation\FieldValidation\FieldValidator;
use SilverStripe\Core\Validation\FieldValidation\FieldValidationInterface;

/**
* A field validator used to validate DBComposite fields
*/
class CompositeFieldValidator extends FieldValidator
{
/**
* @param mixed $value - an iterable list of FieldValidators
*/
public function __construct(string $name, mixed $value)
{
parent::__construct($name, $value);
if (!is_iterable($value)) {
throw new InvalidArgumentException('Value must be iterable');
}
foreach ($value as $child) {
if (!is_a($child, FieldValidationInterface::class)) {
throw new InvalidArgumentException('Child is not a' . FieldValidationInterface::class);
}
}
}

protected function validateValue(): ValidationResult
{
$result = ValidationResult::create();
foreach ($this->value as $child) {
$result->combineAnd($child->validate());
}
return $result;
}
}
41 changes: 41 additions & 0 deletions src/Core/Validation/FieldValidation/DateFieldValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace SilverStripe\Core\Validation\FieldValidation;

use SilverStripe\Core\Validation\FieldValidation\FieldValidator;
use SilverStripe\Core\Validation\ValidationResult;

/**
* Validates that a value is a valid date, which means that it follows the equivalent formats:
* - PHP date format Y-m-d
* - SO format y-MM-dd i.e. DBDate::ISO_DATE
*
* Blank string values are allowed
*/
class DateFieldValidator extends FieldValidator
{
protected function validateValue(): ValidationResult
{
$result = ValidationResult::create();
// Allow empty strings
if ($this->value === '') {
return $result;
}
// Not using symfony/validator because it was allowing d-m-Y format strings
$date = date_parse_from_format($this->getFormat(), $this->value ?? '');
if ($date === false || $date['error_count'] > 0 || $date['warning_count'] > 0) {
$result->addFieldError($this->name, $this->getMessage());
}
return $result;
}

protected function getFormat(): string
{
return 'Y-m-d';
}

protected function getMessage(): string
{
return _t(__CLASS__ . '.INVALID', 'Invalid date');
}
}
25 changes: 25 additions & 0 deletions src/Core/Validation/FieldValidation/DatetimeFieldValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace SilverStripe\Core\Validation\FieldValidation;

use SilverStripe\Core\Validation\FieldValidation\DateFieldValidator;

/**
* Validates that a value is a valid date/time, which means that it follows the equivalent formats:
* - PHP date format Y-m-d H:i:s
* - ISO format 'y-MM-dd HH:mm:ss' i.e. DBDateTime::ISO_DATETIME
*
* Blank string values are allowed
*/
class DatetimeFieldValidator extends DateFieldValidator
{
protected function getFormat(): string
{
return 'Y-m-d H:i:s';
}

protected function getMessage(): string
{
return _t(__CLASS__ . '.INVALID', 'Invalid date/time');
}
}
78 changes: 78 additions & 0 deletions src/Core/Validation/FieldValidation/DecimalFieldValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

namespace SilverStripe\Core\Validation\FieldValidation;

use SilverStripe\Core\Validation\ValidationResult;
use SilverStripe\Core\Validation\FieldValidation\NumericFieldValidator;

/**
* Validates that a value is a valid decimal
* This intended for use when validating that a value can be stored in a database as a decimal
*
* 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 rounded to 1000.00 which exceeds 5 total digits
*/
class DecimalFieldValidator extends NumericFieldValidator
{
/**
* Whole number size e.g. For Decimal(9,2) this would be 9
*/
private int $wholeSize;

/**
* Decimal size e.g. For Decimal(5,2) this would be 2
*/
private int $decimalSize;

public function __construct(
string $name,
mixed $value,
int $wholeSize,
int $decimalSize,
int $minValue = null,
int $maxValue = null,
) {
parent::__construct($name, $value, $minValue, $maxValue);
$this->wholeSize = $wholeSize;
$this->decimalSize = $decimalSize;
}

protected function validateValue(): ValidationResult
{
$result = parent::validateValue();
if (!$result->isValid()) {
return $result;
}
// Convert to absolute value - the minus sign is not relevant for validation
$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',
'Cannot have more than {wholeSize} digits which includes {decimalSize} decimal places',
[
'wholeSize' => $this->wholeSize,
'decimalSize' => $this->decimalSize
]
);
$result->addFieldError($this->name, $message);
}
return $result;
}
}
24 changes: 24 additions & 0 deletions src/Core/Validation/FieldValidation/EmailFieldValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace SilverStripe\Core\Validation\FieldValidation;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\Email;
use SilverStripe\Core\Validation\FieldValidation\StringFieldValidator;
use SilverStripe\Core\Validation\FieldValidation\SymfonyFieldValidatorTrait;
use SilverStripe\Core\Validation\FieldValidation\SymfonyFieldValidatorInterface;

/**
* Validates that a value is a valid email address
* Uses Symfony's Email constraint to validate
*/
class EmailFieldValidator extends StringFieldValidator implements SymfonyFieldValidatorInterface
{
use SymfonyFieldValidatorTrait;

public function getConstraint(): Constraint|array
{
$message = _t(__CLASS__ . '.INVALID', 'Invalid email address');
return new Email(message: $message);
}
}
18 changes: 18 additions & 0 deletions src/Core/Validation/FieldValidation/FieldValidationInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace SilverStripe\Core\Validation\FieldValidation;

use SilverStripe\Core\Validation\ValidationInterface;

/**
* Interface for fields e.g. a DBField or FormField, that can use FieldValidator's
* Intended for use on classes that have the FieldValidationTrait applied
*/
interface FieldValidationInterface extends ValidationInterface
{
public function getName(): string;

public function getValue(): mixed;

public function getValueForValidation(): mixed;
}
Loading

0 comments on commit 3f863b2

Please sign in to comment.