Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH Use FieldValidators for FormField validation #11449

Draft
wants to merge 3 commits into
base: 6
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
}
138 changes: 138 additions & 0 deletions src/Core/Validation/FieldValidation/DateFieldValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<?php

namespace SilverStripe\Core\Validation\FieldValidation;

use Exception;
use InvalidArgumentException;
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
* - ISO format y-MM-dd i.e. DBDate::ISO_DATE
*
* Blank string values are allowed
*/
class DateFieldValidator extends FieldValidator
{
/**
* Minimum value as a date string
*/
private ?string $minValue;

/**
* Minimum value as a unix timestamp
*/
private ?int $minTimestamp;

/**
* Maximum value as a date string
*/
private ?string $maxValue;

/**
* Maximum value as a unix timestamp
*/
private ?int $maxTimestamp;

/**
* Converter to convert date strings to another format for display in error messages
*
* @var callable
*/
private $converter;

public function __construct(
string $name,
mixed $value,
?int $minValue = null,
?int $maxValue = null,
?callable $converter = null,
) {
// Convert Y-m-d args to timestamps
// Intermiediate variables are used to prevent "must not be accessed before initialization" PHP errors
// when reading properties in the constructor
$minTimestamp = null;
$maxTimestamp = null;
if (!is_null($minValue)) {
$minTimestamp = $this->dateToTimestamp($minValue);
}
if (!is_null($maxValue)) {
$maxTimestamp = $this->dateToTimestamp($maxValue);
}
if (!is_null($minTimestamp) && !is_null($maxTimestamp) && $minTimestamp < $maxTimestamp) {
throw new InvalidArgumentException('maxValue cannot be less than minValue');
}
$this->minValue = $minValue;
$this->maxValue = $maxValue;
$this->minTimestamp = $minTimestamp;
$this->maxTimestamp = $maxTimestamp;
$this->converter = $converter;
parent::__construct($name, $value);
}

protected function validateValue(): ValidationResult
{
$result = ValidationResult::create();
// Allow empty strings
if ($this->value === '') {
return $result;
}
// Validate value is a valid date
try {
$timestamp = $this->dateToTimestamp($this->value ?? '');
} catch (Exception) {
$result->addFieldError($this->name, $this->getMessage());
return $result;
}
// Validate value is within range
if (!is_null($this->minTimestamp) && $timestamp < $this->minTimestamp) {
$minValue = $this->minValue;
if (!is_null($this->converter)) {
$minValue = call_user_func($this->converter, $this->minValue) ?: $this->minValue;
}
$message = _t(
__CLASS__ . '.TOOSMALL',
'Value cannot be less than {minValue}',
['minValue' => $minValue]
);
$result->addFieldError($this->name, $message);
} elseif (!is_null($this->maxTimestamp) && $timestamp > $this->maxTimestamp) {
$maxValue = $this->maxValue;
if (!is_null($this->converter)) {
$maxValue = call_user_func($this->converter, $this->maxValue) ?: $this->maxValue;
}
$message = _t(
__CLASS__ . '.TOOLARGE',
'Value cannot be greater than {maxValue}',
['maxValue' => $maxValue]
);
$result->addFieldError($this->name, $message);
}
return $result;
}

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

protected function getMessage(): string
{
return _t(__CLASS__ . '.INVALID', 'Invalid date');
}

/**
* Parse a date string into a unix timestamp using the format specified by getFormat()
*/
private function dateToTimestamp(string $date): int
{
// Not using symfony/validator because it was allowing d-m-Y format strings
$date = date_parse_from_format($this->getFormat(), $date);
if ($date === false || $date['error_count'] > 0 || $date['warning_count'] > 0) {
throw new InvalidArgumentException('Invalid date');
}
return mktime($date['hour'], $date['minute'], $date['second'], $date['month'], $date['day'], $date['year']);
}
}
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');
}
}
Loading
Loading