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