Passed
Push — master ( 94344c...86ce1f )
by Alexander
07:20 queued 04:31
created

Select   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 289
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 83
c 1
b 0
f 0
dl 0
loc 289
ccs 93
cts 93
cp 1
rs 9.68
wmc 34

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A prompt() 0 5 1
A size() 0 5 1
A unselectValue() 0 5 1
A options() 0 3 1
A optionsData() 0 9 1
A required() 0 5 1
A promptOption() 0 5 1
A beforeRender() 0 8 5
B generateInput() 0 35 10
A items() 0 5 1
A multiple() 0 5 1
A disabled() 0 5 1
A ariaLabel() 0 5 1
A tabIndex() 0 5 1
A prepareContainerAttributes() 0 7 2
A autofocus() 0 5 1
A ariaDescribedBy() 0 5 1
A prepareInputAttributes() 0 7 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Form\Field;
6
7
use InvalidArgumentException;
8
use Stringable;
9
use Yiisoft\Form\Field\Base\EnrichmentFromRules\EnrichmentFromRulesInterface;
10
use Yiisoft\Form\Field\Base\EnrichmentFromRules\EnrichmentFromRulesTrait;
11
use Yiisoft\Form\Field\Base\InputField;
12
use Yiisoft\Form\Field\Base\ValidationClass\ValidationClassInterface;
13
use Yiisoft\Form\Field\Base\ValidationClass\ValidationClassTrait;
14
use Yiisoft\Html\Tag\Optgroup;
15
use Yiisoft\Html\Tag\Option;
16
use Yiisoft\Html\Tag\Select as SelectTag;
17
use Yiisoft\Validator\Rule\Required;
18
19
/**
20
 * Represents `<select>` element that provides a menu of options.
21
 *
22
 * @link https://html.spec.whatwg.org/multipage/form-elements.html#the-select-element
23
 * @link https://developer.mozilla.org/docs/Web/HTML/Element/select
24
 */
25
final class Select extends InputField implements EnrichmentFromRulesInterface, ValidationClassInterface
26
{
27
    use EnrichmentFromRulesTrait;
28
    use ValidationClassTrait;
29
30
    private SelectTag $select;
31
32 30
    public function __construct()
33
    {
34 30
        $this->select = SelectTag::tag();
35
    }
36
37 5
    public function items(Optgroup|Option ...$items): self
38
    {
39 5
        $new = clone $this;
40 5
        $new->select = $this->select->items(...$items);
41 5
        return $new;
42
    }
43
44 2
    public function options(Option ...$options): self
45
    {
46 2
        return $this->items(...$options);
47
    }
48
49
    /**
50
     * @param array $data Options data. The array keys are option values, and the array values are the corresponding
51
     * option labels. The array can also be nested (i.e. some array values are arrays too). For each sub-array,
52
     * an option group will be generated whose label is the key associated with the sub-array.
53
     *
54
     * Example:
55
     * ```php
56
     * [
57
     *     '1' => 'Santiago',
58
     *     '2' => 'Concepcion',
59
     *     '3' => 'Chillan',
60
     *     '4' => 'Moscow'
61
     *     '5' => 'San Petersburg',
62
     *     '6' => 'Novosibirsk',
63
     *     '7' => 'Ekaterinburg'
64
     * ];
65
     * ```
66
     *
67
     * Example with options groups:
68
     * ```php
69
     * [
70
     *     '1' => [
71
     *         '1' => 'Santiago',
72
     *         '2' => 'Concepcion',
73
     *         '3' => 'Chillan',
74
     *     ],
75
     *     '2' => [
76
     *         '4' => 'Moscow',
77
     *         '5' => 'San Petersburg',
78
     *         '6' => 'Novosibirsk',
79
     *         '7' => 'Ekaterinburg'
80
     *     ],
81
     * ];
82
     * ```
83
     * @param bool $encode Whether option content should be HTML-encoded.
84
     * @param array[] $optionsAttributes Array of option attribute sets indexed by option values from {@see $data}.
85
     * @param array[] $groupsAttributes Array of group attribute sets indexed by group labels from {@see $data}.
86
     *
87
     * @psalm-param array<array-key, string|array<array-key,string>> $data
88
     *
89
     * @return self
90
     */
91 24
    public function optionsData(
92
        array $data,
93
        bool $encode = true,
94
        array $optionsAttributes = [],
95
        array $groupsAttributes = []
96
    ): self {
97 24
        $new = clone $this;
98 24
        $new->select = $this->select->optionsData($data, $encode, $optionsAttributes, $groupsAttributes);
99 24
        return $new;
100
    }
101
102
    /**
103
     * @param bool $disabled Whether select input is disabled.
104
     *
105
     * @link https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fe-disabled
106
     */
107 2
    public function disabled(bool $disabled = true): self
108
    {
109 2
        $new = clone $this;
110 2
        $new->inputAttributes['disabled'] = $disabled;
111 2
        return $new;
112
    }
113
114
    /**
115
     * Identifies the element (or elements) that describes the object.
116
     *
117
     * @link https://w3c.github.io/aria/#aria-describedby
118
     */
119 2
    public function ariaDescribedBy(?string $value): self
120
    {
121 2
        $new = clone $this;
122 2
        $new->inputAttributes['aria-describedby'] = $value;
123 2
        return $new;
124
    }
125
126
    /**
127
     * Defines a string value that labels the current element.
128
     *
129
     * @link https://w3c.github.io/aria/#aria-label
130
     */
131 2
    public function ariaLabel(?string $value): self
132
    {
133 2
        $new = clone $this;
134 2
        $new->inputAttributes['aria-label'] = $value;
135 2
        return $new;
136
    }
137
138
    /**
139
     * Focus on the control (put cursor into it) when the page loads. Only one form element could be in focus
140
     * at the same time.
141
     *
142
     * @link https://html.spec.whatwg.org/multipage/interaction.html#attr-fe-autofocus
143
     */
144 2
    public function autofocus(bool $value = true): self
145
    {
146 2
        $new = clone $this;
147 2
        $new->inputAttributes['autofocus'] = $value;
148 2
        return $new;
149
    }
150
151
    /**
152
     * The `tabindex` attribute indicates that its element can be focused, and where it participates in sequential
153
     * keyboard navigation (usually with the Tab key, hence the name).
154
     *
155
     * It accepts an integer as a value, with different results depending on the integer's value:
156
     *
157
     * - A negative value (usually `tabindex="-1"`) means that the element is not reachable via sequential keyboard
158
     *   navigation, but could be focused with Javascript or visually. It's mostly useful to create accessible widgets
159
     *   with JavaScript.
160
     * - `tabindex="0"` means that the element should be focusable in sequential keyboard navigation, but its order is
161
     *   defined by the document's source order.
162
     * - A positive value means the element should be focusable in sequential keyboard navigation, with its order
163
     *   defined by the value of the number. That is, `tabindex="4"` is focused before `tabindex="5"`, but after
164
     *   `tabindex="3"`.
165
     *
166
     * @link https://html.spec.whatwg.org/multipage/interaction.html#attr-tabindex
167
     */
168 2
    public function tabIndex(?int $value): self
169
    {
170 2
        $new = clone $this;
171 2
        $new->inputAttributes['tabindex'] = $value;
172 2
        return $new;
173
    }
174
175
    /**
176
     * @param bool $value Whether the user is to be allowed to select zero or more options.
177
     *
178
     * @link https://html.spec.whatwg.org/multipage/form-elements.html#attr-select-multiple
179
     */
180 4
    public function multiple(bool $value = true): self
181
    {
182 4
        $new = clone $this;
183 4
        $new->inputAttributes['multiple'] = $value;
184 4
        return $new;
185
    }
186
187
    /**
188
     * @param string|null $text Text of the option that has dummy value and is rendered as an invitation to select
189
     * a value.
190
     */
191 3
    public function prompt(?string $text): self
192
    {
193 3
        $new = clone $this;
194 3
        $new->select = $this->select->prompt($text);
195 3
        return $new;
196
    }
197
198
    /**
199
     * @param Option|null $option Option that has dummy value and is rendered as an invitation to select a value.
200
     */
201 3
    public function promptOption(?Option $option): self
202
    {
203 3
        $new = clone $this;
204 3
        $new->select = $this->select->promptOption($option);
205 3
        return $new;
206
    }
207
208
    /**
209
     * A boolean attribute. When specified, the element is required.
210
     *
211
     * @param bool $value Whether the control is required for form submission.
212
     *
213
     * @link https://html.spec.whatwg.org/multipage/form-elements.html#attr-select-required
214
     */
215 2
    public function required(bool $value = true): self
216
    {
217 2
        $new = clone $this;
218 2
        $new->inputAttributes['required'] = $value;
219 2
        return $new;
220
    }
221
222
    /**
223
     * The size of the control.
224
     *
225
     * @param int|null $value The number of options to show to the user.
226
     *
227
     * @link https://html.spec.whatwg.org/multipage/form-elements.html#attr-select-size
228
     */
229 2
    public function size(?int $value): self
230
    {
231 2
        $new = clone $this;
232 2
        $new->inputAttributes['size'] = $value;
233 2
        return $new;
234
    }
235
236 2
    public function unselectValue(bool|float|int|string|Stringable|null $value): self
237
    {
238 2
        $new = clone $this;
239 2
        $new->select = $this->select->unselectValue($value);
240 2
        return $new;
241
    }
242
243
    /**
244
     * @psalm-suppress MixedAssignment,MixedArgument Remove after fix https://github.com/yiisoft/validator/issues/225
245
     */
246 29
    protected function beforeRender(): void
247
    {
248 29
        parent::beforeRender();
249 29
        if ($this->enrichmentFromRules && $this->hasFormModelAndAttribute()) {
250 1
            $rules = $this->getFormModel()->getRules()[$this->getFormAttributeName()] ?? [];
251 1
            foreach ($rules as $rule) {
252 1
                if ($rule instanceof Required) {
253 1
                    $this->inputAttributes['required'] = true;
254
                }
255
            }
256
        }
257
    }
258
259 29
    protected function generateInput(): string
260
    {
261 29
        $value = $this->getFormAttributeValue();
262 29
        $multiple = (bool) ($this->inputAttributes['multiple'] ?? false);
263
264 29
        if ($multiple) {
265
            /** @var mixed $value */
266 3
            $value ??= [];
267 3
            if (!is_iterable($value)) {
268 1
                throw new InvalidArgumentException(
269
                    'Select field with multiple option requires iterable or null value.'
270
                );
271
            }
272
        } else {
273 26
            if (!is_bool($value)
274 26
                && !is_string($value)
275 26
                && !is_numeric($value)
276 26
                && $value !== null
277 26
                && (!is_object($value) || !method_exists($value, '__toString'))
278
            ) {
279 1
                throw new InvalidArgumentException(
280
                    'Non-multiple Select field requires a string, numeric, bool, Stringable or null value.'
281
                );
282
            }
283 25
            $value = $value === null ? [] : [$value];
284
        }
285
        /** @psalm-var iterable<int, Stringable|scalar> $value */
286
287 27
        $selectAttributes = $this->getInputAttributes();
288
289 27
        return $this->select
290 27
            ->attributes($selectAttributes)
291 27
            ->name($this->getInputName())
292 27
            ->values($value)
293 27
            ->render();
294
    }
295
296 4
    protected function prepareContainerAttributes(array &$attributes): void
297
    {
298 4
        if ($this->hasFormModelAndAttribute()) {
299 4
            $this->addValidationClassToAttributes(
300
                $attributes,
301 4
                $this->getFormModel(),
302 4
                $this->getFormAttributeName(),
303
            );
304
        }
305
    }
306
307 27
    protected function prepareInputAttributes(array &$attributes): void
308
    {
309 27
        if ($this->hasFormModelAndAttribute()) {
310 27
            $this->addInputValidationClassToAttributes(
311
                $attributes,
312 27
                $this->getFormModel(),
313 27
                $this->getFormAttributeName(),
314
            );
315
        }
316
    }
317
}
318