Passed
Push — rename-getters ( eab17b )
by Dmitriy
02:49
created

Field   F

Complexity

Total Complexity 72

Size/Duplication

Total Lines 955
Duplicated Lines 0 %

Test Coverage

Coverage 91.95%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 297
c 1
b 0
f 0
dl 0
loc 955
ccs 297
cts 323
cp 0.9195
rs 2.64
wmc 72

41 Methods

Rating   Name   Duplication   Size   Complexity  
A requiredCssClass() 0 5 1
A input() 0 19 1
A renderEnd() 0 3 2
B run() 0 29 7
A checkbox() 0 17 2
A renderBegin() 0 17 1
A errorCssClass() 0 5 1
A label() 0 30 4
A setAriaAttributes() 0 5 4
A inputOptions() 0 13 2
A addErrorCssClassToInput() 0 10 3
A error() 0 15 1
A hintOptions() 0 13 2
A passwordInput() 0 18 1
A listBox() 0 14 1
A checkboxList() 0 15 1
A config() 0 6 1
A setForInLabel() 0 7 3
A textInput() 0 18 1
A template() 0 5 1
A radio() 0 18 2
A enclosedByContainer() 0 6 1
A setInputRole() 0 3 1
A errorSummaryCssClass() 0 5 1
A validationStateOn() 0 5 1
A textArea() 0 18 1
A fileInput() 0 20 1
A addSuccessCssClassToInput() 0 10 4
A errorOptions() 0 13 2
A dropDownList() 0 19 1
A addErrorCssClassToContainer() 0 7 2
A addInputId() 0 3 2
A successCssClass() 0 5 1
A inputCssClass() 0 5 1
A radioList() 0 19 1
A validatingCssClass() 0 5 1
A ariaAttribute() 0 5 1
A hiddenInput() 0 18 1
A skipForInLabel() 0 4 2
A labelOptions() 0 13 2
A hint() 0 25 3

How to fix   Complexity   

Complex Class

Complex classes like Field often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Field, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Form\Widget;
6
7
use InvalidArgumentException;
8
use Yiisoft\Arrays\ArrayHelper;
9
use Yiisoft\Factory\Exceptions\InvalidConfigException;
10
use Yiisoft\Form\FormModelInterface;
11
use Yiisoft\Form\Helper\HtmlForm;
12
use Yiisoft\Html\Html;
13
use Yiisoft\Widget\Widget;
14
use function array_merge;
15
use function strtr;
16
17
/**
18
 * Renders the field widget along with label, error tag and hint tag (if any) according to template.
19
 */
20
final class Field extends Widget
21
{
22
    private const DEFAULT_CONTAINER_OPTIONS = ['class' => 'form-group'];
23
    private const DEFAULT_ERROR_OPTIONS = ['class' => 'help-block'];
24
    private const DEFAULT_HINT_OPTIONS = ['class' => 'hint-block'];
25
    private const DEFAULT_LABEL_OPTIONS = ['class' => 'control-label'];
26
27
    private FormModelInterface $data;
28
    private string $attribute;
29
    private array $options = [];
30
    private array $inputOptions = [];
31
    private array $errorOptions = [];
32
    private array $hintOptions = [];
33
    private array $labelOptions = [];
34
    private bool $ariaAttribute = true;
35
    private string $errorCssClass = 'has-error';
36
    private string $errorSummaryCssClass = 'error-summary';
37
    private string $inputCssClass = 'form-control';
38
    private string $requiredCssClass = 'required';
39
    private string $successCssClass = 'has-success';
40
    private string $template = "{label}\n{input}\n{hint}\n{error}";
41
    private string $validatingCssClass = 'validating';
42
    private string $validationStateOn = 'input';
43
    private ?string $inputId = null;
44
    private array $parts = [];
45
    private bool $skipForInLabel = false;
46
    private bool $containerEnabled = true;
47
48
    /**
49
     * Renders the whole field.
50
     *
51
     * This method will generate the label, error tag, input tag and hint tag (if any), and assemble them into HTML
52
     * according to {@see template}.
53
     *
54
     * @param string|null $content the content within the field container.
55
     *
56
     * If `null` (not set), the default methods will be called to generate the label, error tag and input tag, and use
57
     * them as the content.
58
     *
59
     * @throws InvalidConfigException
60
     *
61
     * @return string the rendering result.
62
     */
63 65
    public function run(?string $content = null): string
64
    {
65 65
        if ($content === null) {
66 65
            if (!isset($this->parts['{input}'])) {
67 26
                $this->textInput();
68
            }
69
70 65
            if (!isset($this->parts['{label}'])) {
71 31
                $this->label();
72
            }
73
74 65
            if (!isset($this->parts['{error}'])) {
75 62
                $this->error();
76
            }
77
78 65
            if (!isset($this->parts['{hint}'])) {
79 62
                $this->hint();
80
            }
81
82 65
            $content = strtr($this->template, $this->parts);
83
        } else {
84
            $content = $content($this);
85
        }
86
87 65
        if ($this->containerEnabled) {
88 64
            return $this->renderBegin() . "\n" . $content . "\n" . $this->renderEnd();
89
        }
90
91 1
        return $content;
92
    }
93
94
    /**
95
     * Renders the opening tag of the field container.
96
     *
97
     * @throws InvalidArgumentException
98
     *
99
     * @return string the rendering result.
100
     */
101 64
    private function renderBegin(): string
102
    {
103 64
        $new = clone $this;
104
105 64
        $inputId = $new->addInputId();
106
107 64
        $class = [];
108 64
        $class[] = "field-$inputId";
109 64
        $class[] = $new->options['class'] ?? '';
110
111 64
        $new->options['class'] = trim(implode(' ', array_merge(self::DEFAULT_CONTAINER_OPTIONS, $class)));
112
113 64
        $new->addErrorCssClassToContainer();
114
115 64
        $tag = ArrayHelper::remove($new->options, 'tag', 'div');
116
117 64
        return Html::beginTag($tag, $new->options);
118
    }
119
120
    /**
121
     * Renders the closing tag of the field container.
122
     *
123
     * @return string the rendering result.
124
     */
125 64
    private function renderEnd(): string
126
    {
127 64
        return Html::endTag(ArrayHelper::keyExists($this->options, 'tag') ? $this->options['tag'] : 'div');
128
    }
129
130
    /**
131
     * Generates a label tag for {@see attribute}.
132
     *
133
     * @param bool $enabledLabel enabled/disable <label>.
134
     * @param array $options the tag options in terms of name-value pairs. It will be merged with {@see labelOptions}.
135
     * The options will be rendered as the attributes of the resulting tag. The values will be HTML-encoded using
136
     * {@see \Yiisoft\Html\Html::encode()}. If a value is `null`, the corresponding attribute will not be rendered.
137
     * @param string|null $label the label to use.
138
     * If `null`, the label will be generated via {@see FormModel::getAttributeLabel()}.
139
     * Note that this will NOT be {@see \Yiisoft\Html\Html::encode()|encoded}.
140
     *
141
     * @throws InvalidConfigException
142
     *
143
     * @return self the field object itself.
144
     */
145 62
    public function label(bool $enabledLabel = true, array $options = [], ?string $label = null): self
146
    {
147 62
        if ($enabledLabel === false) {
148 12
            $this->parts['{label}'] = '';
149
150 12
            return $this;
151
        }
152
153 50
        $new = clone $this;
154
155 50
        if ($label !== null) {
156 12
            $new->labelOptions['label'] = $label;
157
        }
158
159 50
        if ($new->data->isAttributeRequired($new->attribute)) {
160 19
            Html::addCssClass($options, $new->requiredCssClass);
161
        }
162
163 50
        $new->labelOptions($options);
164 50
        $new->skipForInLabel();
165
166 50
        unset($options['class']);
167
168 50
        $new->labelOptions = array_merge($new->labelOptions, $options);
169
170 50
        $this->parts['{label}'] = Label::widget()
171 50
            ->config($new->data, $new->attribute, $new->labelOptions)
0 ignored issues
show
Bug introduced by
The method config() does not exist on Yiisoft\Widget\Widget. It seems like you code against a sub-type of said class. However, the method does not exist in Yiisoft\Widget\Tests\Stubs\TestWidget or Yiisoft\Widget\Tests\Stubs\TestWidgetA or Yiisoft\Widget\Tests\Stubs\TestInjectionWidget or Yiisoft\Form\Widget\Form or Yiisoft\Widget\Tests\Stubs\ImmutableWidget or Yiisoft\Widget\Tests\Stubs\TestWidgetB. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

171
            ->/** @scrutinizer ignore-call */ config($new->data, $new->attribute, $new->labelOptions)
Loading history...
172 50
            ->run();
173
174 50
        return $this;
175
    }
176
177
    /**
178
     * Generates a tag that contains the first validation error of {@see attribute}.
179
     *
180
     * Note that even if there is no validation error, this method will still return an empty error tag.
181
     *
182
     * @param array $options the tag options in terms of name-value pairs. It will be merged with
183
     * {@see DEFAULT_ERROR_OPTIONS}.
184
     * The options will be rendered as the attributes of the resulting tag. The values will be HTML-encoded using
185
     * {@see \Yiisoft\Html\Html::encode()}. If this parameter is `false`, no error tag will be rendered.
186
     *
187
     * The following options are specially handled:
188
     *
189
     * - `tag`: this specifies the tag name. If not set, `div` will be used. See also {@see \Yiisoft\Html\Html::tag()}.
190
     *
191
     * If you set a custom `id` for the error element, you may need to adjust the {@see $selectors} accordingly.
192
     *
193
     *@throws InvalidConfigException
194
     *
195
     * @return self the field object itself.
196
     *
197
     * {@see DEFAULT_ERROR_OPTIONS}
198
     */
199 64
    public function error(array $options = []): self
200
    {
201 64
        $new = clone $this;
202
203 64
        $new->errorOptions($options);
204
205 64
        unset($options['class']);
206
207 64
        $new->errorOptions = array_merge($new->errorOptions, $options);
208
209 64
        $this->parts['{error}'] = Error::widget()
210 64
            ->config($new->data, $new->attribute, $new->errorOptions)
211 64
            ->run();
212
213 64
        return $this;
214
    }
215
216
    /**
217
     * Renders the hint tag.
218
     *
219
     * @param string|null $content the hint content. If `null`, the hint will be generated via
220
     * {@see FormModel::getAttributeHint()}.
221
     * @param bool $typeHint If `false`, the generated field will not contain the hint part. Note that this will NOT be
222
     * {@see \Yiisoft\Html\Html::encode()|encoded}.
223
     * @param array $options the tag options in terms of name-value pairs. These will be rendered as the attributes of
224
     * the hint tag. The values will be HTML-encoded using {@see \Yiisoft\Html\Html::encode()}.
225
     *
226
     * The following options are specially handled:
227
     *
228
     * - `tag`: this specifies the tag name. If not set, `div` will be used. See also {@see \Yiisoft\Html\Html::tag()}.
229
     *
230
     * @throws InvalidConfigException
231
     *
232
     * @return self the field object itself.
233
     */
234 64
    public function hint(?string $content = null, bool $typeHint = true, array $options = []): self
235
    {
236 64
        if ($typeHint === false) {
237 1
            $this->parts['{hint}'] = '';
238
239 1
            return $this;
240
        }
241
242 63
        $new = clone $this;
243
244 63
        $new->hintOptions($options);
245
246 63
        unset($options['class']);
247
248 63
        if ($content !== null) {
249 1
            $new->hintOptions['hint'] = $content;
250
        }
251
252 63
        $new->hintOptions = array_merge($new->hintOptions, $options);
253
254 63
        $this->parts['{hint}'] = Hint::widget()
255 63
            ->config($new->data, $new->attribute, $new->hintOptions)
256 63
            ->run();
257
258 63
        return $this;
259
    }
260
261
    /**
262
     * Renders an input tag.
263
     *
264
     * @param string $type the input type (e.g. `text`, `password`).
265
     * @param array $options the tag options in terms of name-value pairs. These will be rendered as the attributes of
266
     * the resulting tag. The values will be HTML-encoded using {@see \Yiisoft\Html\Html::encode()}.
267
     *
268
     * If you set a custom `id` for the input element, you may need to adjust the {@see $selectors} accordingly.
269
     *
270
     * @throws InvalidArgumentException
271
     * @throws InvalidConfigException
272
     *
273
     * @return self the field object itself.
274
     */
275 5
    public function input(string $type, array $options = []): self
276
    {
277 5
        $new = clone $this;
278
279 5
        $new->setAriaAttributes($options);
280 5
        $new->inputOptions($options);
281 5
        $new->addErrorCssClassToInput();
282 5
        $new->addSuccessCssClassToInput();
283
284 5
        unset($options['class']);
285
286 5
        $new->inputOptions = array_merge($new->inputOptions, $options);
287
288 5
        $this->parts['{input}'] = Input::widget()
289 5
            ->type($type)
0 ignored issues
show
Bug introduced by
The method type() does not exist on Yiisoft\Widget\Widget. It seems like you code against a sub-type of Yiisoft\Widget\Widget such as Yiisoft\Form\Widget\ListBox or Yiisoft\Form\Widget\Input or Yiisoft\Form\Widget\ListInput or Yiisoft\Form\Widget\BooleanInput. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

289
            ->/** @scrutinizer ignore-call */ type($type)
Loading history...
290 5
            ->config($new->data, $new->attribute, $new->inputOptions)
291 5
            ->run();
292
293 5
        return $this;
294
    }
295
296
    /**
297
     * Renders a text input.
298
     *
299
     * This method will generate the `name` and `value` tag attributes automatically for the model attribute unless
300
     * they are explicitly specified in `$options`.
301
     *
302
     * @param array $options the tag options in terms of name-value pairs. These will be rendered as the attributes
303
     * of the resulting tag. The values will be HTML-encoded using {@see \Yiisoft\Html\Html::encode()}.
304
     *
305
     * The following special options are recognized:
306
     *
307
     * Note that if you set a custom `id` for the input element, you may need to adjust the value of {@see selectors}
308
     * accordingly.
309
     *
310
     * @throws InvalidConfigException
311
     *
312
     * @return self the field object itself.
313
     */
314 27
    public function textInput(array $options = []): self
315
    {
316 27
        $new = clone $this;
317
318 27
        $new->setAriaAttributes($options);
319 27
        $new->inputOptions($options);
320 27
        $new->addErrorCssClassToInput();
321 27
        $new->addSuccessCssClassToInput();
322
323 27
        unset($options['class']);
324
325 27
        $new->inputOptions = array_merge($new->inputOptions, $options);
326
327 27
        $this->parts['{input}'] = TextInput::widget()
328 27
            ->config($new->data, $new->attribute, $new->inputOptions)
329 27
            ->run();
330
331 27
        return $this;
332
    }
333
334
    /**
335
     * Renders a hidden input.
336
     *
337
     * Note that this method is provided for completeness. In most cases because you do not need to validate a hidden
338
     * input, you should not need to use this method. Instead, you should use
339
     * {@see \Yiisoft\Html\Html::activeHiddenInput()}.
340
     *
341
     * This method will generate the `name` and `value` tag attributes automatically for the model attribute unless
342
     * they are explicitly specified in `$options`.
343
     *
344
     * @param array $options the tag options in terms of name-value pairs. These will be rendered as the attributes of
345
     * the resulting tag. The values will be HTML-encoded using {@see \Yiisoft\Html\Html::encode()}.
346
     *
347
     * If you set a custom `id` for the input element, you may need to adjust the {@see $selectors} accordingly.
348
     *
349
     * @throws InvalidConfigException
350
     *
351
     * @return self the field object itself.
352
     */
353 1
    public function hiddenInput(array $options = []): self
354
    {
355 1
        $new = clone $this;
356
357 1
        $new->inputOptions($options);
358
359 1
        unset($options['class']);
360
361 1
        $new->inputOptions = array_merge($new->inputOptions, $options);
362
363 1
        $this->parts['{label}'] = '';
364 1
        $this->parts['{hint}'] = '';
365 1
        $this->parts['{error}'] = '';
366 1
        $this->parts['{input}'] = HiddenInput::widget()
367 1
            ->config($new->data, $new->attribute, $new->inputOptions)
368 1
            ->run();
369
370 1
        return $this;
371
    }
372
373
    /**
374
     * Renders a password input.
375
     *
376
     * This method will generate the `name` and `value` tag attributes automatically for the model attribute unless
377
     * they are explicitly specified in `$options`.
378
     *
379
     * @param array $options the tag options in terms of name-value pairs. These will be rendered as the attributes of
380
     * the resulting tag. The values will be HTML-encoded using {@see \Yiisoft\Html\Html::encode()}.
381
     *
382
     * If you set a custom `id` for the input element, you may need to adjust the {@see $selectors} accordingly.
383
     *
384
     * @throws InvalidArgumentException
385
     * @throws InvalidConfigException
386
     *
387
     * @return self the field object itself.
388
     */
389 3
    public function passwordInput(array $options = []): self
390
    {
391 3
        $new = clone $this;
392
393 3
        $new->setAriaAttributes($options);
394 3
        $new->inputOptions($options);
395 3
        $new->addErrorCssClassToInput();
396 3
        $new->addSuccessCssClassToInput();
397
398 3
        unset($options['class']);
399
400 3
        $new->inputOptions = array_merge($new->inputOptions, $options);
401
402 3
        $this->parts['{input}'] = PasswordInput::widget()
403 3
            ->config($new->data, $new->attribute, $new->inputOptions)
404 3
            ->run();
405
406 3
        return $this;
407
    }
408
409
    /**
410
     * Renders a file input.
411
     *
412
     * This method will generate the `name` tag attribute automatically for the model attribute unless
413
     * they are explicitly specified in `$options`.
414
     *
415
     * @param array $options the tag options in terms of name-value pairs. These will be rendered as the attributes of
416
     * the resulting tag. The values will be HTML-encoded using {@see \Yiisoft\Html\Html::encode()}.
417
     * @param bool $withoutHiddenInput enable/disable hidden input field.
418
     *
419
     * If you set a custom `id` for the input element, you may need to adjust the {@see $selectors} accordingly.
420
     *
421
     * @throws InvalidArgumentException
422
     * @throws InvalidConfigException
423
     *
424
     * @return self the field object itself.
425
     */
426 4
    public function fileInput(array $options = [], bool $withoutHiddenInput = false): self
427
    {
428 4
        $new = clone $this;
429
430 4
        $new->setAriaAttributes($options);
431 4
        $new->inputOptions($options);
432 4
        $new->addErrorCssClassToInput();
433 4
        $new->addSuccessCssClassToInput();
434 4
        $new->setForInLabel($options);
435
436 4
        unset($options['class']);
437
438 4
        $new->inputOptions = array_merge($new->inputOptions, $options);
439
440 4
        $this->parts['{input}'] = FileInput::widget()
441 4
            ->config($new->data, $new->attribute, $new->inputOptions)
442 4
            ->withoutHiddenInput($withoutHiddenInput)
443 4
            ->run();
444
445 4
        return $this;
446
    }
447
448
    /**
449
     * Renders a text area.
450
     *
451
     * The model attribute value will be used as the content in the textarea.
452
     *
453
     * @param array $options the tag options in terms of name-value pairs. These will be rendered as the attributes of
454
     * the resulting tag. The values will be HTML-encoded using {@see \Yiisoft\Html\Html::encode()}.
455
     *
456
     * If you set a custom `id` for the textarea element, you may need to adjust the {@see $selectors} accordingly.
457
     *
458
     * @throws InvalidArgumentException
459
     * @throws InvalidConfigException
460
     *
461
     * @return self the field object itself.
462
     */
463 3
    public function textArea(array $options = []): self
464
    {
465 3
        $new = clone $this;
466
467 3
        $new->setAriaAttributes($options);
468 3
        $new->inputOptions($options);
469 3
        $new->addErrorCssClassToInput();
470 3
        $new->addSuccessCssClassToInput();
471
472 3
        unset($options['class']);
473
474 3
        $new->inputOptions = array_merge($new->inputOptions, $options);
475
476 3
        $this->parts['{input}'] = TextArea::widget()
477 3
            ->config($new->data, $new->attribute, $new->inputOptions)
478 3
            ->run();
479
480 3
        return $this;
481
    }
482
483
    /**
484
     * Renders a radio button.
485
     *
486
     * This method will generate the `checked` tag attribute according to the model attribute value.
487
     *
488
     * @param array $options the tag options in terms of name-value pairs. The following options are specially handled:
489
     *
490
     * - `uncheck`: string, the value associated with the uncheck state of the radio button. If not set, it will take
491
     * the default value `0`. This method will render a hidden input so that if the radio button is not checked and is
492
     * submitted, the value of this attribute will still be submitted to the server via the hidden input. If you do not
493
     * want any hidden input, you should explicitly set this option as `null`.
494
     * - `label`: string, a label displayed next to the radio button. It will NOT be HTML-encoded. Therefore you can
495
     * pass in HTML code such as an image tag. If this is coming from end users, you should
496
     * {@see \Yiisoft\Html\Html::encode()|encode} it to prevent XSS attacks.
497
     * When this option is specified, the radio button will be enclosed by a label tag. If you do not want any label,
498
     * you should explicitly set this option as `null`.
499
     * - `labelOptions`: array, the HTML attributes for the label tag. This is only used when the `label` option is
500
     * specified.
501
     *
502
     * The rest of the options will be rendered as the attributes of the resulting tag. The values will be HTML-encoded
503
     * using {@see \Yiisoft\Html\Html::encode()}. If a value is `null`, the corresponding attribute will not be
504
     * rendered.
505
     *
506
     * If you set a custom `id` for the input element, you may need to adjust the {@see $selectors} accordingly.
507
     * @param bool $enclosedByLabel whether to enclose the radio within the label.
508
     * If `true`, the method will still use {@see template} to layout the radio button and the error message except
509
     * that the radio is enclosed by the label tag.
510
     *
511
     * @throws InvalidArgumentException
512
     * @throws InvalidConfigException
513
     *
514
     * @return self the field object itself.
515
     */
516 5
    public function radio(array $options = [], bool $enclosedByLabel = true): self
517
    {
518 5
        $new = clone $this;
519
520 5
        if ($enclosedByLabel) {
521 2
            $this->parts['{label}'] = '';
522
        }
523
524 5
        $this->parts['{input}'] = Radio::widget()
525 5
            ->config($new->data, $new->attribute, $options)
526 5
            ->enclosedByLabel($enclosedByLabel)
527 5
            ->run();
528
529 5
        $new->setAriaAttributes($options);
530 5
        $new->addErrorCssClassToInput();
531 5
        $new->addSuccessCssClassToInput();
532
533 5
        return $this;
534
    }
535
536
    /**
537
     * Renders a checkbox.
538
     *
539
     * This method will generate the `checked` tag attribute according to the model attribute value.
540
     *
541
     * @param array $options the tag options in terms of name-value pairs. The following options are specially handled:
542
     *
543
     * - `uncheck`: string, the value associated with the uncheck state of the radio button. If not set, it will take
544
     * the default value `0`. This method will render a hidden input so that if the radio button is not checked and is
545
     * submitted, the value of this attribute will still be submitted to the server via the hidden input. If you do not
546
     * want any hidden input, you should explicitly set this option as `null`.
547
     * - `label`: string, a label displayed next to the checkbox. It will NOT be HTML-encoded. Therefore you can pass
548
     * in HTML code such as an image tag. If this is coming from end users, you should
549
     * {@see \Yiisoft\Html\Html::encode()|encode} it to prevent XSS attacks.
550
     * When this option is specified, the checkbox will be enclosed by a label tag. If you do not want any label, you
551
     * should explicitly set this option as `null`.
552
     * - `labelOptions`: array, the HTML attributes for the label tag. This is only used when the `label` option is
553
     * specified.
554
     *
555
     * The rest of the options will be rendered as the attributes of the resulting tag. The values will be HTML-encoded
556
     * using {@see \Yiisoft\Html\Html::encode()}. If a value is `null`, the corresponding attribute will not be
557
     * rendered.
558
     *
559
     * If you set a custom `id` for the input element, you may need to adjust the {@see $selectors} accordingly.
560
     * @param bool $enclosedByLabel whether to enclose the checkbox within the label.
561
     * If `true`, the method will still use [[template]] to layout the checkbox and the error message except that the
562
     * checkbox is enclosed by the label tag.
563
     *
564
     * @throws InvalidArgumentException
565
     * @throws InvalidConfigException
566
     *
567
     * @return self the field object itself.
568
     */
569 4
    public function checkbox(array $options = [], bool $enclosedByLabel = true): self
570
    {
571 4
        $new = clone $this;
572
573 4
        if ($enclosedByLabel) {
574 1
            $this->parts['{label}'] = '';
575
        }
576
577 4
        $this->parts['{input}'] = CheckBox::widget()
578 4
            ->config($new->data, $new->attribute, $options)
579 4
            ->enclosedByLabel($enclosedByLabel)
580 4
            ->run();
581
582 4
        $new->addErrorCssClassToInput();
583 4
        $new->addSuccessCssClassToInput();
584
585 4
        return $this;
586
    }
587
588
    /**
589
     * Renders a drop-down list.
590
     *
591
     * The selection of the drop-down list is taken from the value of the model attribute.
592
     *
593
     * @param array $items the option data items. The array keys are option values, and the array values are the
594
     * corresponding option labels. The array can also be nested (i.e. some array values are arrays too).
595
     * For each sub-array, an option group will be generated whose label is the key associated with the sub-array.
596
     * If you have a list of data models, you may convert them into the format described above using
597
     * {@see \Yiisoft\Arrays\ArrayHelper::map()}.
598
     *
599
     * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in the
600
     * labels will also be HTML-encoded.
601
     * @param array $options the tag options in terms of name-value pairs.
602
     *
603
     * For the list of available options please refer to the `$options` parameter of
604
     * {@see \Yiisoft\Html\Html::activeDropDownList()}.
605
     *
606
     * If you set a custom `id` for the input element, you may need to adjust the {@see $selectors} accordingly.
607
     *
608
     * @throws InvalidArgumentException
609
     * @throws InvalidConfigException
610
     *
611
     * @return self the field object itself.
612
     */
613 4
    public function dropDownList(array $items, array $options = []): self
614
    {
615 4
        $new = clone $this;
616
617 4
        $new->setAriaAttributes($options);
618 4
        $new->inputOptions($options);
619 4
        $new->addErrorCssClassToInput();
620 4
        $new->addSuccessCssClassToInput();
621
622 4
        unset($options['class']);
623
624 4
        $new->inputOptions = array_merge($new->inputOptions, $options);
625
626 4
        $this->parts['{input}'] = DropDownList::widget()
627 4
            ->config($new->data, $new->attribute, $new->inputOptions)
628 4
            ->items($items)
629 4
            ->run();
630
631 4
        return $this;
632
    }
633
634
    /**
635
     * Renders a list box.
636
     *
637
     * The selection of the list box is taken from the value of the model attribute.
638
     *
639
     * @param array $items the option data items. The array keys are option values, and the array values are the
640
     * corresponding option labels. The array can also be nested (i.e. some array values are arrays too).
641
     * For each sub-array, an option group will be generated whose label is the key associated with the sub-array.
642
     * If you have a list of data models, you may convert them into the format described above using
643
     * {@see \Yiisoft\Arrays\ArrayHelper::map()}.
644
     *
645
     * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in the
646
     * labels will also be HTML-encoded.
647
     * @param array $options the tag options in terms of name-value pairs.
648
     *
649
     * For the list of available options please refer to the `$options` parameter of
650
     * {@see \Yiisoft\Html\Html::activeListBox()}.
651
     *
652
     * If you set a custom `id` for the input element, you may need to adjust the {@see $selectors} accordingly.
653
     *
654
     * @throws InvalidArgumentException
655
     * @throws InvalidConfigException
656
     *
657
     * @return self the field object itself.
658
     */
659 6
    public function listBox(array $items, array $options = []): self
660
    {
661 6
        $new = clone $this;
662
663 6
        $new->setForInLabel($options);
664 6
        $new->setAriaAttributes($options);
665 6
        $new->inputOptions = array_merge($new->inputOptions, $options);
666
667 6
        $this->parts['{input}'] = ListBox::widget()
668 6
            ->config($new->data, $new->attribute, $new->inputOptions)
669 6
            ->items($items)
670 6
            ->run();
671
672 6
        return $this;
673
    }
674
675
    /**
676
     * Renders a list of checkboxes.
677
     *
678
     * A checkbox list allows multiple selection, like {@see listBox()}.
679
     * As a result, the corresponding submitted value is an array.
680
     * The selection of the checkbox list is taken from the value of the model attribute.
681
     *
682
     * @param array $items the data item used to generate the checkboxes.
683
     * The array values are the labels, while the array keys are the corresponding checkbox values.
684
     * @param array $options options (name => config) for the checkbox list.
685
     * For the list of available options please refer to the `$options` parameter of
686
     * {@see \Yiisoft\Html\Html::activeCheckboxList()}.
687
     *
688
     * @throws InvalidArgumentException
689
     * @throws InvalidConfigException
690
     *
691
     * @return self the field object itself.
692
     */
693 4
    public function checkboxList(array $items, array $options = []): self
694
    {
695 4
        $new = clone $this;
696
697 4
        $new->setForInLabel($options);
698 4
        $new->setAriaAttributes($options);
699 4
        $new->inputOptions = array_merge($new->inputOptions, $options);
700 4
        $new->skipForInLabel = true;
701
702 4
        $this->parts['{input}'] = CheckBoxList::widget()
703 4
            ->config($new->data, $new->attribute, $new->inputOptions)
704 4
            ->items($items)
705 4
            ->run();
706
707 4
        return $this;
708
    }
709
710
    /**
711
     * Renders a list of radio buttons.
712
     *
713
     * A radio button list is like a checkbox list, except that it only allows single selection.
714
     * The selection of the radio buttons is taken from the value of the model attribute.
715
     *
716
     * @param array $items the data item used to generate the radio buttons.
717
     * The array values are the labels, while the array keys are the corresponding radio values.
718
     * @param array $options options (name => config) for the radio button list.
719
     * For the list of available options please refer to the `$options` parameter of
720
     * {@see \Yiisoft\Html\Html::activeRadioList()}.
721
     *
722
     * @throws InvalidArgumentException
723
     * @throws InvalidConfigException
724
     *
725
     * @return self the field object itself.
726
     */
727 3
    public function radioList(array $items, array $options = []): self
728
    {
729 3
        $new = clone $this;
730
731 3
        $new->setAriaAttributes($options);
732 3
        $new->setForInLabel($options);
733 3
        $new->inputOptions($options);
734 3
        $new->addErrorCssClassToInput();
735 3
        $new->addSuccessCssClassToInput();
736 3
        $new->setInputRole($options);
737 3
        $new->inputOptions = array_merge($new->inputOptions, $options);
738 3
        $new->skipForInLabel = true;
739
740 3
        $this->parts['{input}'] = RadioList::widget()
741 3
            ->config($new->data, $new->attribute, $new->inputOptions)
742 3
            ->items($items)
743 3
            ->run();
744
745 3
        return $this;
746
    }
747
748 64
    private function addInputId(): string
749
    {
750 64
        return $this->inputId ?: HtmlForm::getInputId($this->data, $this->attribute);
751
    }
752
753
    /**
754
     * Set form model and name for the widget.
755
     *
756
     * @param FormModelInterface $data Form model.
757
     * @param string $attribute Form model property this widget is rendered for.
758
     *
759
     * @return self
760
     */
761 65
    public function config(FormModelInterface $data, string $attribute): self
762
    {
763 65
        $new = clone $this;
764 65
        $new->data = $data;
765 65
        $new->attribute = $attribute;
766 65
        return $new;
767
    }
768
769
    /**
770
     * Generate a container tag for {@see attribute}.
771
     *
772
     * @param bool $containerEnabled enabled/disable container for the widget.
773
     * @param array $options The HTML attributes for the widget container tag if it is enabled.
774
     * See {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
775
     *
776
     * @return self
777
     */
778 2
    public function enclosedByContainer(bool $containerEnabled, array $options = []): self
779
    {
780 2
        $new = clone $this;
781 2
        $new->options = $options;
782 2
        $new->containerEnabled = $containerEnabled;
783 2
        return $new;
784
    }
785
786 1
    public function ariaAttribute(bool $value): self
787
    {
788 1
        $new = clone $this;
789 1
        $new->ariaAttribute = $value;
790 1
        return $new;
791
    }
792
793
    public function errorCssClass(string $value): self
794
    {
795
        $new = clone $this;
796
        $new->errorCssClass = $value;
797
        return $new;
798
    }
799
800
    public function errorSummaryCssClass(string $value): self
801
    {
802
        $new = clone $this;
803
        $new->errorSummaryCssClass = $value;
804
        return $new;
805
    }
806
807 2
    public function inputCssClass(string $value): self
808
    {
809 2
        $new = clone $this;
810 2
        $new->inputCssClass = $value;
811 2
        return $new;
812
    }
813
814
    public function requiredCssClass(string $value): self
815
    {
816
        $new = clone $this;
817
        $new->requiredCssClass = $value;
818
        return $new;
819
    }
820
821
    public function successCssClass(string $value): self
822
    {
823
        $new = clone $this;
824
        $new->successCssClass = $value;
825
        return $new;
826
    }
827
828 2
    public function template(string $value): self
829
    {
830 2
        $new = clone $this;
831 2
        $new->template = $value;
832 2
        return $new;
833
    }
834
835
    public function validatingCssClass(string $value): self
836
    {
837
        $new = clone $this;
838
        $new->validatingCssClass = $value;
839
        return $new;
840
    }
841
842 2
    public function validationStateOn(string $value): self
843
    {
844 2
        $new = clone $this;
845 2
        $new->validationStateOn = $value;
846 2
        return $new;
847
    }
848
849 64
    public function errorOptions(array $options = []): void
850
    {
851 64
        $classFromOptions = ArrayHelper::remove($options, 'class', self::DEFAULT_ERROR_OPTIONS['class']);
852
853 64
        if ($classFromOptions !== self::DEFAULT_ERROR_OPTIONS['class']) {
854 2
            $classFromOptions = self::DEFAULT_ERROR_OPTIONS['class'] . ' ' . $classFromOptions;
855
        }
856
857 64
        $this->errorOptions = array_merge($this->errorOptions, $options);
858
        /**
859
         * @psalm-suppress PossiblyNullPropertyAssignmentValue
860
         */
861 64
        Html::addCssClass($this->errorOptions, $classFromOptions);
862 64
    }
863
864 63
    public function hintOptions(array $options = []): void
865
    {
866 63
        $classFromOptions = ArrayHelper::remove($options, 'class', self::DEFAULT_HINT_OPTIONS['class']);
867
868 63
        if ($classFromOptions !== self::DEFAULT_HINT_OPTIONS['class']) {
869 1
            $classFromOptions = self::DEFAULT_HINT_OPTIONS['class'] . ' ' . $classFromOptions;
870
        }
871
872 63
        $this->hintOptions = array_merge($this->hintOptions, $options);
873
        /**
874
         * @psalm-suppress PossiblyNullPropertyAssignmentValue
875
         */
876 63
        Html::addCssClass($this->hintOptions, $classFromOptions);
877 63
    }
878
879 49
    public function inputOptions(array $options = []): void
880
    {
881 49
        $classFromOptions = ArrayHelper::remove($options, 'class', $this->inputCssClass);
882
883 49
        if ($classFromOptions !== $this->inputCssClass) {
884
            $classFromOptions = $this->inputCssClass . ' ' . $classFromOptions;
885
        }
886
887 49
        $this->inputOptions = array_merge($this->inputOptions, $options);
888
        /**
889
         * @psalm-suppress PossiblyNullPropertyAssignmentValue
890
         */
891 49
        Html::addCssClass($this->inputOptions, $classFromOptions);
892 49
    }
893
894 50
    public function labelOptions(array $options = []): void
895
    {
896 50
        $classFromOptions = ArrayHelper::remove($options, 'class', self::DEFAULT_LABEL_OPTIONS['class']);
897
898 50
        if ($classFromOptions !== self::DEFAULT_LABEL_OPTIONS['class']) {
899 29
            $classFromOptions = self::DEFAULT_LABEL_OPTIONS['class'] . ' ' . $classFromOptions;
900
        }
901
902 50
        $this->labelOptions = array_merge($this->labelOptions, $options);
903
        /**
904
         * @psalm-suppress PossiblyNullPropertyAssignmentValue
905
         */
906 50
        Html::addCssClass($this->labelOptions, $classFromOptions);
907 50
    }
908
909 64
    private function addErrorCssClassToContainer(): void
910
    {
911 64
        if ($this->validationStateOn === 'container') {
912
            /**
913
             * @psalm-suppress PossiblyNullPropertyAssignmentValue
914
             */
915 2
            Html::addCssClass($this->options, $this->errorCssClass);
916
        }
917 64
    }
918
919 56
    private function addErrorCssClassToInput(): void
920
    {
921 56
        if ($this->validationStateOn === 'input') {
922 54
            $attributeName = HtmlForm::getAttributeName($this->attribute);
923
924 54
            if ($this->data->hasErrors($attributeName)) {
925
                /**
926
                 * @psalm-suppress PossiblyNullPropertyAssignmentValue
927
                 */
928 7
                Html::addCssClass($this->inputOptions, $this->errorCssClass);
929
            }
930
        }
931 56
    }
932
933 56
    private function addSuccessCssClassToInput(): void
934
    {
935 56
        if ($this->validationStateOn === 'input') {
936 54
            $attributeName = HtmlForm::getAttributeName($this->attribute);
937
938 54
            if (!$this->data->hasErrors($attributeName) && $this->data->isValidated()) {
939
                /**
940
                 * @psalm-suppress PossiblyNullPropertyAssignmentValue
941
                 */
942 3
                Html::addCssClass($this->inputOptions, $this->successCssClass);
943
            }
944
        }
945 56
    }
946
947 3
    private function setInputRole(array $options = []): void
948
    {
949 3
        $this->inputOptions['role'] = $options['role'] ?? 'radiogroup';
950 3
    }
951
952 50
    private function skipForInLabel(): void
953
    {
954 50
        if ($this->skipForInLabel) {
955
            $this->labelOptions['for'] = null;
956
        }
957 50
    }
958
959 16
    private function setForInLabel(array $options = []): void
960
    {
961 16
        if (isset($options['id'])) {
962
            $this->inputId = $options['id'];
963
964
            if (!isset($this->labelOptions['for'])) {
965
                $this->labelOptions['for'] = $options['id'];
966
            }
967
        }
968 16
    }
969
970 60
    private function setAriaAttributes(array $options = []): void
971
    {
972 60
        if ($this->ariaAttribute) {
973 60
            if (!isset($options['aria-invalid']) && $this->data->hasErrors($this->attribute)) {
974 7
                $this->inputOptions['aria-invalid'] = 'true';
975
            }
976
        }
977 60
    }
978
}
979