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