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