Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.72% covered (success)
95.72%
246 / 257
94.55% covered (success)
94.55%
52 / 55
CRAP
0.00% covered (danger)
0.00%
0 / 1
Form
95.72% covered (success)
95.72%
246 / 257
94.55% covered (success)
94.55%
52 / 55
144
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
45.45% covered (warning)
45.45%
5 / 11
0.00% covered (danger)
0.00%
0 / 1
11.84
 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 $name
198     * @param  mixed  $value
199     * @return Form
200     */
201    public function setAttribute(string $name, mixed $value = null): Form
202    {
203        parent::setAttribute($name, $value);
204
205        if ($name == 'id') {
206            foreach ($this->fieldsets as $i => $fieldset) {
207                $id = $value . '-fieldset-' . ($i + 1);
208                $fieldset->setAttribute('id', $id);
209            }
210
211        } else if ($name == 'class') {
212            foreach ($this->fieldsets as $i => $fieldset) {
213                $class = $value . '-fieldset';
214                $fieldset->setAttribute('class', $class);
215            }
216        }
217
218        return $this;
219    }
220
221    /**
222     * Method to set attributes
223     *
224     * @param  array $attributes
225     * @return Form
226     */
227    public function setAttributes(array $attributes): Form
228    {
229        foreach ($attributes 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     * @param  array $options
568     * @return array
569     */
570    public function toArray(array $options = []): array
571    {
572        $fieldValues = [];
573
574        foreach ($this->fieldsets as $fieldset) {
575            $fieldValues = array_merge($fieldValues, $fieldset->toArray());
576        }
577
578        if (!empty($options)) {
579            if (isset($options['exclude'])) {
580                if (!is_array($options['exclude'])) {
581                    $options['exclude'] = [$options['exclude']];
582                }
583                $fieldValues = array_diff_key($fieldValues, array_flip($options['exclude']));
584            }
585            if (isset($options['filter'])) {
586                $fieldValues = array_filter($fieldValues, $options['filter']);
587            }
588        }
589
590        return $fieldValues;
591    }
592
593    /**
594     * Method to get a field element object
595     *
596     * @param  string $name
597     * @return AbstractElement|null
598     */
599    public function getField(string $name): AbstractElement|null
600    {
601        $namedField = null;
602        $fields     = $this->getFields();
603
604        foreach ($fields as $field) {
605            if ($field->getName() == $name) {
606                $namedField = $field;
607                break;
608            }
609        }
610
611        return $namedField;
612    }
613
614    /**
615     * Method to get field element objects
616     *
617     * @return array
618     */
619    public function getFields(): array
620    {
621        $fields = [];
622
623        foreach ($this->fieldsets as $fieldset) {
624            $fields = array_merge($fields, $fieldset->getAllFields());
625        }
626
627        return $fields;
628    }
629
630    /**
631     * Has a field element object
632     *
633     * @param  string $name
634     * @return bool
635     */
636    public function hasField(string $name): bool
637    {
638        return ($this->getField($name) !== null);
639    }
640
641    /**
642     * Has fields
643     *
644     * @return bool
645     */
646    public function hasFields(): bool
647    {
648        return (!empty($this->getFields()));
649    }
650
651    /**
652     * Method to remove a form field
653     *
654     * @param  string $field
655     * @return Form
656     */
657    public function removeField(string $field): Form
658    {
659        foreach ($this->fieldsets as $fieldset) {
660            if ($fieldset->hasField($field)) {
661                unset($fieldset[$field]);
662            }
663        }
664        return $this;
665    }
666
667    /**
668     * Method to get a field element value
669     *
670     * @param  string $name
671     * @return mixed
672     */
673    public function getFieldValue(string $name): mixed
674    {
675        $fieldValues = $this->toArray();
676        return $fieldValues[$name] ?? null;
677    }
678
679    /**
680     * Method to set a field element value
681     *
682     * @param  string $name
683     * @param  mixed  $value
684     * @return Form
685     */
686    public function setFieldValue(string $name, mixed $value): Form
687    {
688        foreach ($this->fieldsets as $fieldset) {
689            if (isset($fieldset[$name])) {
690                $fieldset[$name] = $value;
691            }
692        }
693        return $this;
694    }
695
696    /**
697     * Method to set field element values
698     *
699     * @param  array $values
700     * @return Form
701     */
702    public function setFieldValues(array $values): Form
703    {
704        $fields = $this->toArray();
705        foreach ($fields as $name => $value) {
706            if (isset($values[$name]) && !($this->getField($name)->isButton())) {
707                $this->setFieldValue($name, $values[$name]);
708            } else if (!($this->getField($name)->isButton())) {
709                $this->getField($name)->resetValue();
710            }
711        }
712
713        $this->filter();
714
715        return $this;
716    }
717
718    /**
719     * Filter value with the filters in the form object
720     *
721     * @param  mixed $field
722     * @return mixed
723     */
724    public function filterValue(mixed $field): mixed
725    {
726        if ($field instanceof AbstractElement) {
727            $name      = $field->getName();
728            $type      = $field->getType();
729            $realValue = $field->getValue();
730        } else {
731            $type      = null;
732            $name      = null;
733            $realValue = $field;
734        }
735
736        foreach ($this->filters as $filter) {
737            if ($realValue !== null) {
738                $realValue = $filter->filter($realValue, $name, $type);
739            }
740        }
741
742        if (($field instanceof AbstractElement) && !($field instanceof Checkbox) &&
743            !($field instanceof Radio) && ($realValue !== null) && ($realValue != '')) {
744            $field->setValue($realValue);
745        }
746
747        return $realValue;
748    }
749
750    /**
751     * Filter values with the filters in the form object
752     *
753     * @param  mixed $values
754     * @return mixed
755     */
756    public function filter(mixed $values = null): mixed
757    {
758        if ($values === null) {
759            $values = $this->getFields();
760        }
761
762        if (is_array($values)) {
763            foreach ($values as $key => $value) {
764                $values[$key] = $this->filterValue($value);
765            }
766        } else {
767            $values = $this->filterValue($values);
768        }
769
770        return $values;
771    }
772
773    /**
774     * Determine whether or not the form object is valid
775     *
776     * @return bool
777     */
778    public function isValid(): bool
779    {
780        $result = true;
781        $fields = $this->getFields();
782        $values = $this->toArray();
783
784        // Check each element for validators, validate them and return the result.
785        foreach ($fields as $field) {
786            if ($field->validate($values) == false) {
787                $result = false;
788            }
789        }
790
791        return $result;
792    }
793
794    /**
795     * Get form element errors for a field.
796     *
797     * @param  string $name
798     * @return array
799     */
800    public function getErrors(string $name): array
801    {
802        $field  = $this->getField($name);
803        $errors = ($field !== null) ? $field->getErrors() : [];
804
805        return $errors;
806    }
807
808    /**
809     * Get all form element errors
810     *
811     * @return array
812     */
813    public function getAllErrors(): array
814    {
815        $errors = [];
816        $fields = $this->getFields();
817        foreach ($fields as $name => $field) {
818            if ($field->hasErrors()) {
819                $errors[str_replace('[]', '', $field->getName())] = $field->getErrors();
820            }
821        }
822
823        return $errors;
824    }
825
826    /**
827     * Method to reset and clear any form field values
828     *
829     * @return Form
830     */
831    public function reset(): Form
832    {
833        $fields = $this->getFields();
834        foreach ($fields as $field) {
835            $field->resetValue();
836        }
837        return $this;
838    }
839
840    /**
841     * Method to clear any security tokens
842     *
843     * @return Form
844     */
845    public function clearTokens(): Form
846    {
847        // Start a session.
848        if (session_id() == '') {
849            session_start();
850        }
851        if ($_SESSION) {
852            if (isset($_SESSION['pop_csrf'])) {
853                unset($_SESSION['pop_csrf']);
854            }
855            if (isset($_SESSION['pop_captcha'])) {
856                unset($_SESSION['pop_captcha']);
857            }
858        }
859
860        return $this;
861    }
862
863    /**
864     * Prepare form object for rendering
865     *
866     * @return Form
867     */
868    public function prepare(): Form
869    {
870        if ($this->getAttribute('id') === null) {
871            $this->setAttribute('id', 'pop-form');
872        }
873        if ($this->getAttribute('class') === null) {
874            $this->setAttribute('class', 'pop-form');
875        }
876
877        if (count($this->columns) > 0) {
878            foreach ($this->columns as $class => $fieldsets) {
879                $column = new Child('div');
880                $column->setAttribute('class', $class);
881                foreach ($fieldsets as $i) {
882                    if (isset($this->fieldsets[$i])) {
883                        $fieldset = $this->fieldsets[$i];
884                        $fieldset->prepare();
885                        $column->addChild($fieldset);
886                    }
887                }
888                $this->addChild($column);
889            }
890        } else {
891            foreach ($this->fieldsets as $fieldset) {
892                $fieldset->prepare();
893                $this->addChild($fieldset);
894            }
895        }
896
897        return $this;
898    }
899
900    /**
901     * Prepare form object for rendering with a view
902     *
903     * @return array
904     */
905    public function prepareForView(): array
906    {
907        $formData = [];
908
909        foreach ($this->fieldsets as $fieldset) {
910            $formData = array_merge($formData, $fieldset->prepareForView());
911        }
912
913        return $formData;
914    }
915
916    /**
917     * Render the form object
918     *
919     * @param  int     $depth
920     * @param  ?string $indent
921     * @param  bool    $inner
922     * @return string|null
923     */
924    public function render(int $depth = 0, ?string $indent = null, bool $inner = false): string|null
925    {
926        if (!($this->hasChildren())) {
927            $this->prepare();
928        }
929
930        foreach ($this->fieldsets as $fieldset) {
931            foreach ($fieldset->getAllFields() as $field) {
932                if ($field instanceof File) {
933                    $this->setAttribute('enctype', 'multipart/form-data');
934                    break;
935                }
936            }
937        }
938
939        return parent::render($depth, $indent, $inner);
940    }
941
942    /**
943     * Render and return the form object as a string
944     *
945     * @return string
946     */
947    public function __toString(): string
948    {
949        return $this->render();
950    }
951
952    /**
953     * Set method to set the property to the value of fields[$name]
954     *
955     * @param  string $name
956     * @param  mixed $value
957     * @return void
958     */
959    public function __set(string $name, mixed $value): void
960    {
961        $this->setFieldValue($name, $value);
962    }
963
964    /**
965     * Get method to return the value of fields[$name]
966     *
967     * @param  string $name
968     * @return mixed
969     */
970    public function __get(string $name): mixed
971    {
972        return $this->getFieldValue($name);
973    }
974
975    /**
976     * Return the isset value of fields[$name]
977     *
978     * @param  string $name
979     * @return bool
980     */
981    public function __isset(string $name): bool
982    {
983        $fieldValues = $this->toArray();
984        return isset($fieldValues[$name]);
985    }
986
987    /**
988     * Unset fields[$name]
989     *
990     * @param  string $name
991     * @return void
992     */
993    public function __unset(string $name): void
994    {
995        $fieldValues = $this->toArray();
996        if (isset($fieldValues[$name])) {
997            $this->getField($name)->resetValue();
998        }
999    }
1000
1001}