Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
134 / 134
100.00% covered (success)
100.00%
11 / 11
CRAP
100.00% covered (success)
100.00%
1 / 1
AbstractSelect
100.00% covered (success)
100.00%
134 / 134
100.00% covered (success)
100.00%
11 / 11
51
100.00% covered (success)
100.00%
1 / 1
 setRequired
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 setDisabled
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 setReadonly
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 getType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getValue
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSelected
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOptions
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
6
 getOptionsAsArray
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 validate
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 parseValues
100.00% covered (success)
100.00%
79 / 79
100.00% covered (success)
100.00%
1 / 1
23
 parseXml
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
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\Element;
16
17use SimpleXMLElement;
18
19/**
20 * Abstract select element class
21 *
22 * @category   Pop
23 * @package    Pop\Form
24 * @author     Nick Sagona, III <nick@popphp.org>
25 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
26 * @license    https://www.popphp.org/license     New BSD License
27 * @version    5.0.0
28 */
29
30abstract class AbstractSelect extends AbstractElement
31{
32
33    /**
34     * Constant for months, short
35     * @var string
36     */
37    const MONTHS_SHORT = 'MONTHS_SHORT';
38
39    /**
40     * Constant for days of the month
41     * @var string
42     */
43    const DAYS_OF_MONTH = 'DAYS_OF_MONTH';
44
45    /**
46     * Constant for 12 hours
47     * @var string
48     */
49    const HOURS_12 = 'HOURS_12';
50
51    /**
52     * Constant for 24 hours
53     * @var string
54     */
55    const HOURS_24 = 'HOURS_24';
56
57    /**
58     * Constant for 60 minutes (0-59)
59     * @var string
60     */
61    const MINUTES = 'MINUTES';
62
63    /**
64     * Constant for minutes in increments of 5
65     * @var string
66     */
67    const MINUTES_5 = 'MINUTES_5';
68
69    /**
70     * Constant for minutes in increments of 10
71     * @var string
72     */
73    const MINUTES_10 = 'MINUTES_10';
74
75    /**
76     * Constant for minutes in increments of 15
77     * @var string
78     */
79    const MINUTES_15 = 'MINUTES_15';
80
81    /**
82     * Selected value(s)
83     * @var mixed
84     */
85    protected mixed $selected = null;
86
87    /**
88     * Set whether the form element is required
89     *
90     * @param  bool    $required
91     * @param  ?string $requiredMessage
92     * @return AbstractSelect
93     */
94    public function setRequired(bool $required, ?string $requiredMessage = 'This field is required.'): AbstractSelect
95    {
96        if ($required) {
97            $this->setAttribute('required', 'required');
98        } else {
99            $this->removeAttribute('required');
100        }
101        parent::setRequired($required, $requiredMessage);
102        return $this;
103    }
104
105    /**
106     * Set whether the form element is disabled
107     *
108     * @param  bool $disabled
109     * @return AbstractSelect
110     */
111    public function setDisabled(bool $disabled): AbstractSelect
112    {
113        if ($disabled) {
114            $this->setAttribute('disabled', 'disabled');
115        } else {
116            $this->removeAttribute('disabled');
117        }
118        parent::setDisabled($disabled);
119        return $this;
120    }
121
122    /**
123     * Set whether the form element is readonly
124     *
125     * @param  bool $readonly
126     * @return AbstractSelect
127     */
128    public function setReadonly(bool $readonly): AbstractSelect
129    {
130        if ($readonly) {
131            $this->setAttribute('readonly', 'readonly');
132            foreach ($this->childNodes as $childNode) {
133                if ($childNode->getAttribute('selected') != 'selected') {
134                    $childNode->setAttribute('disabled', 'disabled');
135                } else {
136                    $childNode->setAttribute('readonly', 'readonly');
137                }
138            }
139        } else {
140            $this->removeAttribute('readonly');
141            foreach ($this->childNodes as $childNode) {
142                $childNode->removeAttribute('disabled');
143                $childNode->removeAttribute('readonly');
144            }
145        }
146
147        parent::setReadonly($readonly);
148        return $this;
149    }
150
151    /**
152     * Get form element object type
153     *
154     * @return string
155     */
156    public function getType(): string
157    {
158        return 'select';
159    }
160
161    /**
162     * Get select form element selected value
163     *
164     * @return mixed
165     */
166    public function getValue(): mixed
167    {
168        return $this->selected;
169    }
170
171    /**
172     * Get select form element selected value (alias)
173     *
174     * @return mixed
175     */
176    public function getSelected(): mixed
177    {
178        return $this->getValue();
179    }
180
181    /**
182     * Get select options
183     *
184     * @return array
185     */
186    public function getOptions(): array
187    {
188        $options = [];
189
190        foreach ($this->childNodes as $child) {
191            if ($child instanceof Select\Option) {
192                $options[] = $child;
193            } else if ($child instanceof Select\Optgroup) {
194                foreach ($child->getChildren() as $c) {
195                    if ($c instanceof Select\Option) {
196                        $options[] = $c;
197                    }
198                }
199            }
200        }
201
202        return $options;
203    }
204
205    /**
206     * Get select options as array
207     *
208     * @return array
209     */
210    public function getOptionsAsArray(): array
211    {
212        $options      = $this->getOptions();
213        $optionsArray = [];
214
215        foreach ($options as $option) {
216            $optionsArray[$option->getValue()] = $option->getNodeValue();
217        }
218
219        return $optionsArray;
220    }
221
222    /**
223     * Validate the form element object
224     *
225     * @param  array $formValues
226     * @return bool
227     */
228    public function validate(array $formValues = []): bool
229    {
230        $value = $this->getValue();
231
232        // Check if the element is required
233        if (($this->required) && empty($value)) {
234            $this->errors[] = $this->getRequiredMessage();
235        }
236
237        $this->validateValue($value, $formValues);
238
239        return (count($this->errors) == 0);
240    }
241
242    /**
243     * Set the select element as multiple
244     *
245     * @param  string|array $values
246     * @param  ?string      $xmlFile
247     * @return array
248     */
249    public static function parseValues(string|array $values, ?string $xmlFile = null): array
250    {
251        $parsedValues = null;
252
253        // If the values are an array of values already
254        if (is_array($values)) {
255            $parsedValues = $values;
256        // Else, if the value is a string
257        } else {
258            // If the value flag is YEAR-based, calculate the year range for the select drop-down menu.
259            if (str_contains($values, 'YEAR')) {
260                $years = [];
261                $yearAry = explode('_', $values);
262                // YEAR_1111_2222 (from year 1111 to 2222)
263                if (isset($yearAry[1]) && isset($yearAry[2])) {
264                    if ($yearAry[1] < $yearAry[2]) {
265                        for ($i = $yearAry[1]; $i <= $yearAry[2]; $i++) {
266                            $years[$i] = $i;
267                        }
268                    } else {
269                        for ($i = $yearAry[1]; $i >= $yearAry[2]; $i--) {
270                            $years[$i] = $i;
271                        }
272                    }
273                // YEAR_1111
274                // If 1111 is less than today's year, then 1111 to present year,
275                // else from present year to 1111
276                } else if (isset($yearAry[1])) {
277                    $year = date('Y');
278                    if ($year < $yearAry[1]) {
279                        for ($i = $year; $i <= $yearAry[1]; $i++) {
280                            $years[$i] = $i;
281                        }
282                    } else {
283                        for ($i = $year; $i >= $yearAry[1]; $i--) {
284                            $years[$i] = $i;
285                        }
286                    }
287                // YEAR, from present year to 10+ years
288                } else {
289                    $year = date('Y');
290                    for ($i = $year; $i <= ($year + 10); $i++) {
291                        $years[$i] = $i;
292                    }
293                }
294                $parsedValues = $years;
295            } else {
296                // Else, if the value flag is one of the pre-defined , set the value of the select drop-down menu to it.
297                switch ($values) {
298                    // Hours, 12-hour values.
299                    // Months, numeric short values.
300                    case Select::HOURS_12:
301                    case Select::MONTHS_SHORT:
302                        $parsedValues = [
303                            '01' => '01', '02' => '02', '03' => '03', '04' => '04', '05' => '05', '06' => '06',
304                            '07' => '07', '08' => '08', '09' => '09', '10' => '10', '11' => '11', '12' => '12'
305                        ];
306                        break;
307                    // Days of Month, numeric short values.
308                    case Select::DAYS_OF_MONTH:
309                        $parsedValues = [
310                            '01' => '01', '02' => '02', '03' => '03', '04' => '04', '05' => '05',
311                            '06' => '06', '07' => '07', '08' => '08', '09' => '09', '10' => '10', '11' => '11',
312                            '12' => '12', '13' => '13', '14' => '14', '15' => '15', '16' => '16', '17' => '17',
313                            '18' => '18', '19' => '19', '20' => '20', '21' => '21', '22' => '22', '23' => '23',
314                            '24' => '24', '25' => '25', '26' => '26', '27' => '27', '28' => '28', '29' => '29',
315                            '30' => '30', '31' => '31'
316                        ];
317                        break;
318                    // Military hours, 24-hour values.
319                    case Select::HOURS_24:
320                        $parsedValues = [
321                            '00' => '00', '01' => '01', '02' => '02', '03' => '03', '04' => '04', '05' => '05',
322                            '06' => '06', '07' => '07', '08' => '08', '09' => '09', '10' => '10', '11' => '11', '12' => '12',
323                            '13' => '13', '14' => '14', '15' => '15', '16' => '16', '17' => '17', '18' => '18', '19' => '19',
324                            '20' => '20', '21' => '21', '22' => '22', '23' => '23'
325                        ];
326                        break;
327                    // Minutes, incremental by 1 minute.
328                    case Select::MINUTES:
329                        $parsedValues = [
330                            '00' => '00', '01' => '01', '02' => '02', '03' => '03', '04' => '04', '05' => '05',
331                            '06' => '06', '07' => '07', '08' => '08', '09' => '09', '10' => '10', '11' => '11', '12' => '12',
332                            '13' => '13', '14' => '14', '15' => '15', '16' => '16', '17' => '17', '18' => '18', '19' => '19',
333                            '20' => '20', '21' => '21', '22' => '22', '23' => '23', '24' => '24', '25' => '25', '26' => '26',
334                            '27' => '27', '28' => '28', '29' => '29', '30' => '30', '31' => '31', '32' => '32', '33' => '33',
335                            '34' => '34', '35' => '35', '36' => '36', '37' => '37', '38' => '38', '39' => '39', '40' => '40',
336                            '41' => '41', '42' => '42', '43' => '43', '44' => '44', '45' => '45', '46' => '46', '47' => '47',
337                            '48' => '48', '49' => '49', '50' => '50', '51' => '51', '52' => '52', '53' => '53', '54' => '54',
338                            '55' => '55', '56' => '56', '57' => '57', '58' => '58', '59' => '59'
339                        ];
340                        break;
341                    // Minutes, incremental by 5 minutes.
342                    case Select::MINUTES_5:
343                        $parsedValues = [
344                            '00' => '00', '05' => '05', '10' => '10', '15' => '15', '20' => '20', '25' => '25',
345                            '30' => '30', '35' => '35', '40' => '40', '45' => '45', '50' => '50', '55' => '55'
346                        ];
347                        break;
348                    // Minutes, incremental by 10 minutes.
349                    case Select::MINUTES_10:
350                        $parsedValues = [
351                            '00' => '00', '10' => '10', '20' => '20', '30' => '30', '40' => '40', '50' => '50'
352                        ];
353                        break;
354                    // Minutes, incremental by 15 minutes.
355                    case Select::MINUTES_15:
356                        $parsedValues = ['00' => '00', '15' => '15', '30' => '30', '45' => '45'];
357                        break;
358                    // Else, set the custom array of values passed.
359                    default:
360                        if ($xmlFile === null) {
361                            $xmlFile = __DIR__ . DIRECTORY_SEPARATOR . 'Data' . DIRECTORY_SEPARATOR . 'options.xml';
362                        }
363                        $parsedValues = self::parseXml($xmlFile, $values);
364                }
365            }
366        }
367
368        return $parsedValues;
369    }
370
371    /**
372     * Static method to parse an XML file of options
373     *
374     * @param string $xmlFile
375     * @param string $name
376     * @throws \Exception
377     * @return array
378     */
379    protected static function parseXml(string $xmlFile, string $name): array
380    {
381        $options = [];
382
383        if (file_exists($xmlFile)) {
384            $xml = new SimpleXMLElement($xmlFile, 0, true);
385            $xmlValues = [];
386            foreach ($xml->set as $node) {
387                $xmlValues[(string)$node->attributes()->name] = [];
388                foreach ($node->opt as $opt) {
389                    $xmlValues[(string)$node->attributes()->name][(string)$opt->attributes()->value] = (string)$opt;
390                }
391            }
392            if (array_key_exists($name, $xmlValues)) {
393                $options = $xmlValues[$name];
394            }
395        }
396
397        return $options;
398    }
399
400}