Passed
Pull Request — master (#275)
by Sergei
02:49
created

Select::beforeRender()   A

Complexity

Conditions 6
Paths 5

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 6

Importance

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