22
33namespace SilverStripe \Core \Validation \FieldValidation ;
44
5+ use Exception ;
6+ use InvalidArgumentException ;
57use SilverStripe \Core \Validation \FieldValidation \FieldValidator ;
68use 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 */
1517class 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}
0 commit comments