Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
214 / 214 |
|
100.00% |
35 / 35 |
CRAP | |
100.00% |
1 / 1 |
Validation | |
100.00% |
214 / 214 |
|
100.00% |
35 / 35 |
85 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
setLanguage | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
getLanguage | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getValidators | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
reset | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
setLabel | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getLabel | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getLabels | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setLabels | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
getRuleset | |
100.00% |
25 / 25 |
|
100.00% |
1 / 1 |
4 | |||
escapeArgs | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
parseRule | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
extractRules | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
getFilledMessage | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
getRules | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setRule | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
setRules | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
getError | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
getErrors | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
setError | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
setMessage | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getMessage | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setMessages | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
getMessages | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
validateRule | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
validateField | |
100.00% |
30 / 30 |
|
100.00% |
1 / 1 |
16 | |||
replaceArgs | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
4 | |||
setEqualsField | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
run | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
validate | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
2 | |||
validateOnly | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
2 | |||
validateOnlySet | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
isRuleAvailable | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
4 | |||
setDebugCollector | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getDebugCollector | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php declare(strict_types=1); |
2 | /* |
3 | * This file is part of Aplus Framework Validation Library. |
4 | * |
5 | * (c) Natan Felles <natanfelles@gmail.com> |
6 | * |
7 | * For the full copyright and license information, please view the LICENSE |
8 | * file that was distributed with this source code. |
9 | */ |
10 | namespace Framework\Validation; |
11 | |
12 | use Framework\Helpers\ArraySimple; |
13 | use Framework\Language\Language; |
14 | use Framework\Validation\Debug\ValidationCollector; |
15 | use InvalidArgumentException; |
16 | use JetBrains\PhpStorm\Pure; |
17 | |
18 | /** |
19 | * Class Validation. |
20 | * |
21 | * @package validation |
22 | */ |
23 | class Validation |
24 | { |
25 | /** |
26 | * The labels used to replace field names. |
27 | * |
28 | * @var array<string,string> The field names as keys and the labels as values |
29 | */ |
30 | protected array $labels = []; |
31 | /** |
32 | * The Validator rules. |
33 | * |
34 | * @var array<string,array<int,array<string,array<int,string>|string>>> The |
35 | * field names as keys and the rules and arguments as values |
36 | */ |
37 | protected array $rules = []; |
38 | /** |
39 | * The last errors. |
40 | * |
41 | * @var array<string,array<string,array<string,array<int,string>|string>>> The |
42 | * field names as keys and the rule and arguments as values |
43 | */ |
44 | protected array $errors = []; |
45 | /** |
46 | * Custom error messages. |
47 | * |
48 | * @var array<string,array<string,string>> The field name as keys and an |
49 | * associative array of rule names as keys and messages as values |
50 | */ |
51 | protected array $messages = []; |
52 | /** |
53 | * The current Validators. |
54 | * |
55 | * @var array<int,string|Validator> Values are the Validators FQCN or |
56 | * instances |
57 | */ |
58 | protected array $validators = []; |
59 | /** |
60 | * The Language instance. |
61 | * |
62 | * @var Language |
63 | */ |
64 | protected Language $language; |
65 | protected ValidationCollector $debugCollector; |
66 | |
67 | /** |
68 | * Validation constructor. |
69 | * |
70 | * @param array<int,string|Validator>|null $validators |
71 | * @param Language|null $language |
72 | */ |
73 | public function __construct(array $validators = null, Language $language = null) |
74 | { |
75 | $defaultValidators = [ |
76 | Validator::class, |
77 | FilesValidator::class, |
78 | ]; |
79 | $this->validators = empty($validators) |
80 | ? $defaultValidators |
81 | : \array_reverse($validators); |
82 | if ($language) { |
83 | $this->setLanguage($language); |
84 | } |
85 | } |
86 | |
87 | public function setLanguage(Language $language = null) : static |
88 | { |
89 | if ($language === null) { |
90 | $language = new Language(); |
91 | } |
92 | $language->addDirectory(__DIR__ . '/Languages'); |
93 | $this->language = $language; |
94 | return $this; |
95 | } |
96 | |
97 | public function getLanguage() : Language |
98 | { |
99 | if ( ! isset($this->language)) { |
100 | $this->setLanguage(); |
101 | } |
102 | return $this->language; |
103 | } |
104 | |
105 | /** |
106 | * @return array<int,string|Validator> |
107 | */ |
108 | public function getValidators() : array |
109 | { |
110 | return $this->validators; |
111 | } |
112 | |
113 | /** |
114 | * Reset the validation. |
115 | * |
116 | * @return static |
117 | */ |
118 | public function reset() : static |
119 | { |
120 | $this->labels = []; |
121 | $this->rules = []; |
122 | $this->errors = []; |
123 | $this->messages = []; |
124 | return $this; |
125 | } |
126 | |
127 | /** |
128 | * Set label for a field. |
129 | * |
130 | * @param string $field |
131 | * @param string $label |
132 | * |
133 | * @return static |
134 | */ |
135 | public function setLabel(string $field, string $label) : static |
136 | { |
137 | $this->labels[$field] = $label; |
138 | return $this; |
139 | } |
140 | |
141 | /** |
142 | * Get the label for a given field. |
143 | * |
144 | * @param string $field |
145 | * |
146 | * @return string|null |
147 | */ |
148 | #[Pure] |
149 | public function getLabel(string $field) : ?string |
150 | { |
151 | return $this->labels[$field] ?? null; |
152 | } |
153 | |
154 | /** |
155 | * Get a list of all labels. |
156 | * |
157 | * @return array<string,string> |
158 | */ |
159 | #[Pure] |
160 | public function getLabels() : array |
161 | { |
162 | return $this->labels; |
163 | } |
164 | |
165 | /** |
166 | * Set fields labels. |
167 | * |
168 | * @param array<string,string> $labels An associative array with fields as |
169 | * keys and label as values |
170 | * |
171 | * @return static |
172 | */ |
173 | public function setLabels(array $labels) : static |
174 | { |
175 | $this->labels = []; |
176 | foreach ($labels as $field => $label) { |
177 | $this->setLabel($field, $label); |
178 | } |
179 | return $this; |
180 | } |
181 | |
182 | /** |
183 | * @return array<mixed> |
184 | */ |
185 | public function getRuleset() : array |
186 | { |
187 | $result = []; |
188 | foreach ($this->getRules() as $field => $rules) { |
189 | $label = $this->getLabel($field); |
190 | $tmp = [ |
191 | 'field' => $field, |
192 | 'label' => $label, |
193 | 'rules' => [], |
194 | ]; |
195 | foreach ($rules as $rule) { |
196 | $rule['args'] = $this->escapeArgs($rule['args']); // @phpstan-ignore-line |
197 | $args = \implode(',', $rule['args']); |
198 | $ruleString = $rule['rule'] . ($args === '' ? '' : ':' . $args); // @phpstan-ignore-line |
199 | $tmp['rules'][] = [ |
200 | 'rule' => $ruleString, |
201 | 'message' => $this->getFilledMessage( |
202 | $field, |
203 | $rule['rule'], // @phpstan-ignore-line |
204 | \array_merge( |
205 | ['field' => $label ?? $field], |
206 | $rule['args'] |
207 | ) |
208 | ), |
209 | ]; |
210 | } |
211 | $result[] = $tmp; |
212 | } |
213 | return $result; |
214 | } |
215 | |
216 | /** |
217 | * @param array<string> $args |
218 | * |
219 | * @return array<string> |
220 | */ |
221 | protected function escapeArgs(array $args) : array |
222 | { |
223 | foreach ($args as &$arg) { |
224 | $arg = \strtr($arg, [',' => '\,']); |
225 | } |
226 | return $args; |
227 | } |
228 | |
229 | /** |
230 | * @param string $rule |
231 | * |
232 | * @return array<string,array<int,string>|string> |
233 | */ |
234 | protected function parseRule(string $rule) : array |
235 | { |
236 | $args = []; |
237 | if (\str_contains($rule, ':')) { |
238 | [$rule, $args] = \explode(':', $rule, 2); |
239 | $args = (array) \preg_split('#(?<!\\\)\,#', $args); |
240 | foreach ($args as &$arg) { |
241 | $arg = \strtr((string) $arg, ['\,' => ',']); |
242 | } |
243 | } |
244 | return ['rule' => $rule, 'args' => $args]; // @phpstan-ignore-line |
245 | } |
246 | |
247 | /** |
248 | * @param string $rules |
249 | * |
250 | * @return array<int,array<string,array<int,string>|string>> |
251 | */ |
252 | #[Pure] |
253 | protected function extractRules(string $rules) : array |
254 | { |
255 | $result = []; |
256 | $rules = (array) \preg_split('#(?<!\\\)\|#', $rules); |
257 | foreach ($rules as $rule) { |
258 | $rule = \strtr((string) $rule, ['\|' => '|']); |
259 | $result[] = $this->parseRule($rule); |
260 | } |
261 | return $result; |
262 | } |
263 | |
264 | /** |
265 | * @param string $field |
266 | * @param string $rule |
267 | * @param array<mixed> $args |
268 | * |
269 | * @return string |
270 | */ |
271 | public function getFilledMessage(string $field, string $rule, array $args = []) : string |
272 | { |
273 | $message = $this->getMessage($field, $rule); |
274 | if ($message === null) { |
275 | return $this->getLanguage()->render('validation', $rule, $args); |
276 | } |
277 | return $this->getLanguage()->formatMessage($message, $args); |
278 | } |
279 | |
280 | /** |
281 | * Get a list of current rules. |
282 | * |
283 | * @return array<string,array<int,array<string,array<int,string>|string>>> |
284 | */ |
285 | #[Pure] |
286 | public function getRules() : array |
287 | { |
288 | return $this->rules; |
289 | } |
290 | |
291 | /** |
292 | * Set rules for a given field. |
293 | * |
294 | * @param string $field |
295 | * @param array<string>|string $rules |
296 | * |
297 | * @return static |
298 | */ |
299 | public function setRule(string $field, array | string $rules) : static |
300 | { |
301 | if (\is_array($rules)) { |
302 | foreach ($rules as &$rule) { |
303 | $rule = $this->parseRule($rule); |
304 | } |
305 | unset($rule); |
306 | $this->rules[$field] = $rules; // @phpstan-ignore-line |
307 | return $this; |
308 | } |
309 | $this->rules[$field] = $this->extractRules($rules); |
310 | return $this; |
311 | } |
312 | |
313 | /** |
314 | * Set field rules. |
315 | * |
316 | * @param array<string,array<string>|string> $rules An associative array |
317 | * with field as keys and values as rules |
318 | * |
319 | * @return static |
320 | */ |
321 | public function setRules(array $rules) : static |
322 | { |
323 | $this->rules = []; |
324 | foreach ($rules as $field => $rule) { |
325 | $this->setRule($field, $rule); |
326 | } |
327 | return $this; |
328 | } |
329 | |
330 | /** |
331 | * Get latest error for a given field. |
332 | * |
333 | * @param string $field |
334 | * |
335 | * @return string|null |
336 | */ |
337 | public function getError(string $field) : ?string |
338 | { |
339 | $error = $this->errors[$field] ?? null; |
340 | if ($error === null) { |
341 | return null; |
342 | } |
343 | $error['args']['args'] = $error['args'] ? \implode(', ', $error['args']) : ''; // @phpstan-ignore-line |
344 | $error['args']['field'] = $this->getLabel($field) ?? $field; |
345 | return $this->getFilledMessage($field, $error['rule'], $error['args']); // @phpstan-ignore-line |
346 | } |
347 | |
348 | /** |
349 | * Get latest errors. |
350 | * |
351 | * @return array<string,string> |
352 | */ |
353 | public function getErrors() : array |
354 | { |
355 | $messages = []; |
356 | foreach (\array_keys($this->errors) as $field) { |
357 | $messages[$field] = $this->getError($field); |
358 | } |
359 | return $messages; |
360 | } |
361 | |
362 | /** |
363 | * @param string $field |
364 | * @param string $rule |
365 | * @param array<int,string> $args |
366 | * |
367 | * @return static |
368 | */ |
369 | public function setError(string $field, string $rule, array $args = []) : static |
370 | { |
371 | // @phpstan-ignore-next-line |
372 | $this->errors[$field] = [ |
373 | 'rule' => $rule, |
374 | 'args' => $args, |
375 | ]; |
376 | return $this; |
377 | } |
378 | |
379 | /** |
380 | * Set a custom error message for a field rule. |
381 | * |
382 | * @param string $field The field name |
383 | * @param string $rule The field rule name |
384 | * @param string $message The custom error message for the field rule |
385 | * |
386 | * @return static |
387 | */ |
388 | public function setMessage(string $field, string $rule, string $message) : static |
389 | { |
390 | $this->messages[$field][$rule] = $message; |
391 | return $this; |
392 | } |
393 | |
394 | /** |
395 | * Get the custom error message from a field rule. |
396 | * |
397 | * @param string $field The field name |
398 | * @param string $rule The rule name |
399 | * |
400 | * @return string|null The message string or null if the message is not set |
401 | */ |
402 | public function getMessage(string $field, string $rule) : ?string |
403 | { |
404 | return $this->messages[$field][$rule] ?? null; |
405 | } |
406 | |
407 | /** |
408 | * Set many custom error messages. |
409 | * |
410 | * @param array<string,array<string,string>> $messages A multi-dimensional |
411 | * array with field names as keys and values as arrays where the keys are |
412 | * rule names and values are the custom error message strings |
413 | * |
414 | * @return static |
415 | */ |
416 | public function setMessages(array $messages) : static |
417 | { |
418 | $this->messages = []; |
419 | foreach ($messages as $field => $rules) { |
420 | foreach ($rules as $rule => $message) { |
421 | $this->setMessage($field, $rule, $message); |
422 | } |
423 | } |
424 | return $this; |
425 | } |
426 | |
427 | /** |
428 | * Get all custom error messages set. |
429 | * |
430 | * @return array<string,array<string,string>> |
431 | */ |
432 | public function getMessages() : array |
433 | { |
434 | return $this->messages; |
435 | } |
436 | |
437 | /** |
438 | * @param string $rule |
439 | * @param string $field |
440 | * @param array<int|string,mixed> $args |
441 | * @param array<string,mixed> $data |
442 | * |
443 | * @return bool |
444 | */ |
445 | protected function validateRule(string $rule, string $field, array $args, array $data) : bool |
446 | { |
447 | foreach ($this->validators as $validator) { |
448 | if (\is_callable([$validator, $rule])) { |
449 | return $validator::$rule($field, $data, ...$args); |
450 | } |
451 | } |
452 | throw new InvalidArgumentException( |
453 | "Validation rule '{$rule}' not found on field '{$field}'" |
454 | ); |
455 | } |
456 | |
457 | /** |
458 | * @param string $field |
459 | * @param array<string,array<mixed>> $rules |
460 | * @param array<string,mixed> $data |
461 | * |
462 | * @since 2.2 Added "blank", "null" and "empty" rules |
463 | * |
464 | * @return bool |
465 | */ |
466 | protected function validateField(string $field, array $rules, array $data) : bool |
467 | { |
468 | $removeKeys = []; |
469 | foreach ($rules as $key => $rule) { |
470 | $fieldExists = \array_key_exists($field, $data); |
471 | // Field is optional. If the field is undefined, validation passes. |
472 | if ($rule['rule'] === 'optional') { |
473 | $removeKeys[] = $key; |
474 | if ( ! $fieldExists) { |
475 | return true; |
476 | } |
477 | } |
478 | // Field must be defined and can have a blank string. |
479 | if ($rule['rule'] === 'blank') { |
480 | $removeKeys[] = $key; |
481 | if ($fieldExists && $data[$field] === '') { |
482 | return true; |
483 | } |
484 | } |
485 | // Field must be defined and can have a null value. |
486 | if ($rule['rule'] === 'null') { |
487 | $removeKeys[] = $key; |
488 | if ($fieldExists && $data[$field] === null) { |
489 | return true; |
490 | } |
491 | } |
492 | // Field must be defined and can have an empty value |
493 | if ($rule['rule'] === 'empty') { |
494 | $removeKeys[] = $key; |
495 | if ($fieldExists && empty($data[$field])) { |
496 | return true; |
497 | } |
498 | } |
499 | } |
500 | foreach ($removeKeys as $removeKey) { |
501 | unset($rules[$removeKey]); |
502 | } |
503 | $status = true; |
504 | foreach ($rules as $rule) { |
505 | $rule['args'] = $this->replaceArgs($rule['args'], $data); |
506 | $status = $this->validateRule($rule['rule'], $field, $rule['args'], $data); |
507 | if ($status !== true) { |
508 | $rule = $this->setEqualsField($rule); |
509 | $this->setError($field, $rule['rule'], $rule['args']); |
510 | break; |
511 | } |
512 | } |
513 | return $status; |
514 | } |
515 | |
516 | /** |
517 | * Replace argument placeholders with data values. |
518 | * |
519 | * @param array<mixed> $args |
520 | * @param array<string,mixed> $data |
521 | * |
522 | * @return array<mixed> |
523 | */ |
524 | protected function replaceArgs(array $args, array $data) : array |
525 | { |
526 | $result = []; |
527 | foreach ($args as $arg) { |
528 | if (\preg_match('#^{(\w+)}$#', $arg)) { |
529 | $key = \substr($arg, 1, -1); |
530 | if (isset($data[$key])) { |
531 | $result[] = $data[$key]; |
532 | continue; |
533 | } |
534 | } |
535 | $result[] = $arg; |
536 | } |
537 | return $result; |
538 | } |
539 | |
540 | /** |
541 | * @param array<string,mixed> $rule |
542 | * |
543 | * @return array<string,mixed> |
544 | */ |
545 | #[Pure] |
546 | protected function setEqualsField(array $rule) : array |
547 | { |
548 | if ($rule['rule'] === 'equals' || $rule['rule'] === 'notEquals') { |
549 | $rule['args'][0] = $this->getLabel($rule['args'][0]) ?? $rule['args'][0]; |
550 | } |
551 | return $rule; |
552 | } |
553 | |
554 | /** |
555 | * @param array<string,array<mixed>> $fieldRules |
556 | * @param array<string,mixed> $data |
557 | * |
558 | * @return bool |
559 | */ |
560 | protected function run(array $fieldRules, array $data) : bool |
561 | { |
562 | $this->errors = []; |
563 | $result = true; |
564 | foreach ($fieldRules as $field => $rules) { |
565 | $status = $this->validateField($field, $rules, $data); |
566 | if ( ! $status) { |
567 | $result = false; |
568 | } |
569 | } |
570 | return $result; |
571 | } |
572 | |
573 | /** |
574 | * Validate data with all rules. |
575 | * |
576 | * @param array<string,mixed> $data |
577 | * |
578 | * @return bool |
579 | */ |
580 | public function validate(array $data) : bool |
581 | { |
582 | if (isset($this->debugCollector)) { |
583 | $start = \microtime(true); |
584 | $validated = $this->run($this->getRules(), $data); |
585 | $end = \microtime(true); |
586 | $this->debugCollector->addData([ |
587 | 'start' => $start, |
588 | 'end' => $end, |
589 | 'validated' => $validated, |
590 | 'errors' => $this->getErrors(), |
591 | 'type' => 'all', |
592 | ]); |
593 | return $validated; |
594 | } |
595 | return $this->run($this->getRules(), $data); |
596 | } |
597 | |
598 | /** |
599 | * Validate only fields set on data. |
600 | * |
601 | * @param array<string,mixed> $data |
602 | * |
603 | * @return bool |
604 | */ |
605 | public function validateOnly(array $data) : bool |
606 | { |
607 | if (isset($this->debugCollector)) { |
608 | $start = \microtime(true); |
609 | $validated = $this->validateOnlySet($data); |
610 | $end = \microtime(true); |
611 | $this->debugCollector->addData([ |
612 | 'start' => $start, |
613 | 'end' => $end, |
614 | 'validated' => $validated, |
615 | 'errors' => $this->getErrors(), |
616 | 'type' => 'only', |
617 | ]); |
618 | return $validated; |
619 | } |
620 | return $this->validateOnlySet($data); |
621 | } |
622 | |
623 | /** |
624 | * @param array<string,mixed> $data |
625 | * |
626 | * @return bool |
627 | */ |
628 | protected function validateOnlySet(array $data) : bool |
629 | { |
630 | $fieldRules = \array_intersect_key( |
631 | $this->getRules(), |
632 | ArraySimple::convert($data) |
633 | ); |
634 | return $this->run($fieldRules, $data); |
635 | } |
636 | |
637 | /** |
638 | * Tells if a rule is available in the current validators. |
639 | * |
640 | * @param string $rule |
641 | * |
642 | * @return bool |
643 | */ |
644 | public function isRuleAvailable(string $rule) : bool |
645 | { |
646 | if (\in_array($rule, [ |
647 | 'blank', |
648 | 'empty', |
649 | 'null', |
650 | 'optional', |
651 | ])) { |
652 | return true; |
653 | } |
654 | foreach ($this->validators as $validator) { |
655 | if (\is_callable([$validator, $rule])) { |
656 | return true; |
657 | } |
658 | } |
659 | return false; |
660 | } |
661 | |
662 | public function setDebugCollector(ValidationCollector $debugCollector) : static |
663 | { |
664 | $this->debugCollector = $debugCollector; |
665 | $this->debugCollector->setValidation($this); |
666 | return $this; |
667 | } |
668 | |
669 | public function getDebugCollector() : ValidationCollector | null |
670 | { |
671 | return $this->debugCollector ?? null; |
672 | } |
673 | } |