Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
128 / 128
100.00% covered (success)
100.00%
28 / 28
CRAP
100.00% covered (success)
100.00%
1 / 1
FormValidator
100.00% covered (success)
100.00%
128 / 128
100.00% covered (success)
100.00%
28 / 28
87
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
6
 createFromConfig
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
6
 addValidators
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addValidator
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 hasValidators
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getValidators
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 hasValidator
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getValidator
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 removeValidators
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 removeValidator
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 setRequired
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 isRequired
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 removeRequired
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setValues
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getValues
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 filterValue
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 filter
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 validate
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
12
 hasErrors
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 getErrors
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 getError
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 addError
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 count
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 toArray
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 __set
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __get
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __isset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __unset
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
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\Form;
16
17/**
18 * Form validator class
19 *
20 * @category   Pop
21 * @package    Pop\Form
22 * @author     Nick Sagona, III <nick@popphp.org>
23 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
24 * @license    https://www.popphp.org/license     New BSD License
25 * @version    5.0.0
26 */
27
28class FormValidator implements FormInterface, \ArrayAccess, \Countable, \IteratorAggregate
29{
30
31    /**
32     * Trait declaration
33     */
34    use FormTrait;
35    use ValidatorEvaluationTrait;
36
37    /**
38     * Form validators
39     * @var array
40     */
41    protected array $validators = [];
42
43    /**
44     * Required fields
45     * @var array
46     */
47    protected array $required = [];
48
49    /**
50     * Form values
51     * @var array
52     */
53    protected array $values = [];
54
55    /**
56     * Form validation errors
57     * @var array
58     */
59    protected array $errors = [];
60
61    /**
62     * Constructor
63     *
64     * Instantiate the form validator object
65     *
66     * @param ?array $validators
67     * @param mixed  $required
68     * @param ?array $values
69     * @param mixed  $filters
70     */
71    public function __construct(?array $validators = null, mixed $required = null, ?array $values = null, mixed $filters = null)
72    {
73        if (!empty($validators)) {
74            $this->addValidators($validators);
75        }
76        if ($required !== null) {
77            $this->setRequired($required);
78        }
79        if ($values !== null) {
80            $this->setValues($values);
81        }
82        if ($filters !== null) {
83            if (is_array($filters)) {
84                $this->addFilters($filters);
85            } else {
86                $this->addFilter($filters);
87            }
88        }
89    }
90
91    /**
92     * Create form validator from config
93     *
94     * @param  array|FormConfig $formConfig
95     * @param  mixed            $required
96     * @param  ?array           $values
97     * @param  mixed            $filters
98     * @return FormValidator
99     */
100    public static function createFromConfig(
101        array|FormConfig $formConfig, mixed $required = null, ?array $values = null, mixed $filters = null
102    ): FormValidator
103    {
104        $validators = [];
105        $required   = [];
106
107        foreach ($formConfig as $key => $value) {
108            if (!empty($value['validator'])) {
109                $validators[$key] = $value['validator'];
110            } else if (!empty($value['validators'])) {
111                $validators[$key] = $value['validators'];
112            }
113            if (isset($value['required']) && ($value['required'] == true)) {
114                $required[] = $key;
115            }
116        }
117
118        return new self($validators, $required, $values, $filters);
119    }
120
121    /**
122     * Add validators
123     *
124     * @param  array $validators
125     * @return FormValidator
126     */
127    public function addValidators(array $validators): FormValidator
128    {
129        foreach ($validators as $field => $validator) {
130            $this->addValidator($field, $validator);
131        }
132        return $this;
133    }
134
135    /**
136     * Add validator
137     *
138     * @param  string $field
139     * @param  mixed  $validator
140     * @return FormValidator
141     */
142    public function addValidator(string $field, mixed $validator): FormValidator
143    {
144        if (!isset($this->validators[$field])) {
145            $this->validators[$field] = [];
146        }
147
148        if (!is_array($validator)) {
149            $validator = [$validator];
150        }
151
152        foreach ($validator as $valid) {
153            if (!in_array($valid, $this->validators[$field], true)) {
154                $this->validators[$field][] = $valid;
155            }
156        }
157
158        return $this;
159    }
160
161    /**
162     * Has validators
163     *
164     * @param  ?string $field
165     * @return bool
166     */
167    public function hasValidators(?string $field = null)
168    {
169        if ($field === null) {
170            return (count($this->validators) > 0);
171        } else if (isset($this->validators[$field])) {
172            return (count($this->validators[$field]) > 0);
173        } else {
174            return false;
175        }
176    }
177
178    /**
179     * Get validators
180     *
181     * @param  string $field
182     * @return mixed
183     */
184    public function getValidators(?string $field = null)
185    {
186        if ($field === null) {
187            return $this->validators;
188        } else if (isset($this->validators[$field])) {
189            return $this->validators[$field];
190        } else {
191            return null;
192        }
193    }
194
195    /**
196     * Has validator
197     *
198     * @param  string $field
199     * @param  int    $index
200     * @return bool
201     */
202    public function hasValidator(string $field, int $index): bool
203    {
204        return (isset($this->validators[$field]) && isset($this->validators[$field][$index]));
205    }
206
207    /**
208     * Get validator
209     *
210     * @param  string $field
211     * @param  int    $index
212     * @return mixed
213     */
214    public function getValidator(string $field, int $index)
215    {
216        return (isset($this->validators[$field]) && isset($this->validators[$field][$index])) ?
217            $this->validators[$field][$index] : null;
218    }
219
220    /**
221     * Remove validators
222     *
223     * @param  ?string $field
224     * @return FormValidator
225     */
226    public function removeValidators(?string $field = null): FormValidator
227    {
228        if (($field !== null) && isset($this->validators[$field])) {
229            unset($this->validators[$field]);
230        } else if ($field === null) {
231            $this->validators = [];
232        }
233        return $this;
234    }
235
236    /**
237     * Remove validator
238     *
239     * @param  string $field
240     * @param  int    $index
241     * @return FormValidator
242     */
243    public function removeValidator(string $field, int $index): FormValidator
244    {
245        if (isset($this->validators[$field]) && isset($this->validators[$field][$index])) {
246            unset($this->validators[$field][$index]);
247        }
248        return $this;
249    }
250
251    /**
252     * Set required
253     *
254     * @param  mixed   $required
255     * @param  ?string $requiredMessage
256     * @return FormValidator
257     */
258    public function setRequired(mixed $required, ?string $requiredMessage = 'This field is required.'): FormValidator
259    {
260        if (!is_array($required)) {
261            $required = [$required];
262        }
263
264        foreach ($required as $req) {
265            if (!in_array($req, $this->required)) {
266                $this->required[$req] = $requiredMessage;
267            }
268        }
269
270        return $this;
271    }
272
273    /**
274     * Is required
275     *
276     * @param  string $field
277     * @return bool
278     */
279    public function isRequired(string $field): bool
280    {
281        return array_key_exists($field, $this->required);
282    }
283
284    /**
285     * Remove required
286     *
287     * @param  string $field
288     * @return FormValidator
289     */
290    public function removeRequired(string $field): FormValidator
291    {
292        if (array_key_exists($field, $this->required)) {
293            unset($this->required[$field]);
294        }
295
296        return $this;
297    }
298
299    /**
300     * Set values
301     *
302     * @param  array $values
303     * @return FormValidator
304     */
305    public function setValues(array $values): FormValidator
306    {
307        $this->values = $values;
308        return $this;
309    }
310
311    /**
312     * Get values
313     *
314     * @return array
315     */
316    public function getValues(): array
317    {
318        return $this->values;
319    }
320
321    /**
322     * Filter value with the filters
323     *
324     * @param  mixed $field
325     * @throws Exception
326     * @return mixed
327     */
328    public function filterValue(mixed $field): mixed
329    {
330        if (!isset($this->values[$field])) {
331            throw new Exception("Error: A value for '" . $field . "' has not been set.");
332        }
333
334        $value = $this->values[$field];
335
336        foreach ($this->filters as $filter) {
337            $value = $filter->filter($value, $field);
338        }
339
340        $this->values[$field] = $value;
341
342        return $value;
343    }
344
345    /**
346     * Filter values with the filters
347     *
348     * @param  mixed $values
349     * @return mixed
350     */
351    public function filter(mixed $values = null): mixed
352    {
353        if ($values !== null) {
354            $this->values = $values;
355        }
356
357        foreach ($this->values as $name => $value) {
358            $this->values[$name] = $this->filterValue($name);
359        }
360
361        return $this->values;
362    }
363
364    /**
365     * Validate values
366     *
367     * @param  mixed $fields
368     * @return bool
369     */
370    public function validate(mixed $fields = null): bool
371    {
372        $this->filter();
373
374        if ($fields !== null) {
375            $fields     = (!is_array($fields)) ? [$fields] : $fields;
376            $formFields = array_filter(
377                $this->values,
378                function ($key) use ($fields) {
379                    return in_array($key, $fields);
380                },
381                ARRAY_FILTER_USE_KEY
382            );
383
384            foreach ($this->required as $field => $requiredMessage) {
385                if (in_array($field, $fields) && !isset($formFields[$field])) {
386                    $this->addError($field, $requiredMessage);
387                }
388            }
389        } else {
390            $formFields = $this->values;
391            // Check for required fields
392            foreach ($this->required as $field => $requiredMessage) {
393                if (!isset($formFields[$field])) {
394                    $this->addError($field, $requiredMessage);
395                }
396            }
397        }
398
399        // Check for required fields and execute any field validators
400        foreach ($formFields as $field => $value) {
401            if ($this->hasValidators($field)) {
402                foreach ($this->validators[$field] as $validator) {
403                    foreach ($this->evaluateValidator($validator, $value, $formFields) as $message) {
404                        $this->addError($field, $message);
405                    }
406                }
407            }
408        }
409
410        return !$this->hasErrors();
411    }
412
413    /**
414     * Has errors
415     *
416     * @param  ?string $field
417     * @return bool
418     */
419    public function hasErrors(?string $field = null): bool
420    {
421        if ($field !== null) {
422            return (isset($this->errors[$field]) && (count($this->errors[$field]) > 0));
423        } else {
424            return (count($this->errors) > 0);
425        }
426    }
427
428    /**
429     * Get errors
430     *
431     * @param  ?string $field
432     * @return array
433     */
434    public function getErrors(?string $field = null): array
435    {
436        if (($field !== null) && isset($this->errors[$field])) {
437            return $this->errors[$field];
438        } else {
439            return $this->errors;
440        }
441    }
442
443    /**
444     * Get error
445     *
446     * @param  string $field
447     * @param  int    $index
448     * @return mixed
449     */
450    public function getError(string $field, int $index): mixed
451    {
452        return (isset($this->errors[$field]) && isset($this->errors[$field][$index])) ?
453            $this->errors[$field][$index] : null;
454    }
455
456    /**
457     * Add error
458     *
459     * @param  string $field
460     * @param  string $error
461     * @return FormValidator
462     */
463    protected function addError(string $field, string $error): FormValidator
464    {
465        if (!isset($this->errors[$field])) {
466            $this->errors[$field] = [];
467        }
468
469        if (!in_array($error, $this->errors[$field])) {
470            $this->errors[$field][] = $error;
471        }
472
473        return $this;
474    }
475
476    /**
477     * Count of values
478     *
479     * @return int
480     */
481    public function count(): int
482    {
483        return count($this->values);
484    }
485
486    /**
487     * Get values
488     *
489     * @param  array $options
490     * @return array
491     */
492    public function toArray(array $options = []): array
493    {
494        $fieldValues = $this->values;
495
496        if (!empty($options)) {
497            if (isset($options['exclude'])) {
498                if (!is_array($options['exclude'])) {
499                    $options['exclude'] = [$options['exclude']];
500                }
501                $fieldValues = array_diff_key($fieldValues, array_flip($options['exclude']));
502            }
503            if (isset($options['filter'])) {
504                $fieldValues = array_filter($fieldValues, $options['filter']);
505            }
506        }
507
508        return $fieldValues;
509    }
510
511    /**
512     * Set method to set the property to the value of values[$name]
513     *
514     * @param  string $name
515     * @param  mixed $value
516     * @return void
517     */
518    public function __set(string $name, mixed $value): void
519    {
520        $this->values[$name] = $value;
521    }
522
523    /**
524     * Get method to return the value of values[$name]
525     *
526     * @param  string $name
527     * @return mixed
528     */
529    public function __get(string $name): mixed
530    {
531        return $this->values[$name] ?? null;
532    }
533
534    /**
535     * Return the isset value of values[$name]
536     *
537     * @param  string $name
538     * @return bool
539     */
540    public function __isset(string $name): bool
541    {
542        return isset($this->values[$name]);
543    }
544
545    /**
546     * Unset values[$name]
547     *
548     * @param  string $name
549     * @return void
550     */
551    public function __unset(string $name): void
552    {
553        if (isset($this->values[$name])) {
554            unset($this->values[$name]);
555        }
556    }
557
558}