Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.00% covered (success)
98.00%
245 / 250
96.36% covered (success)
96.36%
53 / 55
CRAP
0.00% covered (danger)
0.00%
0 / 1
Form
98.00% covered (success)
98.00%
245 / 250
96.36% covered (success)
96.36%
53 / 55
140
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 createFromConfig
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 createFromFieldsetConfig
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 createFieldset
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 setAction
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setMethod
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getAction
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMethod
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setAttribute
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 setAttributes
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addFieldset
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 removeFieldset
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 getFieldset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFieldsets
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasFieldsets
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addColumn
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 hasColumn
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getColumn
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 removeColumn
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getCurrent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setCurrent
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getLegend
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setLegend
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addField
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 addFields
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addFieldFromConfig
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addFieldsFromConfig
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
7
 addFieldsetsFromConfig
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 insertFieldBefore
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 insertFieldAfter
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 count
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 toArray
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getField
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 getFields
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 hasField
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasFields
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 removeField
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getFieldValue
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setFieldValue
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 setFieldValues
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 filterValue
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
9
 filter
85.71% covered (success)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
4.05
 isValid
100.00% covered (success)
100.00%
7 / 7
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
2
 getAllErrors
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 reset
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 clearTokens
50.00% covered (warning)
50.00%
4 / 8
0.00% covered (danger)
0.00%
0 / 1
8.12
 prepare
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
8
 prepareForView
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 render
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 __toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __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%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 __unset
100.00% covered (success)
100.00%
3 / 3
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
16use Pop\Dom\Child;
17use Pop\Form\Element\AbstractElement;
18use Pop\Form\Element\Input\Checkbox;
19use Pop\Form\Element\Input\Radio;
20use Pop\Form\Element\Input\File;
21
22/**
23 * Form class
24 *
25 * @category   Pop
26 * @package    Pop\Form
27 * @author     Nick Sagona, III <dev@nolainteractive.com>
28 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
29 * @license    http://www.popphp.org/license     New BSD License
30 * @version    4.0.0
31 */
32
33class Form extends Child implements FormInterface, \ArrayAccess, \Countable, \IteratorAggregate
34{
35
36    /**
37     * Trait declaration
38     */
39    use FormTrait;
40
41    /**
42     * Field fieldsets
43     * @var array
44     */
45    protected array $fieldsets = [];
46
47    /**
48     * Form columns
49     * @var array
50     */
51    protected array $columns = [];
52
53    /**
54     * Current field fieldset
55     * @var int
56     */
57    protected int $current = 0;
58
59    /**
60     * Constructor
61     *
62     * Instantiate the form object
63     *
64     * @param  ?array  $fields
65     * @param  ?string $action
66     * @param  string  $method
67     */
68    public function __construct(?array $fields = null, ?string $action = null, string $method = 'post')
69    {
70        if ($action === null) {
71            $action = (isset($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : '#';
72        }
73
74        parent::__construct('form');
75        $this->setAction($action);
76        $this->setMethod($method);
77
78        if ($fields !== null) {
79            $this->addFields($fields);
80        }
81    }
82
83    /**
84     * Method to create form object and fields from config
85     *
86     * @param  array|FormConfig $config
87     * @param  ?string           $container
88     * @param  ?string          $action
89     * @param  string           $method
90     * @return Form
91     */
92    public static function createFromConfig(
93        array|FormConfig $config, ?string $container = null, ?string $action = null, string $method = 'post'
94    ): Form
95    {
96        $form = new static(null, $action, $method);
97        $form->addFieldsFromConfig($config, $container);
98        return $form;
99    }
100
101    /**
102     * Method to create form object and fields from config
103     *
104     * @param  array|FormConfig $config
105     * @param  ?string          $container
106     * @param  ?string          $action
107     * @param  string           $method
108     * @return Form
109     */
110    public static function createFromFieldsetConfig(
111        array|FormConfig $config, ?string $container = null, ?string $action = null, string $method = 'post'
112    ): Form
113    {
114        $form = new static(null, $action, $method);
115        $form->addFieldsetsFromConfig($config, $container);
116        return $form;
117    }
118
119    /**
120     * Method to create a new fieldset object
121     *
122     * @param  ?string  $legend
123     * @param  ?string  $container
124     * @return Fieldset
125     */
126    public function createFieldset(?string $legend = null, ?string $container = null): Fieldset
127    {
128        $fieldset = new Fieldset();
129        if ($legend !== null) {
130            $fieldset->setLegend($legend);
131        }
132        if ($container !== null) {
133            $fieldset->setContainer($container);
134        }
135
136        $this->addFieldset($fieldset);
137
138        $id = ($this->getAttribute('id') !== null) ?
139            $this->getAttribute('id') . '-fieldset-' . ($this->current + 1) : 'pop-form-fieldset-' . ($this->current + 1);
140
141        $class = ($this->getAttribute('class') !== null) ?
142            $this->getAttribute('id') . '-fieldset' : 'pop-form-fieldset';
143
144        $fieldset->setAttribute('id', $id);
145        $fieldset->setAttribute('class', $class);
146
147        return $fieldset;
148    }
149
150    /**
151     * Method to set action
152     *
153     * @param  string $action
154     * @return Form
155     */
156    public function setAction(string $action): Form
157    {
158        $this->setAttribute('action', str_replace(['?captcha=1', '&captcha=1'], ['', ''], $action));
159        return $this;
160    }
161
162    /**
163     * Method to set method
164     *
165     * @param  string $method
166     * @return Form
167     */
168    public function setMethod(string $method): Form
169    {
170        $this->setAttribute('method', $method);
171        return $this;
172    }
173
174    /**
175     * Method to get action
176     *
177     * @return string|null
178     */
179    public function getAction(): string|null
180    {
181        return $this->getAttribute('action');
182    }
183
184    /**
185     * Method to get method
186     *
187     * @return string|null
188     */
189    public function getMethod(): string|null
190    {
191        return $this->getAttribute('method');
192    }
193
194    /**
195     * Method to set an attribute
196     *
197     * @param  string $a
198     * @param  string $v
199     * @return Form
200     */
201    public function setAttribute(string $a, string $v): Form
202    {
203        parent::setAttribute($a, $v);
204
205        if ($a == 'id') {
206            foreach ($this->fieldsets as $i => $fieldset) {
207                $id = $v . '-fieldset-' . ($i + 1);
208                $fieldset->setAttribute('id', $id);
209            }
210
211        } else if ($a == 'class') {
212            foreach ($this->fieldsets as $i => $fieldset) {
213                $class = $v . '-fieldset';
214                $fieldset->setAttribute('class', $class);
215            }
216        }
217
218        return $this;
219    }
220
221    /**
222     * Method to set attributes
223     *
224     * @param  array $a
225     * @return Form
226     */
227    public function setAttributes(array $a): Form
228    {
229        foreach ($a as $name => $value) {
230            $this->setAttribute($name, $value);
231        }
232        return $this;
233    }
234
235    /**
236     * Method to add fieldset
237     *
238     * @param  Fieldset $fieldset
239     * @return Form
240     */
241    public function addFieldset(Fieldset $fieldset): Form
242    {
243        $this->fieldsets[] = $fieldset;
244        $this->current     = count($this->fieldsets) - 1;
245        return $this;
246    }
247
248    /**
249     * Method to remove fieldset
250     *
251     * @param  int $i
252     * @return Form
253     */
254    public function removeFieldset(int $i): Form
255    {
256        if (isset($this->fieldsets[(int)$i])) {
257            unset($this->fieldsets[(int)$i]);
258        }
259        $this->fieldsets = array_values($this->fieldsets);
260        if (!isset($this->fieldsets[$this->current])) {
261            $this->current = (count($this->fieldsets) > 0) ? count($this->fieldsets) - 1 : 0;
262        }
263        return $this;
264    }
265
266    /**
267     * Method to get current fieldset
268     *
269     * @return Fieldset|null
270     */
271    public function getFieldset(): Fieldset|null
272    {
273        return $this->fieldsets[$this->current] ?? null;
274    }
275
276    /**
277     * Method to get all fieldsets
278     *
279     * @return array
280     */
281    public function getFieldsets(): array
282    {
283        return $this->fieldsets;
284    }
285
286    /**
287     * Method to determine if the form has fieldsets
288     *
289     * @return bool
290     */
291    public function hasFieldsets(): bool
292    {
293        return !empty($this->fieldsets);
294    }
295
296    /**
297     * Method to add form column
298     *
299     * @param  mixed   $fieldsets
300     * @param  ?string $class
301     * @return Form
302     */
303    public function addColumn(mixed $fieldsets, ?string $class = null): Form
304    {
305        if (!is_array($fieldsets)) {
306            $fieldsets = [$fieldsets];
307        }
308
309        foreach ($fieldsets as $i => $num) {
310            $fieldsets[$i] = (int)$num - 1;
311        }
312
313        if ($class === null) {
314            $class = 'pop-form-column-' . (count($this->columns) + 1);
315        }
316
317        $this->columns[$class] = $fieldsets;
318        return $this;
319    }
320
321    /**
322     * Method to determine if form has a column
323     *
324     * @param  string $class
325     * @return bool
326     */
327    public function hasColumn(string $class): bool
328    {
329        if (is_numeric($class)) {
330            $class = 'pop-form-column-' . $class;
331        }
332
333        return isset($this->columns[$class]);
334    }
335
336    /**
337     * Method to get form column
338     *
339     * @param  string $class
340     * @return array|null
341     */
342    public function getColumn(string $class): array|null
343    {
344        if (is_numeric($class)) {
345            $class = 'pop-form-column-' . $class;
346        }
347
348        return $this->columns[$class] ?? null;
349    }
350
351    /**
352     * Method to remove form column
353     *
354     * @param  string $class
355     * @return Form
356     */
357    public function removeColumn(string $class): Form
358    {
359        if (is_numeric($class)) {
360            $class = 'pop-form-column-' . $class;
361        }
362
363        if (isset($this->columns[$class])) {
364            unset($this->columns[$class]);
365        }
366
367        return $this;
368    }
369
370    /**
371     * Method to get current fieldset index
372     *
373     * @return int
374     */
375    public function getCurrent(): int
376    {
377        return $this->current;
378    }
379
380    /**
381     * Method to get current fieldset index
382     *
383     * @param  int $i
384     * @return Form
385     */
386    public function setCurrent(int $i): Form
387    {
388        $this->current = (int)$i;
389        if (!isset($this->fieldsets[$this->current])) {
390            $this->fieldsets[$this->current] = $this->createFieldset();
391        }
392        return $this;
393    }
394
395    /**
396     * Method to get the legend of the current fieldset
397     *
398     * @return string|null
399     */
400    public function getLegend(): string|null
401    {
402        return $this->fieldsets[$this->current]?->getLegend();
403    }
404
405    /**
406     * Method to set the legend of the current fieldset
407     *
408     * @param  string $legend
409     * @return Form
410     */
411    public function setLegend(string $legend): Form
412    {
413        if (isset($this->fieldsets[$this->current])) {
414            $this->fieldsets[$this->current]->setLegend($legend);
415        }
416        return $this;
417    }
418
419    /**
420     * Method to add a form field
421     *
422     * @param  AbstractElement $field
423     * @param  ?string         $container
424     * @return Form
425     */
426    public function addField(AbstractElement $field, ?string $container = null): Form
427    {
428        if (count($this->fieldsets) == 0) {
429            $this->createFieldset(null, $container);
430        }
431        $this->fieldsets[$this->current]->addField($field);
432        return $this;
433    }
434
435    /**
436     * Method to add form fields
437     *
438     * @param  array $fields
439     * @return Form
440     */
441    public function addFields(array $fields): Form
442    {
443        foreach ($fields as $field) {
444            $this->addField($field);
445        }
446        return $this;
447    }
448
449    /**
450     * Method to add a form field from a config
451     *
452     * @param  string $name
453     * @param  array  $field
454     * @return Form
455     */
456    public function addFieldFromConfig(string $name, array $field): Form
457    {
458        $this->addField(Fields::create($name, $field));
459        return $this;
460    }
461
462    /**
463     * Method to add form fields from config
464     *
465     * @param  array|FormConfig $config
466     * @param  ?string          $container
467     * @return Form
468     */
469    public function addFieldsFromConfig(array|FormConfig $config, ?string $container = null): Form
470    {
471        $i = 1;
472        foreach ($config as $name => $field) {
473            if (is_numeric($name) && !isset($field[$name]['type'])) {
474                $fields = [];
475                foreach ($field as $n => $f) {
476                    $fields[$n] = Fields::create($n, $f);
477                }
478                if ($i > 1) {
479                    $this->fieldsets[$this->current]->createGroup();
480                }
481                if (!isset($this->fieldsets[$this->current])) {
482                    $this->fieldsets[$this->current] = new Fieldset(null, $container);
483                }
484                $this->fieldsets[$this->current]->addFields($fields);
485                $i++;
486            } else {
487                $this->addField(Fields::create($name, $field), $container);
488            }
489        }
490        return $this;
491    }
492
493    /**
494     * Method to add form fieldsets from config
495     *
496     * @param  array|FormConfig $fieldsets
497     * @param  ?string          $container
498     * @return Form
499     */
500    public function addFieldsetsFromConfig(array|FormConfig $fieldsets, ?string $container = null): Form
501    {
502        foreach ($fieldsets as $legend => $config) {
503            if (!is_numeric($legend)) {
504                $this->createFieldset($legend, $container);
505            } else {
506                $this->createFieldset(null, $container);
507            }
508            $this->addFieldsFromConfig($config);
509        }
510
511        return $this;
512    }
513
514    /**
515     * Method to insert a field before another one
516     *
517     * @param  string          $name
518     * @param  AbstractElement $field
519     * @return Form
520     */
521    public function insertFieldBefore(string $name, AbstractElement $field): Form
522    {
523        foreach ($this->fieldsets as $fieldset) {
524            if ($fieldset->hasField($name)) {
525                $fieldset->insertFieldBefore($name, $field);
526                break;
527            }
528        }
529        return $this;
530    }
531
532    /**
533     * Method to insert a field after another one
534     *
535     * @param  string          $name
536     * @param  AbstractElement $field
537     * @return Form
538     */
539    public function insertFieldAfter(string $name, AbstractElement $field): Form
540    {
541        foreach ($this->fieldsets as $fieldset) {
542            if ($fieldset->hasField($name)) {
543                $fieldset->insertFieldAfter($name, $field);
544                break;
545            }
546        }
547        return $this;
548    }
549
550    /**
551     * Method to get the count of elements in the form
552     *
553     * @return int
554     */
555    public function count(): int
556    {
557        $count = 0;
558        foreach ($this->fieldsets as $fieldset) {
559            $count += $fieldset->count();
560        }
561        return $count;
562    }
563
564    /**
565     * Method to get the field values as an array
566     *
567     * @return array
568     */
569    public function toArray(): array
570    {
571        $fieldValues = [];
572
573        foreach ($this->fieldsets as $fieldset) {
574            $fieldValues = array_merge($fieldValues, $fieldset->toArray());
575        }
576
577        return $fieldValues;
578    }
579
580    /**
581     * Method to get a field element object
582     *
583     * @param  string $name
584     * @return AbstractElement|null
585     */
586    public function getField(string $name): AbstractElement|null
587    {
588        $namedField = null;
589        $fields     = $this->getFields();
590
591        foreach ($fields as $field) {
592            if ($field->getName() == $name) {
593                $namedField = $field;
594                break;
595            }
596        }
597
598        return $namedField;
599    }
600
601    /**
602     * Method to get field element objects
603     *
604     * @return array
605     */
606    public function getFields(): array
607    {
608        $fields = [];
609
610        foreach ($this->fieldsets as $fieldset) {
611            $fields = array_merge($fields, $fieldset->getAllFields());
612        }
613
614        return $fields;
615    }
616
617    /**
618     * Has a field element object
619     *
620     * @param  string $name
621     * @return bool
622     */
623    public function hasField(string $name): bool
624    {
625        return ($this->getField($name) !== null);
626    }
627
628    /**
629     * Has fields
630     *
631     * @return bool
632     */
633    public function hasFields(): bool
634    {
635        return (!empty($this->getFields()));
636    }
637
638    /**
639     * Method to remove a form field
640     *
641     * @param  string $field
642     * @return Form
643     */
644    public function removeField(string $field): Form
645    {
646        foreach ($this->fieldsets as $fieldset) {
647            if ($fieldset->hasField($field)) {
648                unset($fieldset[$field]);
649            }
650        }
651        return $this;
652    }
653
654    /**
655     * Method to get a field element value
656     *
657     * @param  string $name
658     * @return mixed
659     */
660    public function getFieldValue(string $name): mixed
661    {
662        $fieldValues = $this->toArray();
663        return $fieldValues[$name] ?? null;
664    }
665
666    /**
667     * Method to set a field element value
668     *
669     * @param  string $name
670     * @param  mixed  $value
671     * @return Form
672     */
673    public function setFieldValue(string $name, mixed $value): Form
674    {
675        foreach ($this->fieldsets as $fieldset) {
676            if (isset($fieldset[$name])) {
677                $fieldset[$name] = $value;
678            }
679        }
680        return $this;
681    }
682
683    /**
684     * Method to set field element values
685     *
686     * @param  array $values
687     * @return Form
688     */
689    public function setFieldValues(array $values): Form
690    {
691        $fields = $this->toArray();
692        foreach ($fields as $name => $value) {
693            if (isset($values[$name]) && !($this->getField($name)->isButton())) {
694                $this->setFieldValue($name, $values[$name]);
695            } else if (!($this->getField($name)->isButton())) {
696                $this->getField($name)->resetValue();
697            }
698        }
699
700        $this->filter();
701
702        return $this;
703    }
704
705    /**
706     * Filter value with the filters in the form object
707     *
708     * @param  mixed $field
709     * @return mixed
710     */
711    public function filterValue(mixed $field): mixed
712    {
713        if ($field instanceof AbstractElement) {
714            $name      = $field->getName();
715            $type      = $field->getType();
716            $realValue = $field->getValue();
717        } else {
718            $type      = null;
719            $name      = null;
720            $realValue = $field;
721        }
722
723        foreach ($this->filters as $filter) {
724            if ($realValue !== null) {
725                $realValue = $filter->filter($realValue, $name, $type);
726            }
727        }
728
729        if (($field instanceof AbstractElement) && !($field instanceof Checkbox) &&
730            !($field instanceof Radio) && ($realValue !== null) && ($realValue != '')) {
731            $field->setValue($realValue);
732        }
733
734        return $realValue;
735    }
736
737    /**
738     * Filter values with the filters in the form object
739     *
740     * @param  mixed $values
741     * @return mixed
742     */
743    public function filter(mixed $values = null): mixed
744    {
745        if ($values === null) {
746            $values = $this->getFields();
747        }
748
749        if (is_array($values)) {
750            foreach ($values as $key => $value) {
751                $values[$key] = $this->filterValue($value);
752            }
753        } else {
754            $values = $this->filterValue($values);
755        }
756
757        return $values;
758    }
759
760    /**
761     * Determine whether or not the form object is valid
762     *
763     * @return bool
764     */
765    public function isValid(): bool
766    {
767        $result = true;
768        $fields = $this->getFields();
769        $values = $this->toArray();
770
771        // Check each element for validators, validate them and return the result.
772        foreach ($fields as $field) {
773            if ($field->validate($values) == false) {
774                $result = false;
775            }
776        }
777
778        return $result;
779    }
780
781    /**
782     * Get form element errors for a field.
783     *
784     * @param  string $name
785     * @return array
786     */
787    public function getErrors(string $name): array
788    {
789        $field  = $this->getField($name);
790        $errors = ($field !== null) ? $field->getErrors() : [];
791
792        return $errors;
793    }
794
795    /**
796     * Get all form element errors
797     *
798     * @return array
799     */
800    public function getAllErrors(): array
801    {
802        $errors = [];
803        $fields = $this->getFields();
804        foreach ($fields as $name => $field) {
805            if ($field->hasErrors()) {
806                $errors[str_replace('[]', '', $field->getName())] = $field->getErrors();
807            }
808        }
809
810        return $errors;
811    }
812
813    /**
814     * Method to reset and clear any form field values
815     *
816     * @return Form
817     */
818    public function reset(): Form
819    {
820        $fields = $this->getFields();
821        foreach ($fields as $field) {
822            $field->resetValue();
823        }
824        return $this;
825    }
826
827    /**
828     * Method to clear any security tokens
829     *
830     * @return Form
831     */
832    public function clearTokens(): Form
833    {
834        // Start a session.
835        if (session_id() == '') {
836            session_start();
837        }
838        if ($_SESSION) {
839            if (isset($_SESSION['pop_csrf'])) {
840                unset($_SESSION['pop_csrf']);
841            }
842            if (isset($_SESSION['pop_captcha'])) {
843                unset($_SESSION['pop_captcha']);
844            }
845        }
846
847        return $this;
848    }
849
850    /**
851     * Prepare form object for rendering
852     *
853     * @return Form
854     */
855    public function prepare(): Form
856    {
857        if ($this->getAttribute('id') === null) {
858            $this->setAttribute('id', 'pop-form');
859        }
860        if ($this->getAttribute('class') === null) {
861            $this->setAttribute('class', 'pop-form');
862        }
863
864        if (count($this->columns) > 0) {
865            foreach ($this->columns as $class => $fieldsets) {
866                $column = new Child('div');
867                $column->setAttribute('class', $class);
868                foreach ($fieldsets as $i) {
869                    if (isset($this->fieldsets[$i])) {
870                        $fieldset = $this->fieldsets[$i];
871                        $fieldset->prepare();
872                        $column->addChild($fieldset);
873                    }
874                }
875                $this->addChild($column);
876            }
877        } else {
878            foreach ($this->fieldsets as $fieldset) {
879                $fieldset->prepare();
880                $this->addChild($fieldset);
881            }
882        }
883
884        return $this;
885    }
886
887    /**
888     * Prepare form object for rendering with a view
889     *
890     * @return array
891     */
892    public function prepareForView(): array
893    {
894        $formData = [];
895
896        foreach ($this->fieldsets as $fieldset) {
897            $formData = array_merge($formData, $fieldset->prepareForView());
898        }
899
900        return $formData;
901    }
902
903    /**
904     * Render the form object
905     *
906     * @param  int     $depth
907     * @param  ?string $indent
908     * @param  bool    $inner
909     * @return string|null
910     */
911    public function render(int $depth = 0, ?string $indent = null, bool $inner = false): string|null
912    {
913        if (!($this->hasChildren())) {
914            $this->prepare();
915        }
916
917        foreach ($this->fieldsets as $fieldset) {
918            foreach ($fieldset->getAllFields() as $field) {
919                if ($field instanceof File) {
920                    $this->setAttribute('enctype', 'multipart/form-data');
921                    break;
922                }
923            }
924        }
925
926        return parent::render($depth, $indent, $inner);
927    }
928
929    /**
930     * Render and return the form object as a string
931     *
932     * @return string
933     */
934    public function __toString(): string
935    {
936        return $this->render();
937    }
938
939    /**
940     * Set method to set the property to the value of fields[$name]
941     *
942     * @param  string $name
943     * @param  mixed $value
944     * @return void
945     */
946    public function __set(string $name, mixed $value): void
947    {
948        $this->setFieldValue($name, $value);
949    }
950
951    /**
952     * Get method to return the value of fields[$name]
953     *
954     * @param  string $name
955     * @return mixed
956     */
957    public function __get(string $name): mixed
958    {
959        return $this->getFieldValue($name);
960    }
961
962    /**
963     * Return the isset value of fields[$name]
964     *
965     * @param  string $name
966     * @return bool
967     */
968    public function __isset(string $name): bool
969    {
970        $fieldValues = $this->toArray();
971        return isset($fieldValues[$name]);
972    }
973
974    /**
975     * Unset fields[$name]
976     *
977     * @param  string $name
978     * @return void
979     */
980    public function __unset(string $name): void
981    {
982        $fieldValues = $this->toArray();
983        if (isset($fieldValues[$name])) {
984            $this->getField($name)->resetValue();
985        }
986    }
987
988}