Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
194 / 194
100.00% covered (success)
100.00%
36 / 36
CRAP
100.00% covered (success)
100.00%
1 / 1
ValidatorSet
100.00% covered (success)
100.00%
194 / 194
100.00% covered (success)
100.00%
36 / 36
113
100.00% covered (success)
100.00%
1 / 1
 getAvailableValidators
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
3
 add
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 load
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 createFromRules
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 addValidatorFromRule
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 addValidatorsFromRules
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addValidator
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 addValidatorsToField
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
5
 addValidators
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
7
 loadValidator
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
6
 loadValidatorsToField
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 loadValidators
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 getValidators
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 hasValidators
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getLoadedValidators
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 hasLoadedValidators
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 isLoaded
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addConditionFromRule
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addConditionsFromRules
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addCondition
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addConditions
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getConditions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasConditions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addError
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getErrors
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 getEvaluated
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 hasErrors
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 isEvaluated
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getValidationStatus
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getConditionStatus
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 evaluateStatus
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
6
 setStrict
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getStrict
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isStrict
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 evaluateConditions
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
7
 evaluate
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
1 / 1
20
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Validator;
16
17use Pop\Utils\Arr;
18use Pop\Utils\Str;
19use Pop\Utils\CallableObject;
20
21/**
22 * Validator set class
23 *
24 * @category   Pop
25 * @package    Pop\Validator
26 * @author     Nick Sagona, III <nick@popphp.org>
27 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
28 * @license    https://www.popphp.org/license     New BSD License
29 * @version    5.0.0
30 */
31class ValidatorSet
32{
33
34    /**
35     * Constants
36     */
37    const PASSED_NONE = 0;
38    const PASSED_ALL  = 1;
39    const PASSED_SOME = 2;
40
41    const STRICT_NONE             = 0;
42    const STRICT_VALIDATIONS_ONLY = 1;
43    const STRICT_CONDITIONS_ONLY  = 2;
44    const STRICT_BOTH             = 3;
45
46    /**
47     * Validators
48     * @var array
49     */
50    protected array $validators = [];
51
52    /**
53     * Load validators
54     * @var array
55     */
56    protected array $loaded = [];
57
58    /**
59     * Evaluated validators and their true/false results
60     * @var array
61     */
62    protected array $evaluated = [];
63
64    /**
65     * Conditions
66     * @var array
67     */
68    protected array $conditions = [];
69
70    /**
71     * Errors
72     * @var array
73     */
74    protected array $errors = [];
75
76    /**
77     * Validation status
78     * @var ?int
79     */
80    protected ?int $validationStatus = null;
81
82    /**
83     * Condition status
84     * @var ?int
85     */
86    protected ?int $conditionStatus = null;
87
88    /**
89     * Strict flag
90     * @var int
91     */
92    protected int $strict = 3;
93
94    /**
95     * Get available validators over the provided input data
96     *
97     * @param  string $directory
98     * @param  ?array $exclude
99     * @return array
100     */
101    public static function getAvailableValidators(string $directory = __DIR__, ?array $exclude = null): array
102    {
103        if ($exclude === null) {
104            $exclude = [
105                '.', '..',
106                'AbstractValidator.php',
107                'Condition.php',
108                'Exception.php',
109                'Rule.php',
110                'TraverseTrait.php',
111                'ValidatorInterface.php',
112                'ValidatorSet.php',
113                'ValueComparisonTrait.php'
114            ];
115        }
116
117        $validatorClasses = array_map(function ($value) {
118            return str_replace('.php', '', $value);
119        }, array_filter(scandir($directory), function ($value) use ($exclude) {
120            return !in_array($value, $exclude);
121        }));
122
123        $validators = [];
124
125        foreach ($validatorClasses as $class) {
126            $validators[$class] = Str::titleCaseToSnakeCase($class);
127        }
128
129        return $validators;
130    }
131
132    /**
133     * Add validators
134     *
135     * @param  array|string $validators
136     * @param  ?string      $field
137     * @param  int          $strict
138     * @return static
139     */
140    public static function add(array|string $validators, ?string $field = null, int $strict = 3): static
141    {
142        $validatorSet = new static();
143        $validatorSet->setStrict($strict);
144
145        $validators = Arr::make($validators);
146
147        if ($field !== null) {
148            $validatorSet->addValidatorsToField($field, $validators);
149        } else {
150            $validatorSet->addValidators($validators);
151        }
152
153        return $validatorSet;
154    }
155
156    /**
157     * Load validator objects directly
158     *
159     * @param  array|string $validators
160     * @param  ?string      $field
161     * @param  int          $strict
162     * @return static
163     */
164    public static function load(array|string $validators, ?string $field = null, int $strict = 3): static
165    {
166        $validatorSet = new static();
167        $validatorSet->setStrict($strict);
168
169        $validators = Arr::make($validators);
170
171        if ($field !== null) {
172            $validatorSet->loadValidatorsToField($field, $validators);
173        } else {
174            $validatorSet->loadValidators($validators);
175        }
176
177        return $validatorSet;
178    }
179
180    /**
181     * Create validator set from rules
182     *
183     * @param  array|string $rules
184     * @param  int          $strict
185     * @param  string       $prefix
186     * @return static
187     */
188    public static function createFromRules(
189        array|string $rules, int $strict = 3, string $prefix = 'Pop\Validator\\'
190    ): static
191    {
192        $validatorSet = new static();
193        $validatorSet->setStrict($strict);
194
195        $rules = Arr::make($rules);
196
197        foreach ($rules as $rule) {
198            ['field' => $field, 'validator' => $validator, 'value' => $value, 'message' => $message] = Rule::parse($rule, $prefix);
199            $validatorSet->addValidator($field, $validator, $value, $message, $prefix);
200        }
201
202        return $validatorSet;
203    }
204
205    /**
206     * Add validator from rule
207     *
208     * @param  string $rule
209     * @param  string $prefix
210     * @return ValidatorSet
211     */
212    public function addValidatorFromRule(string $rule, string $prefix = 'Pop\Validator\\'): ValidatorSet
213    {
214        ['field' => $field, 'validator' => $validator, 'value' => $value, 'message' => $message] = Rule::parse($rule, $prefix);
215        $this->addValidator($field, $validator, $value, $message, $prefix);
216        return $this;
217    }
218
219    /**
220     * Add validators from rules
221     *
222     * @param  array $rules
223     * @param  string $prefix
224     * @return ValidatorSet
225     */
226    public function addValidatorsFromRules(array $rules, string $prefix = 'Pop\Validator\\'): ValidatorSet
227    {
228        foreach ($rules as $rule) {
229            $this->addValidatorFromRule($rule, $prefix);
230        }
231        return $this;
232    }
233
234    /**
235     * Add validator
236     *
237     * @param  string  $field
238     * @param  string  $validator
239     * @param  mixed   $value
240     * @param  ?string $message
241     * @param  ?string $prefix
242     * @return ValidatorSet
243     */
244    public function addValidator(
245        string $field, string $validator, mixed $value = null, ?string $message = null, ?string $prefix = 'Pop\Validator\\'
246    ): ValidatorSet
247    {
248        if (!isset($this->validators[$field])) {
249            $this->validators[$field] = [];
250        }
251        if (!class_exists($prefix . $validator)) {
252            throw new \InvalidArgumentException("Error: The validator class '" . $prefix . $validator . "' does not exist.");
253        }
254
255        $this->validators[$field][] = new CallableObject($prefix . $validator, [$value, $message]);
256        return $this;
257    }
258
259    /**
260     * Add validators to specific field
261     *
262     * @param  string $field
263     * @param  array  $validators
264     * @param  string $prefix
265     * @return ValidatorSet
266     */
267    public function addValidatorsToField(string $field, array $validators, string $prefix = 'Pop\Validator\\'): ValidatorSet
268    {
269        foreach ($validators as $validator => $value) {
270            if (is_array($value) && array_key_exists('value', $value) && isset($value['message'])) {
271                $this->addValidator($field, $validator, $value['value'], $value['message'], $prefix);
272            } else {
273                $this->addValidator($field, $validator, $value, null, $prefix);
274            }
275        }
276        return $this;
277    }
278
279    /**
280     * Add validators to specific field
281     *
282     * @param  array  $validators
283     * @param  string $prefix
284     * @return ValidatorSet
285     */
286    public function addValidators(array $validators, string $prefix = 'Pop\Validator\\'): ValidatorSet
287    {
288        foreach ($validators as $field => $validator) {
289            if (is_array($validator)) {
290                foreach ($validator as $val => $value) {
291                    if (is_array($value) && array_key_exists('value', $value) && isset($value['message'])) {
292                        $this->addValidator($field, $val, $value['value'], $value['message'], $prefix);
293                    } else {
294                        $this->addValidator($field, $val, $value, null, $prefix);
295                    }
296                }
297            } else {
298                $this->addValidator($field, $validator, null, null, $prefix);
299            }
300        }
301        return $this;
302    }
303
304    /**
305     * Load validator
306     *
307     * @param  string            $field
308     * @param  AbstractValidator $validator
309     * @param  array             $input
310     * @return ValidatorSet
311     */
312    public function loadValidator(string $field, AbstractValidator $validator, array $input = []): ValidatorSet
313    {
314        if (!isset($this->loaded[$field])) {
315            $this->loaded[$field] = [];
316        }
317
318        // If the value references a value in the input array
319        $value = $validator->getValue();
320        if (is_string($value) && str_starts_with($value, '[') && str_ends_with($value, ']')) {
321            $value = substr($value, 1, -1);
322            if (array_key_exists($value, $input)) {
323                $validator->setValue($input[$value]);
324            }
325        }
326
327        $this->loaded[$field][] = $validator;
328        return $this;
329    }
330
331    /**
332     * Load validators to specific field
333     *
334     * @param  string $field
335     * @param  array  $validators
336     * @param  array  $input
337     * @return ValidatorSet
338     */
339    public function loadValidatorsToField(string $field, array $validators, array $input = []): ValidatorSet
340    {
341        foreach ($validators as $validator) {
342            if ($validator instanceof CallableObject) {
343                $validator = $validator->call();
344            }
345            $this->loadValidator($field, $validator, $input);
346        }
347        return $this;
348    }
349
350    /**
351     * Load validators to specific field
352     *
353     * @param  ?array $validators
354     * @pparam array  $input
355     * @return ValidatorSet
356     */
357    public function loadValidators(?array $validators = null, array $input = []): ValidatorSet
358    {
359        if ($validators === null) {
360            $validators = $this->validators;
361        }
362        foreach ($validators as $field => $validator) {
363            foreach ($validator as $val) {
364                if ($val instanceof CallableObject) {
365                    $val = $val->call();
366                }
367                $this->loadValidator($field, $val, $input);
368            }
369        }
370        return $this;
371    }
372
373    /**
374     * Get validators
375     *
376     * @param  ?string $field
377     * @return array
378     */
379    public function getValidators(?string $field = null): array
380    {
381        if ($field !== null) {
382            return (isset($this->validators[$field])) ? $this->validators[$field] : [];
383        } else {
384            return $this->validators;
385        }
386    }
387
388    /**
389     * Has validators
390     *
391     * @param  ?string $field
392     * @return bool
393     */
394    public function hasValidators(?string $field = null): bool
395    {
396        if ($field !== null) {
397            return (isset($this->validators[$field]));
398        } else {
399            return (!empty($this->validators));
400        }
401    }
402
403    /**
404     * Get loaded validators
405     *
406     * @param  ?string $field
407     * @return array
408     */
409    public function getLoadedValidators(?string $field = null): array
410    {
411        if ($field !== null) {
412            return (isset($this->loaded[$field])) ? $this->loaded[$field] : [];
413        } else {
414            return $this->loaded;
415        }
416    }
417
418    /**
419     * Has loaded validators
420     *
421     * @param  ?string $field
422     * @return bool
423     */
424    public function hasLoadedValidators(?string $field = null): bool
425    {
426        if ($field !== null) {
427            return (isset($this->loaded[$field]));
428        } else {
429            return (!empty($this->loaded));
430        }
431    }
432
433    /**
434     * Is loaded
435     *
436     * @param  ?string $field
437     * @return bool
438     */
439    public function isLoaded(?string $field = null): bool
440    {
441        if ($field !== null) {
442            return (isset($this->loaded[$field]));
443        } else {
444            return (!empty($this->loaded));
445        }
446    }
447
448    /**
449     * Add condition from rule
450     *
451     * @param  string $rule
452     * @param  string $prefix
453     * @return ValidatorSet
454     */
455    public function addConditionFromRule(string $rule, string $prefix = 'Pop\Validator\\'): ValidatorSet
456    {
457        $this->addCondition(Condition::createFromRule($rule, $prefix));
458        return $this;
459    }
460
461    /**
462     * Add conditions from rules
463     *
464     * @param  array  $rules
465     * @param  string $prefix
466     * @return ValidatorSet
467     */
468    public function addConditionsFromRules(array $rules, string $prefix = 'Pop\Validator\\'): ValidatorSet
469    {
470        foreach ($rules as $rule) {
471            $this->addConditionFromRule($rule, $prefix);
472        }
473        return $this;
474    }
475
476    /**
477     * Add condition
478     *
479     * @param  Condition $condition
480     * @return ValidatorSet
481     */
482    public function addCondition(Condition $condition): ValidatorSet
483    {
484        $this->conditions[] = $condition;
485        return $this;
486    }
487
488    /**
489     * Add conditions
490     *
491     * @param  array $conditions
492     * @return ValidatorSet
493     */
494    public function addConditions(array $conditions): ValidatorSet
495    {
496        foreach ($conditions as $condition) {
497            $this->addCondition($condition);
498        }
499        return $this;
500    }
501
502    /**
503     * Get conditions
504     *
505     * @return array
506     */
507    public function getConditions(): array
508    {
509        return $this->conditions;
510    }
511
512    /**
513     * Has conditions
514     *
515     * @return bool
516     */
517    public function hasConditions(): bool
518    {
519        return (!empty($this->conditions));
520    }
521
522    /**
523     * Add error
524     *
525     * @param  string $field
526     * @param  string $error
527     * @return ValidatorSet
528     */
529    public function addError(string $field, string $error): ValidatorSet
530    {
531        if (!isset($this->errors[$field])) {
532            $this->errors[$field] = [];
533        }
534        $this->errors[$field][] = $error;
535        return $this;
536    }
537
538    /**
539     * Get errors
540     *
541     * @param  ?string $field
542     * @return array
543     */
544    public function getErrors(?string $field = null): array
545    {
546        if ($field !== null) {
547            return (isset($this->errors[$field])) ? $this->errors[$field] : [];
548        } else {
549            return $this->errors;
550        }
551    }
552
553    /**
554     * Get evaluated
555     *
556     * @param  ?string $field
557     * @return mixed
558     */
559    public function getEvaluated(?string $field = null): mixed
560    {
561        if ($field !== null) {
562            return (isset($this->evaluated[$field])) ? $this->evaluated[$field] : null;
563        } else {
564            return $this->evaluated;
565        }
566    }
567
568    /**
569     * Has errors
570     *
571     * @param  ?string $field
572     * @return bool
573     */
574    public function hasErrors(?string $field = null): bool
575    {
576        if ($field !== null) {
577            return (isset($this->errors[$field]));
578        } else {
579            return (!empty($this->errors));
580        }
581    }
582
583    /**
584     * Is evaluated
585     *
586     * @param  ?string $field
587     * @return bool
588     */
589    public function isEvaluated(?string $field = null): bool
590    {
591        if ($field !== null) {
592            return (array_key_exists($field, $this->evaluated));
593        } else {
594            return (!empty($this->evaluated));
595        }
596    }
597
598    /**
599     * Get validation status
600     *
601     * @return ?int
602     */
603    public function getValidationStatus(): ?int
604    {
605        return $this->validationStatus;
606    }
607    /**
608     * Get condition status
609     *
610     * @return ?int
611     */
612    public function getConditionStatus(): ?int
613    {
614        return $this->conditionStatus;
615    }
616
617    /**
618     * Evaluate status
619     *
620     * @return ValidatorSet
621     */
622    public function evaluateStatus(): ValidatorSet
623    {
624        $numOfValidators = 0;
625        $numOfPassed     = 0;
626
627        foreach ($this->evaluated as $field => $evaluated) {
628            foreach ($evaluated as $result) {
629                $numOfValidators++;
630                if ($result) {
631                    $numOfPassed++;
632                }
633            }
634        }
635
636        if ($numOfValidators == $numOfPassed) {
637            $this->validationStatus = self::PASSED_ALL;
638        } else if ($numOfPassed > 0) {
639            $this->validationStatus = self::PASSED_SOME;
640        } else {
641            $this->validationStatus = self::PASSED_NONE;
642        }
643
644        return $this;
645    }
646
647    /**
648     * Set strict
649     *
650     * @param  int $strict
651     * @return ValidatorSet
652     */
653    public function setStrict(int $strict): ValidatorSet
654    {
655        if ($strict < 0 || $strict > 3) {
656            throw new \InvalidArgumentException('Error: Strict must be between 0 and 3');
657        }
658        $this->strict = $strict;
659        return $this;
660    }
661
662    /**
663     * Get strict
664     *
665     * @return int
666     */
667    public function getStrict(): int
668    {
669        return $this->strict;
670    }
671
672    /**
673     * Is strict
674     *
675     * @return bool
676     */
677    public function isStrict(): bool
678    {
679        return ($this->strict > 0);
680    }
681
682    /**
683     * Evaluate all conditions over the provided input data
684     *
685     * @param  array $input
686     * @return bool
687     */
688    public function evaluateConditions(array $input): bool
689    {
690        $numOfConditions = count($this->conditions);
691        $numOfPassed     = 0;
692
693        foreach ($this->conditions as $condition) {
694            if ($condition->evaluate($input)) {
695                $numOfPassed++;
696            }
697        }
698
699        if ($numOfConditions == $numOfPassed) {
700            $this->conditionStatus = self::PASSED_ALL;
701        } else if ($numOfPassed > 0) {
702            $this->conditionStatus = self::PASSED_SOME;
703        } else {
704            $this->conditionStatus = self::PASSED_NONE;
705        }
706
707        // Passed all
708        if (($this->strict == self::STRICT_BOTH) || ($this->strict == self::STRICT_CONDITIONS_ONLY)) {
709            return ($numOfConditions == $numOfPassed);
710        // Passed some, or false if none passed
711        } else {
712            return ($numOfPassed > 0);
713        }
714    }
715
716    /**
717     * Evaluate all validators over the provided input data
718     *
719     * @param  array   $input
720     * @param  ?string $prefix
721     * @return bool
722     */
723    public function evaluate(array $input, ?string $prefix = 'Pop\Validator\\'): bool
724    {
725        // If conditions are met, or there are no conditions
726        if (!($this->hasConditions()) || $this->evaluateConditions($input)) {
727            if (!$this->isLoaded()) {
728                $this->loadValidators(null, $input);
729            }
730
731            foreach ($this->loaded as $field => $validators) {
732                foreach ($validators as $validator) {
733                    $result = (isset($input[$field]) && !Rule::isHasClass(get_class($validator), true, $prefix)) ?
734                        $validator->evaluate($input[$field]) : $validator->evaluate($input);
735                    if (!$result) {
736                        $this->addError($field, $validator->getMessage());
737                    }
738                    if (!isset($this->evaluated[$field])) {
739                        $this->evaluated[$field] = [];
740                    }
741                    $this->evaluated[$field][] = $result;
742                }
743            }
744
745            // Catch any validators that were added after a load call
746            foreach ($this->validators as $field => $validators) {
747                if (!$this->isEvaluated($field)) {
748                    $this->loadValidatorsToField($field, $validators, $input);
749
750                    if ($this->isLoaded($field)) {
751                        foreach ($this->loaded[$field] as $validator) {
752                            $result = (isset($input[$field]) && !Rule::isHasClass(get_class($validator), true, $prefix)) ?
753                                $validator->evaluate($input[$field]) : $validator->evaluate($input);
754                            if (!$result) {
755                                $this->addError($field, $validator->getMessage());
756                            }
757                            if (!isset($this->evaluated[$field])) {
758                                $this->evaluated[$field] = [];
759                            }
760                            $this->evaluated[$field][] = $result;
761                        }
762                    }
763                }
764            }
765
766            $this->evaluateStatus();
767        }
768
769        // Passed all
770        if (($this->strict == self::STRICT_BOTH) || ($this->strict == self::STRICT_VALIDATIONS_ONLY)) {
771            return (!$this->hasErrors());
772        // Passed some, or false if none passed
773        } else {
774            return ($this->validationStatus > 0);
775        }
776    }
777
778}