1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Utils. |
4
|
|
|
* |
5
|
|
|
* @copyright Copyright (c) 2016 Starweb / Ehandelslogik i Lund AB |
6
|
|
|
* @license BSD 3-Clause |
7
|
|
|
*/ |
8
|
|
|
|
9
|
|
|
namespace Starlit\Utils\Validation; |
10
|
|
|
|
11
|
|
|
use Symfony\Component\Translation\TranslatorInterface as SymfonyTranslatorInterface; |
12
|
|
|
|
13
|
|
|
/** |
14
|
|
|
* A crude validator. |
15
|
|
|
*/ |
16
|
|
|
class Validator |
17
|
|
|
{ |
18
|
|
|
/** |
19
|
|
|
* @var array |
20
|
|
|
*/ |
21
|
|
|
protected static $validRuleProperties = [ |
22
|
|
|
'required', |
23
|
|
|
'nonEmpty', |
24
|
|
|
'min', |
25
|
|
|
'max', |
26
|
|
|
'minLength', |
27
|
|
|
'maxLength', |
28
|
|
|
'regexp', |
29
|
|
|
'regexpExpl', |
30
|
|
|
'email', |
31
|
|
|
'textKey', |
32
|
|
|
'date', |
33
|
|
|
'dateTime', |
34
|
|
|
'custom' |
35
|
|
|
]; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* @var ValidatorTranslatorInterface |
39
|
|
|
*/ |
40
|
|
|
protected $translator; |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* Fields to be validated and their rules. |
44
|
|
|
* |
45
|
|
|
* @var array |
46
|
|
|
*/ |
47
|
|
|
protected $fieldRules; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* @var array |
51
|
|
|
*/ |
52
|
|
|
protected $validatedData = []; |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* @var array |
56
|
|
|
*/ |
57
|
|
|
protected $fieldsRuleProperties; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* Constructor. |
61
|
|
|
* |
62
|
|
|
* @param array $fieldsRuleProperties |
63
|
|
|
* @param ValidatorTranslatorInterface|SymfonyTranslatorInterface|null $translator |
64
|
|
|
*/ |
65
|
28 |
|
public function __construct(array $fieldsRuleProperties = [], $translator = null) |
66
|
|
|
{ |
67
|
28 |
|
$this->fieldsRuleProperties = $fieldsRuleProperties; |
68
|
|
|
|
69
|
28 |
|
if (!$translator) { |
70
|
1 |
|
$this->translator = new DefaultValidatorTranslator(); |
71
|
28 |
|
} elseif ($translator instanceof ValidatorTranslatorInterface) { |
72
|
2 |
|
$this->translator = $translator; |
73
|
28 |
|
} elseif ($translator instanceof SymfonyTranslatorInterface) { |
74
|
27 |
|
$this->translator = new SymfonyTranslatorProxy($translator); |
75
|
27 |
|
} else { |
76
|
1 |
|
throw new \InvalidArgumentException("Translator must implement ValidatorTranslatorInterface"); |
77
|
|
|
} |
78
|
28 |
|
} |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* @param array $newFieldsRuleProperties |
82
|
|
|
*/ |
83
|
3 |
|
public function addFieldsRuleProperties(array $newFieldsRuleProperties) |
84
|
|
|
{ |
85
|
3 |
|
foreach ($newFieldsRuleProperties as $fieldName => $newRuleProperties) { |
86
|
3 |
|
if (isset($this->fieldsRuleProperties[$fieldName])) { |
87
|
1 |
|
$this->fieldsRuleProperties[$fieldName] = array_merge( |
88
|
1 |
|
$this->fieldsRuleProperties[$fieldName], |
89
|
|
|
$newRuleProperties |
90
|
1 |
|
); |
91
|
1 |
|
} else { |
92
|
3 |
|
$this->fieldsRuleProperties[$fieldName] = $newRuleProperties; |
93
|
|
|
} |
94
|
3 |
|
} |
95
|
3 |
|
} |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* @param string $fieldName |
99
|
|
|
*/ |
100
|
1 |
|
public function removeFieldRuleProperties($fieldName) |
101
|
|
|
{ |
102
|
1 |
|
unset($this->fieldsRuleProperties[$fieldName]); |
103
|
1 |
|
} |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* @param string $fieldName |
107
|
|
|
* @return array |
108
|
|
|
*/ |
109
|
2 |
|
public function getFieldRuleProperties($fieldName) |
110
|
|
|
{ |
111
|
2 |
|
if (isset($this->fieldsRuleProperties[$fieldName])) { |
112
|
2 |
|
return $this->fieldsRuleProperties[$fieldName]; |
113
|
|
|
} |
114
|
|
|
|
115
|
1 |
|
return []; |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
/** |
119
|
|
|
* Validate and return error messages (if any). |
120
|
|
|
* |
121
|
|
|
* @param array|null $data The data (e.g. from a form post) to be validated and set |
122
|
|
|
* @return array An array with all (if any) of error messages |
123
|
|
|
*/ |
124
|
3 |
|
public function validate($data) |
125
|
|
|
{ |
126
|
3 |
|
$errorMsgs = []; |
127
|
3 |
|
foreach ($this->fieldsRuleProperties as $fieldName => $ruleProperties) { |
128
|
|
|
// Get value to validate |
129
|
3 |
|
if (isset($data[$fieldName])) { |
130
|
3 |
|
$value = $data[$fieldName]; |
131
|
|
|
|
132
|
|
|
// Trim all values unless explicitly set to not |
133
|
3 |
|
if (is_string($value) && (!isset($ruleProperties['trim']) || $ruleProperties['trim'] === true)) { |
134
|
3 |
|
$value = trim($value); |
135
|
3 |
|
} |
136
|
|
|
// Don't validate empty values that are not set and not required |
137
|
3 |
|
} elseif (empty($ruleProperties['required']) && empty($ruleProperties['nonEmpty'])) { |
138
|
3 |
|
continue; |
139
|
|
|
} else { |
140
|
|
|
// Empty string as default (not null because that adds complexity to checks) |
141
|
1 |
|
$value = ''; |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
// Validate value against each of the fields' rules |
145
|
3 |
|
$errorMsg = $this->validateValue($value, $ruleProperties); |
146
|
|
|
|
147
|
|
|
// If field has any error messages, add to error array, otherwise add value to validated data |
148
|
3 |
|
if ($errorMsg) { |
149
|
1 |
|
$errorMsgs[$fieldName] = $errorMsg; |
150
|
1 |
|
} else { |
151
|
2 |
|
$this->validatedData[$fieldName] = $value; |
152
|
|
|
} |
153
|
3 |
|
} |
154
|
|
|
|
155
|
3 |
|
return $errorMsgs; |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
/** |
159
|
|
|
* @param mixed $value |
160
|
|
|
* @param array $ruleProperties |
161
|
|
|
* @return string|null |
162
|
|
|
*/ |
163
|
23 |
|
public function validateValue($value, array $ruleProperties) |
164
|
|
|
{ |
165
|
|
|
// Field name |
166
|
23 |
|
if (isset($ruleProperties['textKey'])) { |
167
|
1 |
|
$fieldName = $this->translator->trans($ruleProperties['textKey']); |
168
|
1 |
|
unset($ruleProperties['textKey']); |
169
|
1 |
|
} else { |
170
|
22 |
|
$fieldName = $this->translator->trans('errorTheNoneSpecifiedField'); |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
// Unset rule properties that are not rules |
174
|
23 |
|
$rules = $ruleProperties; |
175
|
23 |
|
unset($rules['textKey']); |
176
|
23 |
|
unset($rules['regexpExpl']); |
177
|
23 |
|
unset($rules['trim']); |
178
|
|
|
|
179
|
23 |
|
foreach ($rules as $rule => $ruleContents) { |
180
|
23 |
|
$errorMsg = null; |
181
|
|
|
|
182
|
|
|
switch ($rule) { |
183
|
23 |
|
case 'required': |
184
|
4 |
|
if (!is_bool($ruleContents)) { |
185
|
1 |
|
throw new \InvalidArgumentException("Invalid required validation rule[{$rule}]"); |
186
|
|
|
} |
187
|
|
|
|
188
|
3 |
|
if ($ruleContents && (((string) $value) === '')) { |
189
|
3 |
|
$errorMsg = $this->translator->trans('errorFieldXIsRequired', ['%field%' => $fieldName]); |
190
|
3 |
|
} |
191
|
|
|
|
192
|
3 |
|
break; |
193
|
20 |
|
case 'nonEmpty': |
194
|
2 |
|
if (!is_bool($ruleContents)) { |
195
|
1 |
|
throw new \InvalidArgumentException("Invalid nonEmpty validation rule[{$rule}]"); |
196
|
|
|
} |
197
|
|
|
|
198
|
1 |
|
if ($ruleContents && empty($value)) { |
199
|
1 |
|
$errorMsg = $this->translator->trans('errorFieldXIsRequired', ['%field%' => $fieldName]); |
200
|
1 |
|
} |
201
|
|
|
|
202
|
1 |
|
break; |
203
|
18 |
View Code Duplication |
case 'min': |
|
|
|
|
204
|
2 |
|
if (!is_numeric($ruleContents)) { |
205
|
1 |
|
throw new \InvalidArgumentException("Invalid min validation rule[{$ruleContents}]"); |
206
|
|
|
} |
207
|
|
|
|
208
|
1 |
|
if ($value < $ruleContents) { |
209
|
|
|
$fieldName = |
210
|
1 |
|
$errorMsg = $this->translator->trans( |
211
|
1 |
|
'errorFieldMustBeMinNumber', |
212
|
1 |
|
['%field%' => $fieldName, '%number%' => $ruleContents] |
213
|
1 |
|
); |
214
|
1 |
|
} |
215
|
|
|
|
216
|
1 |
|
break; |
217
|
17 |
View Code Duplication |
case 'max': |
|
|
|
|
218
|
2 |
|
if (!is_numeric($ruleContents)) { |
219
|
1 |
|
throw new \InvalidArgumentException("Invalid max validation rule[{$ruleContents}]"); |
220
|
|
|
} |
221
|
|
|
|
222
|
1 |
|
if ($value > $ruleContents) { |
223
|
1 |
|
$errorMsg = $this->translator->trans( |
224
|
1 |
|
'errorFieldMustBeMaxNumber', |
225
|
|
|
[ |
226
|
1 |
|
'%field%' => $fieldName, |
227
|
|
|
'%number%' => $ruleContents |
228
|
1 |
|
] |
229
|
1 |
|
); |
230
|
1 |
|
} |
231
|
|
|
|
232
|
1 |
|
break; |
233
|
16 |
View Code Duplication |
case 'minLength': |
|
|
|
|
234
|
4 |
|
if (!is_int($ruleContents) || $ruleContents < 1) { |
235
|
1 |
|
throw new \InvalidArgumentException("Invalid min length validation rule[{$ruleContents}]"); |
236
|
|
|
} |
237
|
|
|
|
238
|
3 |
|
if (mb_strlen($value) < $ruleContents) { |
239
|
1 |
|
$errorMsg = $this->translator->trans( |
240
|
1 |
|
'errorFieldMustBeMinXLength', |
241
|
1 |
|
['%field%' => $fieldName, '%numberOf%' => $ruleContents] |
242
|
1 |
|
); |
243
|
1 |
|
} |
244
|
|
|
|
245
|
3 |
|
break; |
246
|
13 |
View Code Duplication |
case 'maxLength': |
|
|
|
|
247
|
2 |
|
if (!is_int($ruleContents) || $ruleContents < 1) { |
248
|
1 |
|
throw new \InvalidArgumentException("Invalid max length validation rule[{$ruleContents}]"); |
249
|
|
|
} |
250
|
|
|
|
251
|
1 |
|
if ((((string) $value) !== '') && mb_strlen($value) > $ruleContents) { |
252
|
1 |
|
$errorMsg = $this->translator->trans( |
253
|
1 |
|
'errorFieldMustBeMaxXLength', |
254
|
1 |
|
['%field%' => $fieldName, '%numberOf%' => $ruleContents] |
255
|
1 |
|
); |
256
|
1 |
|
} |
257
|
|
|
|
258
|
1 |
|
break; |
259
|
12 |
View Code Duplication |
case 'length': |
|
|
|
|
260
|
2 |
|
if (!is_int($ruleContents) || $ruleContents < 1) { |
261
|
1 |
|
throw new \InvalidArgumentException("Invalid length validation rule[{$ruleContents}]"); |
262
|
|
|
} |
263
|
|
|
|
264
|
1 |
|
if ((((string) $value) !== '') && mb_strlen($value) !== $ruleContents) { |
265
|
1 |
|
$errorMsg = $this->translator->trans( |
266
|
1 |
|
'errorFieldMustBeXLength', |
267
|
1 |
|
['%field%' => $fieldName, '%numberOf%' => $ruleContents] |
268
|
1 |
|
); |
269
|
1 |
|
} |
270
|
|
|
|
271
|
1 |
|
break; |
272
|
10 |
|
case 'regexp': |
273
|
2 |
|
if (!$ruleContents) { |
274
|
1 |
|
throw new \InvalidArgumentException("Invalid regexp validation rule[{$ruleContents}]"); |
275
|
|
|
} |
276
|
|
|
|
277
|
1 |
|
if ((((string) $value) !== '') && !preg_match('/' . $ruleContents . '/', $value)) { |
278
|
1 |
|
$errorMsg = $this->translator->trans( |
279
|
1 |
|
'errorFieldInvalidFormat', |
280
|
1 |
|
['%field%' => $fieldName] |
281
|
1 |
|
); |
282
|
|
|
|
283
|
1 |
|
if (isset($ruleProperties['regexpExpl'])) { |
284
|
1 |
|
$errorMsg .= $this->translator->trans( |
285
|
1 |
|
'errorFieldValidCharactersAreX', |
286
|
1 |
|
['%characters%' => $ruleProperties['regexpExpl']] |
287
|
1 |
|
); |
288
|
1 |
|
} |
289
|
1 |
|
} |
290
|
|
|
|
291
|
1 |
|
break; |
292
|
9 |
|
case 'email': |
293
|
2 |
|
if (!is_bool($ruleContents)) { |
294
|
1 |
|
throw new \InvalidArgumentException("Invalid email validation rule[{$rule}]"); |
295
|
|
|
} |
296
|
|
|
|
297
|
1 |
|
if ($ruleContents && (((string) $value) !== '') && !filter_var($value, FILTER_VALIDATE_EMAIL)) { |
298
|
1 |
|
$errorMsg = $this->translator->trans('errorInvalidEmail'); |
299
|
1 |
|
} |
300
|
|
|
|
301
|
1 |
|
break; |
302
|
8 |
|
case 'date': |
303
|
|
|
// No break |
304
|
8 |
|
case 'dateTime': |
305
|
5 |
|
if (!is_bool($ruleContents)) { |
306
|
1 |
|
throw new \InvalidArgumentException("Invalid date validation rule[{$rule}]"); |
307
|
|
|
} |
308
|
|
|
|
309
|
4 |
|
if ($ruleContents && ((string) $value) !== '') { |
310
|
4 |
|
$isValueOk = function ($format) use ($value) { |
311
|
4 |
|
return (\DateTime::createFromFormat($format, $value) !== false |
312
|
4 |
|
&& !\DateTime::getLastErrors()["warning_count"] |
313
|
4 |
|
&& !\DateTime::getLastErrors()["error_count"]); |
314
|
4 |
|
}; |
315
|
|
|
|
316
|
|
|
// Allow datetime with and without seconds |
317
|
4 |
|
if (($rule === 'date' && !$isValueOk('Y-m-d')) |
318
|
3 |
|
|| ($rule === 'dateTime' && !$isValueOk('Y-m-d H:i') |
319
|
3 |
|
&& !$isValueOk('Y-m-d H:i:s')) |
320
|
4 |
|
) { |
321
|
2 |
|
$errorMsg = $this->translator->trans( |
322
|
2 |
|
($rule === 'date') ? 'errorInvalidDate' : 'errorInvalidDateTime' |
323
|
2 |
|
); |
324
|
2 |
|
} |
325
|
4 |
|
} |
326
|
|
|
|
327
|
4 |
|
break; |
328
|
3 |
|
case 'custom': |
329
|
2 |
|
if (!is_callable($ruleContents)) { |
330
|
1 |
|
throw new \InvalidArgumentException("Invalid custom validation rule[{$rule}]"); |
331
|
|
|
} |
332
|
|
|
|
333
|
1 |
|
$errorMsg = $ruleContents($value); |
334
|
|
|
|
335
|
1 |
|
break; |
336
|
1 |
|
default: |
337
|
1 |
|
throw new \InvalidArgumentException("Unknown validation rule[{$rule}]"); |
338
|
1 |
|
} |
339
|
|
|
|
340
|
11 |
|
if ($errorMsg) { |
341
|
7 |
|
return $errorMsg; |
342
|
|
|
} |
343
|
6 |
|
} |
344
|
|
|
|
345
|
6 |
|
return null; |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
/** |
349
|
|
|
* @param string $key |
350
|
|
|
* @param mixed $default |
351
|
|
|
* @return array |
352
|
|
|
*/ |
353
|
1 |
|
public function getValidatedData($key = null, $default = null) |
354
|
|
|
{ |
355
|
1 |
|
if ($key !== null) { |
356
|
1 |
|
return isset($this->validatedData[$key]) ? $this->validatedData[$key] : $default; |
357
|
|
|
} else { |
358
|
1 |
|
return $this->validatedData; |
359
|
|
|
} |
360
|
|
|
} |
361
|
|
|
|
362
|
|
|
/** |
363
|
|
|
* @return array |
364
|
|
|
*/ |
365
|
1 |
|
public static function getValidRuleProperties() |
366
|
|
|
{ |
367
|
1 |
|
return static::$validRuleProperties; |
368
|
|
|
} |
369
|
|
|
} |
370
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.