Skip to content

Commit 220c8e4

Browse files
committed
ENH Use FieldValidators for FormField validation
1 parent 26d5b11 commit 220c8e4

File tree

96 files changed

+3051
-1546
lines changed

Some content is hidden

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

96 files changed

+3051
-1546
lines changed

src/Core/Validation/FieldValidation/BigIntFieldValidator.php

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

55
use RunTimeException;
6-
use SilverStripe\Core\Validation\FieldValidation\IntFieldValidator;
76
use SilverStripe\Core\Validation\ValidationResult;
87

98
/**

src/Core/Validation/FieldValidation/BooleanFieldValidator.php

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

55
use SilverStripe\Core\Validation\ValidationResult;
6-
use SilverStripe\Core\Validation\FieldValidation\FieldValidator;
76

87
/**
98
* Validates that a value is a boolean

src/Core/Validation/FieldValidation/CompositeFieldValidator.php

-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
use InvalidArgumentException;
66
use SilverStripe\Core\Validation\ValidationResult;
7-
use SilverStripe\Core\Validation\FieldValidation\FieldValidator;
87
use SilverStripe\Core\Validation\FieldValidation\FieldValidationInterface;
98

109
/**

src/Core/Validation/FieldValidation/DateFieldValidator.php

+99-9
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,106 @@
22

33
namespace SilverStripe\Core\Validation\FieldValidation;
44

5-
use SilverStripe\Core\Validation\FieldValidation\FieldValidator;
5+
use InvalidArgumentException;
66
use SilverStripe\Core\Validation\ValidationResult;
77

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

src/Core/Validation/FieldValidation/DatetimeFieldValidator.php

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

33
namespace SilverStripe\Core\Validation\FieldValidation;
44

5-
use SilverStripe\Core\Validation\FieldValidation\DateFieldValidator;
6-
75
/**
86
* Validates that a value is a valid date/time, which means that it follows the equivalent formats:
97
* - PHP date format Y-m-d H:i:s

src/Core/Validation/FieldValidation/DecimalFieldValidator.php

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

55
use SilverStripe\Core\Validation\ValidationResult;
6-
use SilverStripe\Core\Validation\FieldValidation\NumericFieldValidator;
76

87
/**
98
* Validates that a value is a valid decimal
@@ -23,7 +22,7 @@
2322
* 1234.9 - 4 digits the before the decimal point
2423
* 999.999 - would be rounded to 1000.00 which exceeds 5 total digits
2524
*/
26-
class DecimalFieldValidator extends NumericFieldValidator
25+
class DecimalFieldValidator extends NumericNonStringFieldValidator
2726
{
2827
/**
2928
* Whole number size e.g. For Decimal(9,2) this would be 9

src/Core/Validation/FieldValidation/EmailFieldValidator.php

+1-4
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@
44

55
use Symfony\Component\Validator\Constraint;
66
use Symfony\Component\Validator\Constraints\Email;
7-
use SilverStripe\Core\Validation\FieldValidation\StringFieldValidator;
8-
use SilverStripe\Core\Validation\FieldValidation\SymfonyFieldValidatorTrait;
9-
use SilverStripe\Core\Validation\FieldValidation\SymfonyFieldValidatorInterface;
107

118
/**
129
* Validates that a value is a valid email address
@@ -19,6 +16,6 @@ class EmailFieldValidator extends StringFieldValidator implements SymfonyFieldVa
1916
public function getConstraint(): Constraint|array
2017
{
2118
$message = _t(__CLASS__ . '.INVALID', 'Invalid email address');
22-
return new Email(message: $message);
19+
return new Email(message: $message, mode: Email::VALIDATION_MODE_STRICT);
2320
}
2421
}

src/Core/Validation/FieldValidation/FieldValidationTrait.php

+23-11
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
use RuntimeException;
66
use SilverStripe\Core\Injector\Injector;
77
use SilverStripe\Core\Config\Configurable;
8-
use SilverStripe\Core\Validation\FieldValidation\FieldValidationInterface;
98
use SilverStripe\Core\Validation\ValidationResult;
109

1110
/**
@@ -21,13 +20,17 @@ trait FieldValidationTrait
2120
*
2221
* Each item in the array can be one of the following
2322
* a) MyFieldValidator::class,
24-
* b) MyFieldValidator::class => [null, 'getSomething'],
25-
* c) MyFieldValidator::class => null,
23+
* b) MyFieldValidator::class => ['argNameA' => null, 'argNameB' => 'getSomething'],
24+
* d) MyFieldValidator::class => null,
2625
*
2726
* a) Will create a MyFieldValidator and pass the name and value of the field as args to the constructor
2827
* b) Will create a MyFieldValidator and pass the name, value, and pass additional args, where each null values
2928
* will be passed as null, and non-null values will call a method on the field e.g. will pass null for the first
3029
* additional arg and call $field->getSomething() to get a value for the second additional arg
30+
* Keys are used to speicify the arg name, which is done to prevents duplicate
31+
* args being add to config when a subclass defines the same FieldValidator as a parent class.
32+
* Note that keys are not named args, they are simply arbitary keys - though best practice is
33+
* for the keys to match constructor argument names.
3134
* c) Will disable a previously set MyFieldValidator. This is useful to disable a FieldValidator that was set
3235
* on a parent class
3336
*
@@ -49,6 +52,18 @@ public function validate(): ValidationResult
4952
return $result;
5053
}
5154

55+
/**
56+
* Get the value of this field for use in validation via FieldValidators
57+
*
58+
* Intended to be overridden in subclasses when there is a need to provide something different
59+
* from the value of the field itself, for instance DBComposite and CompositeField which need to
60+
* provide a value that is a combination of the values of their children
61+
*/
62+
public function getValueForValidation(): mixed
63+
{
64+
return $this->getValue();
65+
}
66+
5267
/**
5368
* Get instantiated FieldValidators based on `field_validators` configuration
5469
*/
@@ -62,11 +77,9 @@ private function getFieldValidators(): array
6277
}
6378
/** @var FieldValidationInterface|Configurable $this */
6479
$name = $this->getName();
80+
// For composite fields e.g. MyCompositeField[MySubField] we want to use the name of the composite field
81+
$name = preg_replace('#\[[^\]]+\]$#', '', $name);
6582
$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-
}
7083
$classes = $this->getClassesFromConfig();
7184
foreach ($classes as $class => $argCalls) {
7285
$args = [$name, $value];
@@ -122,10 +135,9 @@ private function getClassesFromConfig(): array
122135
if (!is_array($argCalls)) {
123136
throw new RuntimeException("argCalls for FieldValidator $class is not an array");
124137
}
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);
138+
// Ensure that argCalls is a numerically indexed array
139+
// as they may have been defined with string keys to prevent duplicate args
140+
$argCalls = array_values($argCalls);
129141
$classes[$class] = $argCalls;
130142
}
131143
foreach (array_keys($disabledClasses) as $class) {

src/Core/Validation/FieldValidation/IntFieldValidator.php

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

55
use SilverStripe\Core\Validation\ValidationResult;
6-
use SilverStripe\Core\Validation\FieldValidation\NumericFieldValidator;
76

87
/**
98
* Validates that a value is a 32-bit signed integer
109
*/
11-
class IntFieldValidator extends NumericFieldValidator
10+
class IntFieldValidator extends NumericNonStringFieldValidator
1211
{
1312
/**
1413
* The minimum value for a signed 32-bit integer.

src/Core/Validation/FieldValidation/IpFieldValidator.php

-3
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@
44

55
use Symfony\Component\Validator\Constraint;
66
use Symfony\Component\Validator\Constraints\Ip;
7-
use SilverStripe\Core\Validation\FieldValidation\StringFieldValidator;
8-
use SilverStripe\Core\Validation\FieldValidation\SymfonyFieldValidatorTrait;
9-
use SilverStripe\Core\Validation\FieldValidation\SymfonyFieldValidatorInterface;
107

118
/**
129
* Validates that a value is a valid IP address

src/Core/Validation/FieldValidation/LocaleFieldValidator.php

-3
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@
44

55
use Symfony\Component\Validator\Constraint;
66
use Symfony\Component\Validator\Constraints\Locale;
7-
use SilverStripe\Core\Validation\FieldValidation\StringFieldValidator;
8-
use SilverStripe\Core\Validation\FieldValidation\SymfonyFieldValidatorTrait;
9-
use SilverStripe\Core\Validation\FieldValidation\SymfonyFieldValidatorInterface;
107

118
/**
129
* Validates that a value is a valid locale

src/Core/Validation/FieldValidation/MultiOptionFieldValidator.php

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

33
namespace SilverStripe\Core\Validation\FieldValidation;
44

5-
use InvalidArgumentException;
65
use SilverStripe\Core\Validation\ValidationResult;
7-
use SilverStripe\Core\Validation\FieldValidation\OptionFieldValidator;
86

97
/**
108
* Validates that that all values are one of a set of options
119
*/
1210
class MultiOptionFieldValidator extends OptionFieldValidator
1311
{
1412
/**
15-
* @param mixed $value - an array of values to be validated
13+
* @param mixed $value - an iterable set of values to be validated
1614
*/
1715
public function __construct(string $name, mixed $value, array $options)
1816
{
19-
if (!is_iterable($value) && !is_null($value)) {
20-
throw new InvalidArgumentException('Value must be iterable');
21-
}
2217
parent::__construct($name, $value, $options);
2318
}
2419

2520
protected function validateValue(): ValidationResult
2621
{
2722
$result = ValidationResult::create();
23+
if (!is_iterable($this->value) && !is_null($this->value)) {
24+
$result->addFieldError($this->name, $this->getMessage());
25+
return $result;
26+
}
2827
foreach ($this->value as $value) {
2928
$this->checkValueInOptions($value, $result);
3029
}

src/Core/Validation/FieldValidation/NumericFieldValidator.php

+4-6
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22

33
namespace SilverStripe\Core\Validation\FieldValidation;
44

5-
use SilverStripe\Core\Validation\ValidationResult;
6-
use SilverStripe\Core\Validation\FieldValidation\FieldValidator;
75
use InvalidArgumentException;
6+
use SilverStripe\Core\Validation\ValidationResult;
87

98
/**
109
* Validates that a value is a numeric value
@@ -26,7 +25,7 @@ public function __construct(
2625
string $name,
2726
mixed $value,
2827
?int $minValue = null,
29-
?int $maxValue = null
28+
?int $maxValue = null,
3029
) {
3130
if (!is_null($minValue) && !is_null($maxValue) && $maxValue < $minValue) {
3231
throw new InvalidArgumentException('maxValue cannot be less than minValue');
@@ -39,9 +38,8 @@ public function __construct(
3938
protected function validateValue(): ValidationResult
4039
{
4140
$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');
41+
if (!is_numeric($this->value)) {
42+
$message = _t(__CLASS__ . '.NOTNUMERIC', 'Must be numeric');
4543
$result->addFieldError($this->name, $message);
4644
} elseif (!is_null($this->minValue) && $this->value < $this->minValue) {
4745
$result->addFieldError($this->name, $this->getTooSmallMessage());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace SilverStripe\Core\Validation\FieldValidation;
4+
5+
use SilverStripe\Core\Validation\ValidationResult;
6+
7+
/**
8+
* Validates that a value is a numeric value and not a string
9+
*/
10+
class NumericNonStringFieldValidator extends NumericFieldValidator
11+
{
12+
protected function validateValue(): ValidationResult
13+
{
14+
$result = parent::validateValue();
15+
if (is_numeric($this->value) && is_string($this->value)) {
16+
$message = _t(__CLASS__ . '.WRONGTYPE', 'Must be numeric and not a string');
17+
$result->addFieldError($this->name, $message);
18+
}
19+
return $result;
20+
}
21+
}

0 commit comments

Comments
 (0)