Skip to content

Commit e733757

Browse files
committed
ENH Use FieldValidators for FormField validation
1 parent e057675 commit e733757

29 files changed

+485
-713
lines changed

src/Core/Validation/FieldValidation/DateFieldValidator.php

Lines changed: 101 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,113 @@
22

33
namespace SilverStripe\Core\Validation\FieldValidation;
44

5+
use Exception;
6+
use InvalidArgumentException;
57
use SilverStripe\Core\Validation\FieldValidation\FieldValidator;
68
use SilverStripe\Core\Validation\ValidationResult;
79

810
/**
911
* Validates that a value is a valid date, which means that it follows the equivalent formats:
1012
* - PHP date format Y-m-d
11-
* - SO format y-MM-dd i.e. DBDate::ISO_DATE
13+
* - ISO format y-MM-dd i.e. DBDate::ISO_DATE
1214
*
1315
* Blank string values are allowed
1416
*/
1517
class DateFieldValidator extends FieldValidator
1618
{
19+
/**
20+
* Minimum value as a date string
21+
*/
22+
private ?string $minValue;
23+
24+
/**
25+
* Minimum value as a unix timestamp
26+
*/
27+
private ?int $minTimestamp;
28+
29+
/**
30+
* Maximum value as a date string
31+
*/
32+
private ?string $maxValue;
33+
34+
/**
35+
* Maximum value as a unix timestamp
36+
*/
37+
private ?int $maxTimestamp;
38+
39+
/**
40+
* Converter to convert date strings to another format for display in error messages
41+
*
42+
* @var callable
43+
*/
44+
private $converter;
45+
46+
public function __construct(
47+
string $name,
48+
mixed $value,
49+
?int $minValue = null,
50+
?int $maxValue = null,
51+
?callable $converter = null,
52+
) {
53+
// Convert Y-m-d args to timestamps
54+
// Intermiediate variables are used to prevent "must not be accessed before initialization" PHP errors
55+
// when reading properties in the constructor
56+
$minTimestamp = null;
57+
$maxTimestamp = null;
58+
if (!is_null($minValue)) {
59+
$minTimestamp = $this->dateToTimestamp($minValue);
60+
}
61+
if (!is_null($maxValue)) {
62+
$maxTimestamp = $this->dateToTimestamp($maxValue);
63+
}
64+
if (!is_null($minTimestamp) && !is_null($maxTimestamp) && $minTimestamp < $maxTimestamp) {
65+
throw new InvalidArgumentException('maxValue cannot be less than minValue');
66+
}
67+
$this->minValue = $minValue;
68+
$this->maxValue = $maxValue;
69+
$this->minTimestamp = $minTimestamp;
70+
$this->maxTimestamp = $maxTimestamp;
71+
$this->converter = $converter;
72+
parent::__construct($name, $value);
73+
}
74+
1775
protected function validateValue(): ValidationResult
1876
{
1977
$result = ValidationResult::create();
2078
// Allow empty strings
2179
if ($this->value === '') {
2280
return $result;
2381
}
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) {
82+
// Validate value is a valid date
83+
try {
84+
$timestamp = $this->dateToTimestamp($this->value ?? '');
85+
} catch (Exception) {
2786
$result->addFieldError($this->name, $this->getMessage());
87+
return $result;
88+
}
89+
// Validate value is within range
90+
if (!is_null($this->minTimestamp) && $timestamp < $this->minTimestamp) {
91+
$minValue = $this->minValue;
92+
if (!is_null($this->converter)) {
93+
$minValue = call_user_func($this->converter, $this->minValue) ?: $this->minValue;
94+
}
95+
$message = _t(
96+
__CLASS__ . '.TOOSMALL',
97+
'Value cannot be less than {minValue}',
98+
['minValue' => $minValue]
99+
);
100+
$result->addFieldError($this->name, $message);
101+
} elseif (!is_null($this->maxTimestamp) && $timestamp > $this->maxTimestamp) {
102+
$maxValue = $this->maxValue;
103+
if (!is_null($this->converter)) {
104+
$maxValue = call_user_func($this->converter, $this->maxValue) ?: $this->maxValue;
105+
}
106+
$message = _t(
107+
__CLASS__ . '.TOOLARGE',
108+
'Value cannot be greater than {maxValue}',
109+
['maxValue' => $maxValue]
110+
);
111+
$result->addFieldError($this->name, $message);
28112
}
29113
return $result;
30114
}
@@ -38,4 +122,17 @@ protected function getMessage(): string
38122
{
39123
return _t(__CLASS__ . '.INVALID', 'Invalid date');
40124
}
125+
126+
/**
127+
* Parse a date string into a unix timestamp using the format specified by getFormat()
128+
*/
129+
private function dateToTimestamp(string $date): int
130+
{
131+
// Not using symfony/validator because it was allowing d-m-Y format strings
132+
$date = date_parse_from_format($this->getFormat(), $date);
133+
if ($date === false || $date['error_count'] > 0 || $date['warning_count'] > 0) {
134+
throw new InvalidArgumentException('Invalid date');
135+
}
136+
return mktime($date['hour'], $date['minute'], $date['second'], $date['month'], $date['day'], $date['year']);
137+
}
41138
}

src/Forms/CompositeField.php

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace SilverStripe\Forms;
44

55
use SilverStripe\Dev\Debug;
6+
use SilverStripe\Core\Validation\ValidationResult;
67

78
/**
89
* Base class for all fields that contain other fields.
@@ -120,7 +121,7 @@ public function getChildren()
120121
* If the CompositeField doesn't have a name, but we still want the ID/name to be set.
121122
* This code generates the ID from the nested children.
122123
*/
123-
public function getName()
124+
public function getName(): string
124125
{
125126
if ($this->name) {
126127
return $this->name;
@@ -523,18 +524,18 @@ public function debug(): string
523524
return $result;
524525
}
525526

526-
/**
527-
* Validate this field
528-
*
529-
* @param Validator $validator
530-
* @return bool
531-
*/
532-
public function validate($validator)
527+
// TODO: replace with CompositeFieldValidator ?
528+
public function validate(): ValidationResult
533529
{
534-
$valid = true;
535-
foreach ($this->children as $child) {
536-
$valid = ($child && $child->validate($validator) && $valid);
537-
}
538-
return $this->extendValidationResult($valid, $validator);
530+
$result = ValidationResult::create();
531+
$this->beforeExtending('updateValidate', function () use ($result) {
532+
foreach ($this->children as $child) {
533+
if (!$child) {
534+
continue;
535+
}
536+
$result->combineAnd($child->validate());
537+
}
538+
});
539+
return $result->combineAnd(parent::validate());
539540
}
540541
}

0 commit comments

Comments
 (0)