Test Failed
Pull Request — master (#192)
by Alexander
12:02 queued 09:06
created

PartsField   F

Complexity

Total Complexity 70

Size/Duplication

Total Lines 485
Duplicated Lines 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
eloc 205
c 3
b 1
f 0
dl 0
loc 485
rs 2.8
wmc 70

42 Methods

Rating   Name   Duplication   Size   Complexity  
A errorId() 0 5 1
A hintId() 0 5 1
B generateLabel() 0 24 7
B generateError() 0 24 7
A hideLabel() 0 5 1
B generateHint() 0 24 7
A errorClass() 0 8 1
A templateBegin() 0 5 1
A labelConfig() 0 5 1
A renderHint() 0 3 1
A labelId() 0 5 1
A generateInput() 0 3 1
A errorAttributes() 0 5 1
A replaceHintAttributes() 0 6 1
A replaceLabelAttributes() 0 6 1
A error() 0 5 1
A generateEndContent() 0 10 2
A renderLabel() 0 3 1
A token() 0 7 1
A hintConfig() 0 5 1
A label() 0 5 1
A template() 0 5 1
A replaceErrorClass() 0 6 1
A hintAttributes() 0 5 1
A generateBeginInput() 0 3 1
A labelClass() 0 8 1
A generateEndInput() 0 3 1
A replaceLabelClass() 0 6 1
A templateEnd() 0 5 1
A hint() 0 5 1
A generateContent() 0 10 2
A errorConfig() 0 5 1
A replaceHintClass() 0 6 1
A renderError() 0 3 1
A labelAttributes() 0 5 1
A tokens() 0 29 5
A generateBeginContent() 0 10 2
A replaceErrorAttributes() 0 6 1
A hintClass() 0 8 1
A makeContent() 0 7 2
A validateToken() 0 11 3
A shouldHideLabel() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like PartsField 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 PartsField, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Form\Field\Base;
6
7
use InvalidArgumentException;
8
use Stringable;
9
use Yiisoft\Form\Field\Part\Error;
10
use Yiisoft\Form\Field\Part\Hint;
11
use Yiisoft\Form\Field\Part\Label;
12
use Yiisoft\Html\Html;
13
14
use function in_array;
15
16
abstract class PartsField extends BaseField
17
{
18
    private const BUILTIN_TOKENS = [
19
        '{input}',
20
        '{label}',
21
        '{hint}',
22
        '{error}',
23
    ];
24
25
    /**
26
     * @var string[]|Stringable[]
27
     * @psalm-var array<non-empty-string,string|Stringable>
28
     */
29
    private array $extraTokens = [];
30
31
    protected string $templateBegin = "{label}\n{input}";
32
    protected string $templateEnd = "{input}\n{hint}\n{error}";
33
    protected string $template = "{label}\n{input}\n{hint}\n{error}";
34
    protected ?bool $hideLabel = null;
35
36
    private bool $replaceLabelAttributes = false;
37
    private bool $replaceLabelClass = false;
38
    private array $labelAttributes = [];
39
    private array $labelConfig = [];
40
41
    private bool $replaceHintAttributes = false;
42
    private bool $replaceHintClass = false;
43
    private array $hintAttributes = [];
44
    private array $hintConfig = [];
45
46
    private bool $replaceErrorAttributes = false;
47
    private bool $replaceErrorClass = false;
48
    private array $errorAttributes = [];
49
    private array $errorConfig = [];
50
51
    final public function tokens(array $tokens): static
52
    {
53
        $new = clone $this;
54
55
        foreach ($tokens as $token => $value) {
56
            if (!is_string($token)) {
57
                throw new InvalidArgumentException(
58
                    sprintf(
59
                        'Token should be string. %s given.',
60
                        $token,
61
                    )
62
                );
63
            }
64
65
            if (!is_string($value) && !$value instanceof Stringable) {
66
                throw new InvalidArgumentException(
67
                    sprintf(
68
                        'Token value should be string or Stringable. %s given.',
69
                        get_debug_type($value),
70
                    )
71
                );
72
            }
73
74
            $this->validateToken($token);
75
76
            $new->extraTokens[$token] = $value;
77
        }
78
79
        return $new;
80
    }
81
82
    final public function token(string $token, string|Stringable $value): static
83
    {
84
        $this->validateToken($token);
85
86
        $new = clone $this;
87
        $new->extraTokens[$token] = $value;
88
        return $new;
89
    }
90
91
    /**
92
     * Set layout template for render a field.
93
     */
94
    final public function template(string $template): static
95
    {
96
        $new = clone $this;
97
        $new->template = $template;
98
        return $new;
99
    }
100
101
    final public function templateBegin(string $template): static
102
    {
103
        $new = clone $this;
104
        $new->templateBegin = $template;
105
        return $new;
106
    }
107
108
    final public function templateEnd(string $template): static
109
    {
110
        $new = clone $this;
111
        $new->templateEnd = $template;
112
        return $new;
113
    }
114
115
    final public function hideLabel(?bool $hide = true): static
116
    {
117
        $new = clone $this;
118
        $new->hideLabel = $hide;
119
        return $new;
120
    }
121
122
    final public function labelConfig(array $config): static
123
    {
124
        $new = clone $this;
125
        $new->labelConfig = $config;
126
        return $new;
127
    }
128
129
    final public function labelAttributes(array $attributes): static
130
    {
131
        $new = clone $this;
132
        $new->labelAttributes = array_merge($new->labelAttributes, $attributes);
133
        return $new;
134
    }
135
136
    final public function replaceLabelAttributes(array $attributes): static
137
    {
138
        $new = clone $this;
139
        $new->labelAttributes = $attributes;
140
        $new->replaceLabelAttributes = true;
141
        return $new;
142
    }
143
144
    /**
145
     * Set label tag ID.
146
     *
147
     * @param string|null $id Label tag ID.
148
     */
149
    final public function labelId(?string $id): static
150
    {
151
        $new = clone $this;
152
        $new->labelAttributes['id'] = $id;
153
        return $new;
154
    }
155
156
    /**
157
     * Add one or more CSS classes to the label tag.
158
     *
159
     * @param string|null ...$class One or many CSS classes.
160
     */
161
    final public function labelClass(?string ...$class): static
162
    {
163
        $new = clone $this;
164
        Html::addCssClass(
165
            $new->labelAttributes,
166
            array_filter($class, static fn ($c) => $c !== null),
167
        );
168
        return $new;
169
    }
170
171
    /**
172
     * Replace label tag CSS classes with a new set of classes.
173
     *
174
     * @param string|null ...$class One or many CSS classes.
175
     */
176
    final public function replaceLabelClass(?string ...$class): static
177
    {
178
        $new = clone $this;
179
        $new->labelAttributes['class'] = array_filter($class, static fn ($c) => $c !== null);
180
        $new->replaceLabelClass = true;
181
        return $new;
182
    }
183
184
    final public function label(?string $content): static
185
    {
186
        $new = clone $this;
187
        $new->labelConfig['content()'] = [$content];
188
        return $new;
189
    }
190
191
    final public function hintConfig(array $config): static
192
    {
193
        $new = clone $this;
194
        $new->hintConfig = $config;
195
        return $new;
196
    }
197
198
    final public function hintAttributes(array $attributes): static
199
    {
200
        $new = clone $this;
201
        $new->hintAttributes = array_merge($new->hintAttributes, $attributes);
202
        return $new;
203
    }
204
205
    final public function replaceHintAttributes(array $attributes): static
206
    {
207
        $new = clone $this;
208
        $new->hintAttributes = $attributes;
209
        $new->replaceHintAttributes = true;
210
        return $new;
211
    }
212
213
    /**
214
     * Set hint tag ID.
215
     *
216
     * @param string|null $id Hint tag ID.
217
     */
218
    final public function hintId(?string $id): static
219
    {
220
        $new = clone $this;
221
        $new->hintAttributes['id'] = $id;
222
        return $new;
223
    }
224
225
    /**
226
     * Add one or more CSS classes to the hint tag.
227
     *
228
     * @param string|null ...$class One or many CSS classes.
229
     */
230
    final public function hintClass(?string ...$class): static
231
    {
232
        $new = clone $this;
233
        Html::addCssClass(
234
            $new->hintAttributes,
235
            array_filter($class, static fn ($c) => $c !== null),
236
        );
237
        return $new;
238
    }
239
240
    /**
241
     * Replace hint tag CSS classes with a new set of classes.
242
     *
243
     * @param string|null ...$class One or many CSS classes.
244
     */
245
    final public function replaceHintClass(?string ...$class): static
246
    {
247
        $new = clone $this;
248
        $new->hintAttributes['class'] = array_filter($class, static fn ($c) => $c !== null);
249
        $new->replaceHintClass = true;
250
        return $new;
251
    }
252
253
    final public function hint(?string $content): static
254
    {
255
        $new = clone $this;
256
        $new->hintConfig['content()'] = [$content];
257
        return $new;
258
    }
259
260
    final public function errorConfig(array $config): static
261
    {
262
        $new = clone $this;
263
        $new->errorConfig = $config;
264
        return $new;
265
    }
266
267
    final public function errorAttributes(array $attributes): static
268
    {
269
        $new = clone $this;
270
        $new->errorAttributes = array_merge($new->errorAttributes, $attributes);
271
        return $new;
272
    }
273
274
    final public function replaceErrorAttributes(array $attributes): static
275
    {
276
        $new = clone $this;
277
        $new->errorAttributes = $attributes;
278
        $new->replaceErrorAttributes = true;
279
        return $new;
280
    }
281
282
    /**
283
     * Set error tag ID.
284
     *
285
     * @param string|null $id Error tag ID.
286
     */
287
    final public function errorId(?string $id): static
288
    {
289
        $new = clone $this;
290
        $new->errorAttributes['id'] = $id;
291
        return $new;
292
    }
293
294
    /**
295
     * Add one or more CSS classes to the error tag.
296
     *
297
     * @param string|null ...$class One or many CSS classes.
298
     */
299
    final public function errorClass(?string ...$class): static
300
    {
301
        $new = clone $this;
302
        Html::addCssClass(
303
            $new->errorAttributes,
304
            array_filter($class, static fn ($c) => $c !== null),
305
        );
306
        return $new;
307
    }
308
309
    /**
310
     * Replace error tag CSS classes with a new set of classes.
311
     *
312
     * @param string|null ...$class One or many CSS classes.
313
     */
314
    final public function replaceErrorClass(?string ...$class): static
315
    {
316
        $new = clone $this;
317
        $new->errorAttributes['class'] = array_filter($class, static fn ($c) => $c !== null);
318
        $new->replaceErrorClass = true;
319
        return $new;
320
    }
321
322
    final public function error(?string $message): static
323
    {
324
        $new = clone $this;
325
        $new->errorConfig['message()'] = [$message];
326
        return $new;
327
    }
328
329
    protected function shouldHideLabel(): bool
330
    {
331
        return false;
332
    }
333
334
    protected function generateInput(): string
335
    {
336
        return '';
337
    }
338
339
    protected function generateBeginInput(): string
340
    {
341
        return '';
342
    }
343
344
    protected function generateEndInput(): string
345
    {
346
        return '';
347
    }
348
349
    protected function renderLabel(Label $label): string
350
    {
351
        return $label->render();
352
    }
353
354
    protected function renderHint(Hint $hint): string
355
    {
356
        return $hint->render();
357
    }
358
359
    protected function renderError(Error $error): string
360
    {
361
        return $error->render();
362
    }
363
364
    final protected function generateContent(): ?string
365
    {
366
        $parts = [
367
            '{input}' => $this->generateInput(),
368
            '{label}' => ($this->hideLabel ?? $this->shouldHideLabel()) ? '' : $this->generateLabel(),
369
            '{hint}' => $this->generateHint(),
370
            '{error}' => $this->generateError(),
371
        ];
372
373
        return $this->makeContent($this->template, $parts);
374
    }
375
376
    final protected function generateBeginContent(): string
377
    {
378
        $parts = [
379
            '{input}' => $this->generateBeginInput(),
380
            '{label}' => ($this->hideLabel ?? $this->shouldHideLabel()) ? '' : $this->generateLabel(),
381
            '{hint}' => $this->generateHint(),
382
            '{error}' => $this->generateError(),
383
        ];
384
385
        return $this->makeContent($this->templateBegin, $parts);
386
    }
387
388
    final protected function generateEndContent(): string
389
    {
390
        $parts = [
391
            '{input}' => $this->generateEndInput(),
392
            '{label}' => ($this->hideLabel ?? $this->shouldHideLabel()) ? '' : $this->generateLabel(),
393
            '{hint}' => $this->generateHint(),
394
            '{error}' => $this->generateError(),
395
        ];
396
397
        return $this->makeContent($this->templateEnd, $parts);
398
    }
399
400
    private function makeContent(string $template, array $parts): string
401
    {
402
        if (!empty($this->extraTokens)) {
403
            $parts += $this->extraTokens;
404
        }
405
406
        return preg_replace('/^\h*\v+/m', '', trim(strtr($template, $parts)));
407
    }
408
409
    private function generateLabel(): string
410
    {
411
        $label = Label::widget($this->labelConfig);
412
413
        $labelAttributes = $this->labelAttributes;
414
        if (!empty($labelAttributes)) {
415
            if ($this->replaceLabelAttributes) {
416
                $label = $label->replaceAttributes($labelAttributes);
0 ignored issues
show
Bug introduced by
The method replaceAttributes() does not exist on Yiisoft\Widget\Widget. It seems like you code against a sub-type of Yiisoft\Widget\Widget such as Yiisoft\Form\Field\Part\Label or Yiisoft\Form\Field\Part\Error or Yiisoft\Form\Field\Part\Hint. ( Ignorable by Annotation )

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

416
                /** @scrutinizer ignore-call */ 
417
                $label = $label->replaceAttributes($labelAttributes);
Loading history...
417
            } else {
418
                /** @var string|string[]|null $class */
419
                $class = $this->labelAttributes['class'] ?? null;
420
                unset($labelAttributes['class']);
421
422
                $label = $label->attributes($labelAttributes);
0 ignored issues
show
Bug introduced by
The method attributes() does not exist on Yiisoft\Widget\Widget. It seems like you code against a sub-type of Yiisoft\Widget\Widget such as Yiisoft\Form\Field\Part\Label or Yiisoft\Form\Field\Part\Error or Yiisoft\Form\Field\Part\Hint. ( Ignorable by Annotation )

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

422
                /** @scrutinizer ignore-call */ 
423
                $label = $label->attributes($labelAttributes);
Loading history...
423
424
                if ($this->replaceLabelClass) {
425
                    $label = is_array($class) ? $label->replaceClass(...$class) : $label->replaceClass($class);
426
                } elseif ($class !== null) {
427
                    $label = is_array($class) ? $label->class(...$class) : $label->class($class);
428
                }
429
            }
430
        }
431
432
        return $this->renderLabel($label);
433
    }
434
435
    private function generateHint(): string
436
    {
437
        $hint = Hint::widget($this->hintConfig);
438
439
        $hintAttributes = $this->hintAttributes;
440
        if (!empty($hintAttributes)) {
441
            if ($this->replaceHintAttributes) {
442
                $hint = $hint->replaceAttributes($hintAttributes);
443
            } else {
444
                /** @var string|string[]|null $class */
445
                $class = $this->hintAttributes['class'] ?? null;
446
                unset($hintAttributes['class']);
447
448
                $hint = $hint->attributes($hintAttributes);
449
450
                if ($this->replaceHintClass) {
451
                    $hint = is_array($class) ? $hint->replaceClass(...$class) : $hint->replaceClass($class);
452
                } elseif ($class !== null) {
453
                    $hint = is_array($class) ? $hint->class(...$class) : $hint->class($class);
454
                }
455
            }
456
        }
457
458
        return $this->renderHint($hint);
459
    }
460
461
    private function generateError(): string
462
    {
463
        $error = Error::widget($this->errorConfig);
464
465
        $errorAttributes = $this->errorAttributes;
466
        if (!empty($errorAttributes)) {
467
            if ($this->replaceErrorAttributes) {
468
                $error = $error->replaceAttributes($errorAttributes);
469
            } else {
470
                /** @var string|string[]|null $class */
471
                $class = $this->errorAttributes['class'] ?? null;
472
                unset($errorAttributes['class']);
473
474
                $error = $error->attributes($errorAttributes);
475
476
                if ($this->replaceErrorClass) {
477
                    $error = is_array($class) ? $error->replaceClass(...$class) : $error->replaceClass($class);
478
                } elseif ($class !== null) {
479
                    $error = is_array($class) ? $error->class(...$class) : $error->class($class);
480
                }
481
            }
482
        }
483
484
        return $this->renderError($error);
485
    }
486
487
    /**
488
     * @psalm-assert non-empty-string $token
489
     */
490
    private function validateToken(string $token): void
491
    {
492
        if ($token === '') {
493
            throw new InvalidArgumentException('Token must be non-empty string.');
494
        }
495
496
        if (in_array($token, self::BUILTIN_TOKENS, true)) {
497
            throw new InvalidArgumentException(
498
                sprintf(
499
                    'Token name "%s" is built-in.',
500
                    $token,
501
                )
502
            );
503
        }
504
    }
505
}
506