Passed
Pull Request — master (#192)
by Alexander
05:43 queued 02:47
created

Select   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 276
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 77
c 2
b 0
f 0
dl 0
loc 276
ccs 87
cts 87
cp 1
rs 9.84
wmc 32

18 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 items() 0 5 1
A multiple() 0 5 1
A prepareContainerTagAttributes() 0 7 2
A disabled() 0 5 1
A ariaLabel() 0 5 1
A tabIndex() 0 5 1
A autofocus() 0 5 1
A beforeRender() 0 8 5
B generateInput() 0 33 10
A ariaDescribedBy() 0 5 1
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
 * A control for selecting amongst a set of options.
21
 *
22
 * @link https://html.spec.whatwg.org/multipage/form-elements.html#the-select-element
23
 */
24
final class Select extends InputField implements EnrichmentFromRulesInterface, ValidationClassInterface
25
{
26
    use EnrichmentFromRulesTrait;
27
    use ValidationClassTrait;
28
29
    private SelectTag $select;
30
31 30
    public function __construct()
32
    {
33 30
        $this->select = SelectTag::tag();
34
    }
35
36 5
    public function items(Optgroup|Option ...$items): self
37
    {
38 5
        $new = clone $this;
39 5
        $new->select = $this->select->items(...$items);
40 5
        return $new;
41
    }
42
43 2
    public function options(Option ...$options): self
44
    {
45 2
        return $this->items(...$options);
46
    }
47
48
    /**
49
     * @param array $data Options data. The array keys are option values, and the array values are the corresponding
50
     * option labels. The array can also be nested (i.e. some array values are arrays too). For each sub-array,
51
     * an option group will be generated whose label is the key associated with the sub-array.
52
     *
53
     * Example:
54
     * ```php
55
     * [
56
     *     '1' => 'Santiago',
57
     *     '2' => 'Concepcion',
58
     *     '3' => 'Chillan',
59
     *     '4' => 'Moscow'
60
     *     '5' => 'San Petersburg',
61
     *     '6' => 'Novosibirsk',
62
     *     '7' => 'Ekaterinburg'
63
     * ];
64
     * ```
65
     *
66
     * Example with options groups:
67
     * ```php
68
     * [
69
     *     '1' => [
70
     *         '1' => 'Santiago',
71
     *         '2' => 'Concepcion',
72
     *         '3' => 'Chillan',
73
     *     ],
74
     *     '2' => [
75
     *         '4' => 'Moscow',
76
     *         '5' => 'San Petersburg',
77
     *         '6' => 'Novosibirsk',
78
     *         '7' => 'Ekaterinburg'
79
     *     ],
80
     * ];
81
     * ```
82
     * @param bool $encode Whether option content should be HTML-encoded.
83
     * @param array[] $optionsAttributes Array of option attribute sets indexed by option values from {@see $data}.
84
     * @param array[] $groupsAttributes Array of group attribute sets indexed by group labels from {@see $data}.
85
     *
86
     * @psalm-param array<array-key, string|array<array-key,string>> $data
87
     *
88
     * @return self
89
     */
90 24
    public function optionsData(
91
        array $data,
92
        bool $encode = true,
93
        array $optionsAttributes = [],
94
        array $groupsAttributes = []
95
    ): self {
96 24
        $new = clone $this;
97 24
        $new->select = $this->select->optionsData($data, $encode, $optionsAttributes, $groupsAttributes);
98 24
        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->inputTagAttributes['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->inputTagAttributes['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->inputTagAttributes['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->inputTagAttributes['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->inputTagAttributes['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->inputTagAttributes['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->inputTagAttributes['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->inputTagAttributes['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 Remove after fix https://github.com/yiisoft/validator/issues/225
244
     */
245 29
    protected function beforeRender(): void
246
    {
247 29
        parent::beforeRender();
248 29
        if ($this->enrichmentFromRules && $this->hasFormModelAndAttribute()) {
249 1
            $rules = $this->getFormModel()->getRules()[$this->getAttributeName()] ?? [];
250 1
            foreach ($rules as $rule) {
251 1
                if ($rule instanceof Required) {
252 1
                    $this->inputTagAttributes['required'] = true;
253
                }
254
            }
255
        }
256
    }
257
258 29
    protected function generateInput(): string
259
    {
260 29
        $value = $this->getAttributeValue();
261 29
        $multiple = (bool) ($this->inputTagAttributes['multiple'] ?? false);
262
263 29
        if ($multiple) {
264
            /** @var mixed $value */
265 3
            $value ??= [];
266 3
            if (!is_iterable($value)) {
267 1
                throw new InvalidArgumentException(
268
                    'Select field with multiple option requires iterable or null value.'
269
                );
270
            }
271
        } else {
272 26
            if (!is_bool($value)
273 26
                && !is_string($value)
274 26
                && !is_numeric($value)
275 26
                && $value !== null
276 26
                && (!is_object($value) || !method_exists($value, '__toString'))
277
            ) {
278 1
                throw new InvalidArgumentException(
279
                    'Non-multiple Select field requires a string, numeric, bool, Stringable or null value.'
280
                );
281
            }
282 25
            $value = $value === null ? [] : [$value];
283
        }
284
        /** @psalm-var iterable<int, Stringable|scalar> $value */
285
286 27
        return $this->select
287 27
            ->attributes($this->inputTagAttributes)
288 27
            ->name($this->getInputName())
289 27
            ->values($value)
290 27
            ->render();
291
    }
292
293 4
    protected function prepareContainerTagAttributes(array &$attributes): void
294
    {
295 4
        if ($this->hasFormModelAndAttribute()) {
296 4
            $this->addValidationClassToTagAttributes(
297
                $attributes,
298 4
                $this->getFormModel(),
299 4
                $this->getAttributeName(),
300
            );
301
        }
302
    }
303
}
304