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