Skip to content

Commit b596697

Browse files
committed
ENH Use FieldValidators for FormField validation
1 parent 574b4e8 commit b596697

File tree

86 files changed

+2502
-1356
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

86 files changed

+2502
-1356
lines changed

src/Core/Validation/FieldValidation/DateFieldValidator.php

+102-9
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,109 @@
22

33
namespace SilverStripe\Core\Validation\FieldValidation;
44

5-
use SilverStripe\Core\Validation\FieldValidation\FieldValidator;
5+
use Error;
6+
use Exception;
7+
use InvalidArgumentException;
68
use SilverStripe\Core\Validation\ValidationResult;
9+
use SilverStripe\Core\Validation\FieldValidation\StringFieldValidator;
710

811
/**
912
* Validates that a value is a valid date, which means that it follows the equivalent formats:
1013
* - PHP date format Y-m-d
11-
* - SO format y-MM-dd i.e. DBDate::ISO_DATE
14+
* - ISO format y-MM-dd i.e. DBDate::ISO_DATE
1215
*
1316
* Blank string values are allowed
1417
*/
15-
class DateFieldValidator extends FieldValidator
18+
class DateFieldValidator extends StringFieldValidator
1619
{
20+
/**
21+
* Minimum value as a date string
22+
*/
23+
private ?string $minValue = null;
24+
25+
/**
26+
* Minimum value as a unix timestamp
27+
*/
28+
private ?int $minTimestamp = null;
29+
30+
/**
31+
* Maximum value as a date string
32+
*/
33+
private ?string $maxValue = null;
34+
35+
/**
36+
* Maximum value as a unix timestamp
37+
*/
38+
private ?int $maxTimestamp = null;
39+
40+
/**
41+
* Converter to convert date strings to another format for display in error messages
42+
*
43+
* @var callable
44+
*/
45+
private $converter;
46+
47+
public function __construct(
48+
string $name,
49+
mixed $value,
50+
?string $minValue = null,
51+
?string $maxValue = null,
52+
?callable $converter = null,
53+
) {
54+
// Convert Y-m-d args to timestamps
55+
if (!is_null($minValue)) {
56+
$this->minTimestamp = $this->dateToTimestamp($minValue);
57+
}
58+
if (!is_null($maxValue)) {
59+
$this->maxTimestamp = $this->dateToTimestamp($maxValue);
60+
}
61+
if (!is_null($this->minTimestamp) && !is_null($this->maxTimestamp)
62+
&& $this->maxTimestamp < $this->minTimestamp
63+
) {
64+
throw new InvalidArgumentException('maxValue cannot be less than minValue');
65+
}
66+
$this->minValue = $minValue;
67+
$this->maxValue = $maxValue;
68+
$this->converter = $converter;
69+
parent::__construct($name, $value);
70+
}
71+
1772
protected function validateValue(): ValidationResult
1873
{
19-
$result = ValidationResult::create();
20-
// Allow empty strings
21-
if ($this->value === '') {
74+
$result = parent::validateValue();
75+
// If the value is not a string, we can't validate it as a date
76+
if (!$result->isValid()) {
2277
return $result;
2378
}
24-
// Not using symfony/validator because it was allowing d-m-Y format strings
25-
$date = date_parse_from_format($this->getFormat(), $this->value ?? '');
26-
if ($date === false || $date['error_count'] > 0 || $date['warning_count'] > 0) {
79+
// Validate value is a valid date
80+
$timestamp = $this->dateToTimestamp($this->value ?? '');
81+
if ($timestamp === false) {
2782
$result->addFieldError($this->name, $this->getMessage());
83+
return $result;
84+
}
85+
// Validate value is within range
86+
if (!is_null($this->minTimestamp) && $timestamp < $this->minTimestamp) {
87+
$minValue = $this->minValue;
88+
if (!is_null($this->converter)) {
89+
$minValue = call_user_func($this->converter, $this->minValue) ?: $this->minValue;
90+
}
91+
$message = _t(
92+
__CLASS__ . '.TOOSMALL',
93+
'Value cannot be older than {minValue}',
94+
['minValue' => $minValue]
95+
);
96+
$result->addFieldError($this->name, $message);
97+
} elseif (!is_null($this->maxTimestamp) && $timestamp > $this->maxTimestamp) {
98+
$maxValue = $this->maxValue;
99+
if (!is_null($this->converter)) {
100+
$maxValue = call_user_func($this->converter, $this->maxValue) ?: $this->maxValue;
101+
}
102+
$message = _t(
103+
__CLASS__ . '.TOOLARGE',
104+
'Value cannot be newer than {maxValue}',
105+
['maxValue' => $maxValue]
106+
);
107+
$result->addFieldError($this->name, $message);
28108
}
29109
return $result;
30110
}
@@ -38,4 +118,17 @@ protected function getMessage(): string
38118
{
39119
return _t(__CLASS__ . '.INVALID', 'Invalid date');
40120
}
121+
122+
/**
123+
* Parse a date string into a unix timestamp using the format specified by getFormat()
124+
*/
125+
private function dateToTimestamp(string $date): int|false
126+
{
127+
// Not using symfony/validator because it was allowing d-m-Y format strings
128+
$date = date_parse_from_format($this->getFormat(), $date);
129+
if ($date === false || $date['error_count'] > 0 || $date['warning_count'] > 0) {
130+
return false;
131+
}
132+
return mktime($date['hour'], $date['minute'], $date['second'], $date['month'], $date['day'], $date['year']);
133+
}
41134
}

src/Core/Validation/FieldValidation/DecimalFieldValidator.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
namespace SilverStripe\Core\Validation\FieldValidation;
44

55
use SilverStripe\Core\Validation\ValidationResult;
6-
use SilverStripe\Core\Validation\FieldValidation\NumericFieldValidator;
6+
use SilverStripe\Core\Validation\FieldValidation\NumericNonStringFieldValidator;
77

88
/**
99
* Validates that a value is a valid decimal
@@ -23,7 +23,7 @@
2323
* 1234.9 - 4 digits the before the decimal point
2424
* 999.999 - would be rounded to 1000.00 which exceeds 5 total digits
2525
*/
26-
class DecimalFieldValidator extends NumericFieldValidator
26+
class DecimalFieldValidator extends NumericNonStringFieldValidator
2727
{
2828
/**
2929
* Whole number size e.g. For Decimal(9,2) this would be 9

src/Core/Validation/FieldValidation/EmailFieldValidator.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,6 @@ class EmailFieldValidator extends StringFieldValidator implements SymfonyFieldVa
1919
public function getConstraint(): Constraint|array
2020
{
2121
$message = _t(__CLASS__ . '.INVALID', 'Invalid email address');
22-
return new Email(message: $message);
22+
return new Email(message: $message, mode: Email::VALIDATION_MODE_STRICT);
2323
}
2424
}

src/Core/Validation/FieldValidation/FieldValidationTrait.php

+23-10
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,17 @@ trait FieldValidationTrait
2121
*
2222
* Each item in the array can be one of the following
2323
* a) MyFieldValidator::class,
24-
* b) MyFieldValidator::class => [null, 'getSomething'],
25-
* c) MyFieldValidator::class => null,
24+
* b) MyFieldValidator::class => ['argNameA' => null, 'argNameB' => 'getSomething'],
25+
* d) MyFieldValidator::class => null,
2626
*
2727
* a) Will create a MyFieldValidator and pass the name and value of the field as args to the constructor
2828
* b) Will create a MyFieldValidator and pass the name, value, and pass additional args, where each null values
2929
* will be passed as null, and non-null values will call a method on the field e.g. will pass null for the first
3030
* additional arg and call $field->getSomething() to get a value for the second additional arg
31+
* Keys are used to speicify the arg name, which is done to prevents duplicate
32+
* args being add to config when a subclass defines the same FieldValidator as a parent class.
33+
* Note that keys are not named args, they are simply arbitary keys - though best practice is
34+
* for the keys to match constructor argument names.
3135
* c) Will disable a previously set MyFieldValidator. This is useful to disable a FieldValidator that was set
3236
* on a parent class
3337
*
@@ -49,6 +53,18 @@ public function validate(): ValidationResult
4953
return $result;
5054
}
5155

56+
/**
57+
* Get the value of this field for use in validation via FieldValidators
58+
*
59+
* Intended to be overridden in subclasses when there is a need to provide something different
60+
* from the value of the field itself, for instance DBComposite and CompositeField which need to
61+
* provide a value that is a combination of the values of their children
62+
*/
63+
public function getValueForValidation(): mixed
64+
{
65+
return $this->getValue();
66+
}
67+
5268
/**
5369
* Get instantiated FieldValidators based on `field_validators` configuration
5470
*/
@@ -62,11 +78,9 @@ private function getFieldValidators(): array
6278
}
6379
/** @var FieldValidationInterface|Configurable $this */
6480
$name = $this->getName();
81+
// For composite fields e.g. MyCompositeField[MySubField] we want to use the name of the composite field
82+
$name = preg_replace('#\[[^\]]+\]$#', '', $name);
6583
$value = $this->getValueForValidation();
66-
// Field name is required for FieldValidators when called ValidationResult::addFieldMessage()
67-
if ($name === '') {
68-
throw new RuntimeException('Field name is blank');
69-
}
7084
$classes = $this->getClassesFromConfig();
7185
foreach ($classes as $class => $argCalls) {
7286
$args = [$name, $value];
@@ -122,10 +136,9 @@ private function getClassesFromConfig(): array
122136
if (!is_array($argCalls)) {
123137
throw new RuntimeException("argCalls for FieldValidator $class is not an array");
124138
}
125-
// array_unique() is used to dedupe any cases where a subclass defines the same FieldValidator
126-
// this config can happens when a subclass defines a FieldValidator that was already defined on a parent
127-
// class, though it calls different methods
128-
$argCalls = array_unique($argCalls);
139+
// Ensure that argCalls is a numerically indexed array
140+
// as they may have been defined with string keys to prevent duplicate args
141+
$argCalls = array_values($argCalls);
129142
$classes[$class] = $argCalls;
130143
}
131144
foreach (array_keys($disabledClasses) as $class) {

src/Core/Validation/FieldValidation/IntFieldValidator.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
/**
99
* Validates that a value is a 32-bit signed integer
1010
*/
11-
class IntFieldValidator extends NumericFieldValidator
11+
class IntFieldValidator extends NumericNonStringFieldValidator
1212
{
1313
/**
1414
* The minimum value for a signed 32-bit integer.

src/Core/Validation/FieldValidation/MultiOptionFieldValidator.php

+5-5
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
namespace SilverStripe\Core\Validation\FieldValidation;
44

5-
use InvalidArgumentException;
65
use SilverStripe\Core\Validation\ValidationResult;
76
use SilverStripe\Core\Validation\FieldValidation\OptionFieldValidator;
87

@@ -12,19 +11,20 @@
1211
class MultiOptionFieldValidator extends OptionFieldValidator
1312
{
1413
/**
15-
* @param mixed $value - an array of values to be validated
14+
* @param mixed $value - an iterable set of values to be validated
1615
*/
1716
public function __construct(string $name, mixed $value, array $options)
1817
{
19-
if (!is_iterable($value) && !is_null($value)) {
20-
throw new InvalidArgumentException('Value must be iterable');
21-
}
2218
parent::__construct($name, $value, $options);
2319
}
2420

2521
protected function validateValue(): ValidationResult
2622
{
2723
$result = ValidationResult::create();
24+
if (!is_iterable($this->value) && !is_null($this->value)) {
25+
$result->addFieldError($this->name, $this->getMessage());
26+
return $result;
27+
}
2828
foreach ($this->value as $value) {
2929
$this->checkValueInOptions($value, $result);
3030
}

src/Core/Validation/FieldValidation/NumericFieldValidator.php

+3-4
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public function __construct(
2626
string $name,
2727
mixed $value,
2828
?int $minValue = null,
29-
?int $maxValue = null
29+
?int $maxValue = null,
3030
) {
3131
if (!is_null($minValue) && !is_null($maxValue) && $maxValue < $minValue) {
3232
throw new InvalidArgumentException('maxValue cannot be less than minValue');
@@ -39,9 +39,8 @@ public function __construct(
3939
protected function validateValue(): ValidationResult
4040
{
4141
$result = ValidationResult::create();
42-
if (!is_numeric($this->value) || is_string($this->value)) {
43-
// Must be a numeric value, though not as a numeric string
44-
$message = _t(__CLASS__ . '.WRONGTYPE', 'Must be numeric, and not a string');
42+
if (!is_numeric($this->value)) {
43+
$message = _t(__CLASS__ . '.NOTNUMERIC', 'Must be numeric');
4544
$result->addFieldError($this->name, $message);
4645
} elseif (!is_null($this->minValue) && $this->value < $this->minValue) {
4746
$result->addFieldError($this->name, $this->getTooSmallMessage());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace SilverStripe\Core\Validation\FieldValidation;
4+
5+
use SilverStripe\Core\Validation\ValidationResult;
6+
use SilverStripe\Core\Validation\FieldValidation\NumericFieldValidator;
7+
8+
/**
9+
* Validates that a value is a numeric value and not a string
10+
*/
11+
class NumericNonStringFieldValidator extends NumericFieldValidator
12+
{
13+
protected function validateValue(): ValidationResult
14+
{
15+
$result = parent::validateValue();
16+
if (is_numeric($this->value) && is_string($this->value)) {
17+
$message = _t(__CLASS__ . '.WRONGTYPE', 'Must be numeric and not a string');
18+
$result->addFieldError($this->name, $message);
19+
}
20+
return $result;
21+
}
22+
}

src/Core/Validation/FieldValidation/OptionFieldValidator.php

+6-2
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,12 @@ protected function validateValue(): ValidationResult
3131
protected function checkValueInOptions(mixed $value, ValidationResult $result): void
3232
{
3333
if (!in_array($value, $this->options, true)) {
34-
$message = _t(__CLASS__ . '.NOTALLOWED', 'Not an allowed value');
35-
$result->addFieldError($this->name, $message);
34+
$result->addFieldError($this->name, $this->getMessage());
3635
}
3736
}
37+
38+
protected function getMessage(): string
39+
{
40+
return _t(__CLASS__ . '.NOTALLOWED', 'Not an allowed value');
41+
}
3842
}

src/Core/Validation/FieldValidation/StringFieldValidator.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ protected function validateValue(): ValidationResult
6161
if (!is_null($this->maxLength) && $len > $this->maxLength) {
6262
$message = _t(
6363
__CLASS__ . '.TOOLONG',
64-
'Can not have more than {maxLength} characters',
64+
'Cannot have more than {maxLength} characters',
6565
['maxLength' => $this->maxLength]
6666
);
6767
$result->addFieldError($this->name, $message);

0 commit comments

Comments
 (0)