1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Spiral Framework. |
4
|
|
|
* |
5
|
|
|
* @license MIT |
6
|
|
|
* @author Anton Titov (Wolfy-J) |
7
|
|
|
*/ |
8
|
|
|
|
9
|
|
|
namespace Spiral\Validation; |
10
|
|
|
|
11
|
|
|
use Interop\Container\ContainerInterface; |
12
|
|
|
use Psr\Log\LoggerAwareInterface; |
13
|
|
|
use Spiral\Core\Component; |
14
|
|
|
use Spiral\Core\Exceptions\ScopeException; |
15
|
|
|
use Spiral\Core\Traits\SaturateTrait; |
16
|
|
|
use Spiral\Debug\Traits\LoggerTrait; |
17
|
|
|
use Spiral\Models\AccessorInterface; |
18
|
|
|
use Spiral\Models\EntityInterface; |
19
|
|
|
use Spiral\Translator\Traits\TranslatorTrait; |
20
|
|
|
use Spiral\Validation\Configs\ValidatorConfig; |
21
|
|
|
use Spiral\Validation\Exceptions\ValidationException; |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* Validator is default implementation of ValidatorInterface. Class support functional rules with |
25
|
|
|
* user parameters. In addition, part of validation rules moved into validation checkers used to |
26
|
|
|
* simplify adding new rules, checkers are resolved using container and can be rebinded in |
27
|
|
|
* application. |
28
|
|
|
* |
29
|
|
|
* Examples: |
30
|
|
|
* "status" => [ |
31
|
|
|
* ["notEmpty"], |
32
|
|
|
* ["string::shorter", 10, "error" => "Your string is too short."], |
33
|
|
|
* [["MyClass", "myMethod"], "error" => "Custom validation failed."] |
34
|
|
|
* ], |
35
|
|
|
* "email" => [ |
36
|
|
|
* ["notEmpty", "error" => "Please enter your email address."], |
37
|
|
|
* ["email", "error" => "Email is not valid."] |
38
|
|
|
* ], |
39
|
|
|
* "pin" => [ |
40
|
|
|
* ["string::regexp", "/[0-9]{5}/", "error" => "Invalid pin format, if you don't know your |
41
|
|
|
* pin, please skip this field."] |
42
|
|
|
* ], |
43
|
|
|
* "flag" => ["notEmpty", "boolean"] |
44
|
|
|
* |
45
|
|
|
* In cases where you don't need custom message or check parameters you can use simplified |
46
|
|
|
* rule syntax: |
47
|
|
|
* "flag" => ["notEmpty", "boolean"] |
48
|
|
|
*/ |
49
|
|
|
class Validator extends Component implements ValidatorInterface, LoggerAwareInterface |
50
|
|
|
{ |
51
|
|
|
use LoggerTrait, TranslatorTrait, SaturateTrait; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* Return from validation rule to stop any future field validations. Internal contract. |
55
|
|
|
*/ |
56
|
|
|
const STOP_VALIDATION = -99; |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* @invisible |
60
|
|
|
* @var ValidatorConfig |
61
|
|
|
*/ |
62
|
|
|
private $config = null; |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* @var array|\ArrayAccess |
66
|
|
|
*/ |
67
|
|
|
private $data = []; |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* Validation rules, see class title for description. |
71
|
|
|
* |
72
|
|
|
* @var array |
73
|
|
|
*/ |
74
|
|
|
private $rules = []; |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* Error messages raised while validation. |
78
|
|
|
* |
79
|
|
|
* @var array |
80
|
|
|
*/ |
81
|
|
|
private $errors = []; |
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* Errors provided from outside. |
85
|
|
|
* |
86
|
|
|
* @var array |
87
|
|
|
*/ |
88
|
|
|
private $registeredErrors = []; |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* If rule has no definer error message this text will be used instead. Localizable. |
92
|
|
|
* |
93
|
|
|
* @invisible |
94
|
|
|
* @var string |
95
|
|
|
*/ |
96
|
|
|
protected $defaultMessage = "[[Condition '{condition}' does not meet.]]"; |
97
|
|
|
|
98
|
|
|
/** |
99
|
|
|
* @invisible |
100
|
|
|
* @var ContainerInterface |
101
|
|
|
*/ |
102
|
|
|
protected $container = null; |
103
|
|
|
|
104
|
|
|
/** |
105
|
|
|
* {@inheritdoc} |
106
|
|
|
* |
107
|
|
|
* @param array $rules Validation rules. |
108
|
|
|
* @param array|\ArrayAccess $data Data or model to be validated. |
109
|
|
|
* @param ValidatorConfig $config Saturated using shared container |
110
|
|
|
* @param ContainerInterface $container Saturated using shared container |
111
|
|
|
* |
112
|
|
|
* @throws ScopeException |
113
|
|
|
*/ |
114
|
|
|
public function __construct( |
115
|
|
|
array $rules = [], |
116
|
|
|
$data = [], |
117
|
|
|
ValidatorConfig $config = null, |
118
|
|
|
ContainerInterface $container = null |
119
|
|
|
) { |
120
|
|
|
$this->data = $data; |
121
|
|
|
$this->rules = $rules; |
122
|
|
|
|
123
|
|
|
$this->config = $this->saturate($config, ValidatorConfig::class); |
124
|
|
|
$this->container = $this->saturate($container, ContainerInterface::class); |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
/** |
128
|
|
|
* {@inheritdoc} |
129
|
|
|
*/ |
130
|
|
View Code Duplication |
public function setRules(array $rules): ValidatorInterface |
|
|
|
|
131
|
|
|
{ |
132
|
|
|
if ($this->rules == $rules) { |
133
|
|
|
return $this; |
134
|
|
|
} |
135
|
|
|
|
136
|
|
|
$this->rules = $rules; |
137
|
|
|
$this->reset(); |
138
|
|
|
|
139
|
|
|
return $this; |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
/** |
143
|
|
|
* {@inheritdoc} |
144
|
|
|
*/ |
145
|
|
View Code Duplication |
public function setData($data): ValidatorInterface |
|
|
|
|
146
|
|
|
{ |
147
|
|
|
if ($this->data == $data) { |
148
|
|
|
return $this; |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
$this->data = $data; |
152
|
|
|
$this->reset(); |
153
|
|
|
|
154
|
|
|
return $this; |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
/** |
158
|
|
|
* {@inheritdoc} |
159
|
|
|
*/ |
160
|
|
|
public function getData() |
161
|
|
|
{ |
162
|
|
|
return $this->data; |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
/** |
166
|
|
|
* {@inheritdoc} |
167
|
|
|
*/ |
168
|
|
|
public function isValid(): bool |
169
|
|
|
{ |
170
|
|
|
$this->validate(); |
171
|
|
|
|
172
|
|
|
return empty($this->errors) && empty($this->registeredErrors); |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
/** |
176
|
|
|
* {@inheritdoc} |
177
|
|
|
*/ |
178
|
|
|
public function registerError(string $field, string $error): ValidatorInterface |
179
|
|
|
{ |
180
|
|
|
$this->registeredErrors[$field] = $error; |
181
|
|
|
|
182
|
|
|
return $this; |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
/** |
186
|
|
|
* {@inheritdoc} |
187
|
|
|
*/ |
188
|
|
|
public function flushRegistered(): ValidatorInterface |
189
|
|
|
{ |
190
|
|
|
$this->registeredErrors = []; |
191
|
|
|
|
192
|
|
|
return $this; |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
/** |
196
|
|
|
* {@inheritdoc} |
197
|
|
|
*/ |
198
|
|
|
public function hasErrors(): bool |
199
|
|
|
{ |
200
|
|
|
return !$this->isValid(); |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
/** |
204
|
|
|
* {@inheritdoc} |
205
|
|
|
*/ |
206
|
|
|
public function getErrors(): array |
207
|
|
|
{ |
208
|
|
|
$this->validate(); |
209
|
|
|
|
210
|
|
|
return $this->registeredErrors + $this->errors; |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
/** |
214
|
|
|
* Receive field from context data or return default value. |
215
|
|
|
* |
216
|
|
|
* @param string $field |
217
|
|
|
* @param mixed $default |
218
|
|
|
* |
219
|
|
|
* @return mixed |
220
|
|
|
*/ |
221
|
|
|
public function getValue(string $field, $default = null) |
222
|
|
|
{ |
223
|
|
|
$value = isset($this->data[$field]) ? $this->data[$field] : $default; |
224
|
|
|
|
225
|
|
|
return ($value instanceof EntityInterface || $value instanceof AccessorInterface) |
226
|
|
|
? $value->packValue() |
|
|
|
|
227
|
|
|
: $value; |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
/** |
231
|
|
|
* Reset validation state. |
232
|
|
|
*/ |
233
|
|
|
public function reset() |
234
|
|
|
{ |
235
|
|
|
$this->errors = []; |
236
|
|
|
$this->registeredErrors = []; |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
/** |
240
|
|
|
* Validate context data with set of validation rules. |
241
|
|
|
*/ |
242
|
|
|
protected function validate() |
243
|
|
|
{ |
244
|
|
|
$this->errors = []; |
245
|
|
|
foreach ($this->rules as $field => $rules) { |
246
|
|
|
|
247
|
|
|
foreach ($rules as $rule) { |
248
|
|
|
if (isset($this->errors[$field])) { |
249
|
|
|
//We are validating field till first error |
250
|
|
|
continue; |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
//Condition is either rule itself or first array element |
254
|
|
|
$condition = is_string($rule) ? $rule : $rule[0]; |
255
|
|
|
$arguments = is_string($rule) ? [] : $this->fetchArguments($rule); |
256
|
|
|
|
257
|
|
|
if (empty($this->getValue($field)) && !$this->config->emptyCondition($condition)) { |
258
|
|
|
//There is no need to validate empty field except for special conditions |
259
|
|
|
break; |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
$result = $this->check($field, $this->getValue($field), $condition, $arguments); |
263
|
|
|
|
264
|
|
|
if ($result === true) { |
265
|
|
|
//No errors |
266
|
|
|
continue; |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
if ($result === self::STOP_VALIDATION) { |
270
|
|
|
//Validation has to be stopped per rule request |
271
|
|
|
break; |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
if ($result instanceof CheckerInterface) { |
275
|
|
|
//Failed inside checker, this is implementation agreement |
276
|
|
|
if ($message = $result->getMessage($condition[1])) { |
277
|
|
|
|
278
|
|
|
//Checker provides it's own message for condition |
279
|
|
|
$this->addMessage( |
280
|
|
|
$field, |
281
|
|
|
is_string($rule) ? $message : $this->fetchMessage($rule, $message), |
282
|
|
|
$condition, |
283
|
|
|
$arguments |
284
|
|
|
); |
285
|
|
|
|
286
|
|
|
continue; |
287
|
|
|
} |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
//Default message |
291
|
|
|
$message = $this->say($this->defaultMessage); |
292
|
|
|
|
293
|
|
|
//Recording error message |
294
|
|
|
$this->addMessage( |
295
|
|
|
$field, |
296
|
|
|
is_string($rule) ? $message : $this->fetchMessage($rule, $message), |
297
|
|
|
$condition, |
298
|
|
|
$arguments |
299
|
|
|
); |
300
|
|
|
} |
301
|
|
|
} |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
/** |
305
|
|
|
* Check field with given condition. Can return instance of Checker (data is not valid) to |
306
|
|
|
* clarify error. |
307
|
|
|
* |
308
|
|
|
* @param string $field |
309
|
|
|
* @param mixed $value |
310
|
|
|
* @param mixed $condition Reference, can be altered if alias exists. |
311
|
|
|
* @param array $arguments Rule arguments if any. |
312
|
|
|
* |
313
|
|
|
* @return bool|CheckerInterface |
314
|
|
|
* @throws ValidationException |
315
|
|
|
*/ |
316
|
|
|
protected function check(string $field, $value, &$condition, array $arguments = []) |
317
|
|
|
{ |
318
|
|
|
//Supports both class::func and class:func |
|
|
|
|
319
|
|
|
$condition = str_replace('::', ':', $this->config->resolveAlias($condition)); |
320
|
|
|
|
321
|
|
|
try { |
322
|
|
|
if (!is_array($condition) && strpos($condition, ':')) { |
323
|
|
|
$condition = explode(':', $condition); |
324
|
|
|
if ($this->config->hasChecker($condition[0])) { |
325
|
|
|
|
326
|
|
|
$checker = $this->makeChecker($condition[0]); |
327
|
|
|
if (!$result = $checker->check($condition[1], $value, $arguments)) { |
328
|
|
|
//To let validation() method know that message should be handled via Checker |
329
|
|
|
return $checker; |
330
|
|
|
} |
331
|
|
|
|
332
|
|
|
return $result; |
333
|
|
|
} |
334
|
|
|
} |
335
|
|
|
|
336
|
|
|
if (is_array($condition)) { |
337
|
|
|
//We are going to resolve class using constructor |
338
|
|
|
$condition[0] = is_object($condition[0]) |
339
|
|
|
? $condition[0] |
340
|
|
|
: $this->container->get($condition[0]); |
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
//Value always coming first |
344
|
|
|
array_unshift($arguments, $value); |
345
|
|
|
|
346
|
|
|
return call_user_func_array($condition, $arguments); |
347
|
|
|
} catch (\Error $e) { |
348
|
|
|
throw new ValidationException("Invalid rule definition", $e->getCode(), $e); |
349
|
|
|
} catch (\Throwable $e) { |
350
|
|
|
$this->logException($field, func_get_arg(2), $e); |
351
|
|
|
|
352
|
|
|
return false; |
353
|
|
|
} |
354
|
|
|
} |
355
|
|
|
|
356
|
|
|
/** |
357
|
|
|
* Get or create instance of validation checker. |
358
|
|
|
* |
359
|
|
|
* @param string $name |
360
|
|
|
* |
361
|
|
|
* @return CheckerInterface |
362
|
|
|
* @throws ValidationException |
363
|
|
|
*/ |
364
|
|
|
protected function makeChecker(string $name): CheckerInterface |
365
|
|
|
{ |
366
|
|
|
if (!$this->config->hasChecker($name)) { |
367
|
|
|
throw new ValidationException( |
368
|
|
|
"Unable to create validation checker defined by '{$name}' name." |
369
|
|
|
); |
370
|
|
|
} |
371
|
|
|
|
372
|
|
|
return $this->container->get($this->config->checkerClass($name))->withValidator($this); |
373
|
|
|
} |
374
|
|
|
|
375
|
|
|
/** |
376
|
|
|
* Fetch validation rule arguments from rule definition. |
377
|
|
|
* |
378
|
|
|
* @param array $rule |
379
|
|
|
* |
380
|
|
|
* @return array |
381
|
|
|
*/ |
382
|
|
|
private function fetchArguments(array $rule): array |
383
|
|
|
{ |
384
|
|
|
unset($rule[0], $rule['message'], $rule['error']); |
385
|
|
|
|
386
|
|
|
return array_values($rule); |
387
|
|
|
} |
388
|
|
|
|
389
|
|
|
/** |
390
|
|
|
* Fetch error message from rule definition or use default message. Method will check "message" |
391
|
|
|
* and "error" properties of definition. |
392
|
|
|
* |
393
|
|
|
* @param array $rule |
394
|
|
|
* @param string $message Default message to use. |
395
|
|
|
* |
396
|
|
|
* @return string |
397
|
|
|
*/ |
398
|
|
|
private function fetchMessage(array $rule, string $message): string |
399
|
|
|
{ |
400
|
|
|
if (isset($rule['message'])) { |
401
|
|
|
return $rule['message']; |
402
|
|
|
} |
403
|
|
|
|
404
|
|
|
if (isset($rule['error'])) { |
405
|
|
|
return $rule['error']; |
406
|
|
|
} |
407
|
|
|
|
408
|
|
|
return $message; |
409
|
|
|
} |
410
|
|
|
|
411
|
|
|
/** |
412
|
|
|
* Register error message for specified field. Rule definition will be interpolated into |
413
|
|
|
* message. |
414
|
|
|
* |
415
|
|
|
* @param string $field |
416
|
|
|
* @param string $message |
417
|
|
|
* @param mixed $condition |
418
|
|
|
* @param array $arguments |
419
|
|
|
*/ |
420
|
|
|
private function addMessage(string $field, string $message, $condition, array $arguments = []) |
421
|
|
|
{ |
422
|
|
View Code Duplication |
if (is_array($condition)) { |
|
|
|
|
423
|
|
|
if (is_object($condition[0])) { |
424
|
|
|
$condition[0] = get_class($condition[0]); |
425
|
|
|
} |
426
|
|
|
|
427
|
|
|
$condition = join('::', $condition); |
428
|
|
|
} |
429
|
|
|
|
430
|
|
|
$this->errors[$field] = \Spiral\interpolate( |
431
|
|
|
$message, |
432
|
|
|
compact('field', 'condition') + $arguments |
433
|
|
|
); |
434
|
|
|
} |
435
|
|
|
|
436
|
|
|
/** |
437
|
|
|
* @param string $field |
438
|
|
|
* @param array $condition |
439
|
|
|
* @param \Throwable $e |
440
|
|
|
*/ |
441
|
|
|
protected function logException(string $field, $condition, \Throwable $e) |
442
|
|
|
{ |
443
|
|
View Code Duplication |
if (is_array($condition)) { |
|
|
|
|
444
|
|
|
if (is_object($condition[0])) { |
445
|
|
|
$condition[0] = get_class($condition[0]); |
446
|
|
|
} |
447
|
|
|
|
448
|
|
|
$condition = join('::', $condition); |
449
|
|
|
} |
450
|
|
|
|
451
|
|
|
$this->logger()->error( |
452
|
|
|
"Condition '{condition}' failed with '{e}' while checking '{field}' field.", |
453
|
|
|
compact('condition', 'field') + ['e' => $e->getMessage()] |
454
|
|
|
); |
455
|
|
|
} |
456
|
|
|
} |
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.