Passed
Push — master ( ef6f5d...60f4bc )
by Alexander
02:52
created

FormModel   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 363
Duplicated Lines 0 %

Test Coverage

Coverage 99.3%

Importance

Changes 9
Bugs 0 Features 0
Metric Value
eloc 117
c 9
b 0
f 0
dl 0
loc 363
ccs 141
cts 142
cp 0.993
rs 5.5199
wmc 56

26 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A getAttributeValue() 0 3 1
A getFormErrors() 0 3 1
A writeProperty() 0 16 2
A attributes() 0 3 1
A generateAttributeLabel() 0 4 1
A getRules() 0 3 1
A getAttributePlaceholders() 0 3 1
A processValidationResult() 0 13 3
A getAttributeHints() 0 3 1
A getInflector() 0 6 2
A addErrors() 0 5 3
A setAttribute() 0 21 6
A collectAttributes() 0 17 4
A getAttributeHint() 0 7 2
A createFormErrors() 0 9 2
A getNestedAttributeValue() 0 14 2
A getFormName() 0 12 3
A isValidated() 0 3 1
A readProperty() 0 19 3
A getAttributePlaceholder() 0 7 2
A getAttributeLabel() 0 12 3
A getAttributeLabels() 0 3 1
A getNestedAttribute() 0 16 3
A hasAttribute() 0 3 1
A load() 0 22 5

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Form;
6
7
use Closure;
8
use InvalidArgumentException;
9
use ReflectionClass;
10
use ReflectionNamedType;
11
use Stringable;
12
use Yiisoft\Strings\Inflector;
13
use Yiisoft\Strings\StringHelper;
14
use Yiisoft\Validator\PostValidationHookInterface;
15
use Yiisoft\Validator\ResultSet;
16
use Yiisoft\Validator\RulesProviderInterface;
17
18
use function array_key_exists;
19
use function explode;
20
use function is_subclass_of;
21
use function strpos;
22
23
/**
24
 * Form model represents an HTML form: its data, validation and presentation.
25
 */
26
abstract class FormModel implements FormModelInterface, PostValidationHookInterface, RulesProviderInterface
27
{
28
    private string $formErrorsClass = FormErrors::class;
29
    private array $attributes;
30
    private FormErrorsInterface $formErrors;
31
    private ?Inflector $inflector = null;
32
    private bool $validated = false;
33
34 755
    public function __construct()
35
    {
36 755
        $this->attributes = $this->collectAttributes();
37 755
        $this->formErrors = $this->createFormErrors($this->formErrorsClass);
38 755
    }
39
40 1
    public function attributes(): array
41
    {
42 1
        return array_keys($this->attributes);
43
    }
44
45 354
    public function getAttributeHint(string $attribute): string
46
    {
47 354
        $attributeHints = $this->getAttributeHints();
48 354
        $hint = $attributeHints[$attribute] ?? '';
49 354
        $nestedAttributeHint = $this->getNestedAttributeValue('getAttributeHint', $attribute);
50
51 354
        return $nestedAttributeHint !== '' ? $nestedAttributeHint : $hint;
52
    }
53
54
    /**
55
     * @return string[]
56
     */
57 339
    public function getAttributeHints(): array
58
    {
59 339
        return [];
60
    }
61
62 375
    public function getAttributeLabel(string $attribute): string
63
    {
64 375
        $label = $this->generateAttributeLabel($attribute);
65 375
        $labels = $this->getAttributeLabels();
66
67 375
        if (array_key_exists($attribute, $labels)) {
68 2
            $label = $labels[$attribute];
69
        }
70
71 375
        $nestedAttributeLabel = $this->getNestedAttributeValue('getAttributeLabel', $attribute);
72
73 375
        return $nestedAttributeLabel !== '' ? $nestedAttributeLabel : $label;
74
    }
75
76
    /**
77
     * @return string[]
78
     */
79 373
    public function getAttributeLabels(): array
80
    {
81 373
        return [];
82
    }
83
84 363
    public function getAttributePlaceholder(string $attribute): string
85
    {
86 363
        $attributePlaceHolders = $this->getAttributePlaceholders();
87 363
        $placeholder = $attributePlaceHolders[$attribute] ?? '';
88 363
        $nestedAttributePlaceholder = $this->getNestedAttributeValue('getAttributePlaceholder', $attribute);
89
90 363
        return $nestedAttributePlaceholder !== '' ? $nestedAttributePlaceholder : $placeholder;
91
    }
92
93
    /**
94
     * @return string[]
95
     */
96 361
    public function getAttributePlaceholders(): array
97
    {
98 361
        return [];
99
    }
100
101
    /**
102
     * @return iterable|object|scalar|Stringable|null
103
     */
104 687
    public function getAttributeValue(string $attribute)
105
    {
106 687
        return $this->readProperty($attribute);
107
    }
108
109
    /**
110
     * @return FormErrorsInterface Get FormErrors object.
111
     */
112 366
    public function getFormErrors(): FormErrorsInterface
113
    {
114 366
        return $this->formErrors;
115
    }
116
117
    /**
118
     * @return string Returns classname without a namespace part or empty string when class is anonymous
119
     */
120 725
    public function getFormName(): string
121
    {
122 725
        if (strpos(static::class, '@anonymous') !== false) {
123 6
            return '';
124
        }
125
126 720
        $className = strrchr(static::class, '\\');
127 720
        if ($className === false) {
128 1
            return static::class;
129
        }
130
131 719
        return substr($className, 1);
132
    }
133
134 696
    public function hasAttribute(string $attribute): bool
135
    {
136 696
        return array_key_exists($attribute, $this->attributes);
137
    }
138
139
    /**
140
     * @param array $data
141
     * @param string|null $formName
142
     *
143
     * @return bool
144
     */
145 26
    public function load(array $data, ?string $formName = null): bool
146
    {
147 26
        $scope = $formName ?? $this->getFormName();
148
149
        /**
150
         * @psalm-var array<string, scalar|Stringable|null>
151
         */
152 26
        $values = [];
153
154 26
        if ($scope === '' && !empty($data)) {
155 3
            $values = $data;
156 24
        } elseif (isset($data[$scope])) {
157
            /** @var mixed */
158 23
            $values = $data[$scope];
159
        }
160
161
        /** @var array<string, scalar|Stringable|null> $values */
162 26
        foreach ($values as $name => $value) {
163 26
            $this->setAttribute($name, $value);
164
        }
165
166 26
        return $values !== [];
167
    }
168
169
    /**
170
     * @param iterable|object|scalar|Stringable|null $value
171
     *
172
     * @psalm-suppress PossiblyInvalidCast
173
     */
174 72
    public function setAttribute(string $name, $value): void
175
    {
176 72
        [$realName] = $this->getNestedAttribute($name);
177
178 72
        if (isset($this->attributes[$realName])) {
179 71
            switch ($this->attributes[$realName]) {
180 71
                case 'bool':
181 7
                    $this->writeProperty($name, (bool) $value);
182 7
                    break;
183 71
                case 'float':
184 1
                    $this->writeProperty($name, (float) $value);
185 1
                    break;
186 71
                case 'int':
187 17
                    $this->writeProperty($name, (int) $value);
188 17
                    break;
189 69
                case 'string':
190 61
                    $this->writeProperty($name, (string) $value);
191 61
                    break;
192
                default:
193 11
                    $this->writeProperty($name, $value);
194 11
                    break;
195
            }
196
        }
197 72
    }
198
199 26
    public function processValidationResult(ResultSet $resultSet): void
200
    {
201 26
        $this->validated = false;
202
203
        /** @var array<string, Resultset> $resultSet */
204 26
        foreach ($resultSet as $attribute => $result) {
205 26
            if ($result->isValid() === false) {
206 24
                $this->formErrors->clear($attribute);
207
                /** @psalm-suppress InvalidArgument */
208 24
                $this->addErrors([$attribute => $result->getErrors()]);
209
            }
210
        }
211 26
        $this->validated = true;
212 26
    }
213
214 532
    public function getRules(): array
215
    {
216 532
        return [];
217
    }
218
219
    /**
220
     * Returns the list of attribute types indexed by attribute names.
221
     *
222
     * By default, this method returns all non-static properties of the class.
223
     *
224
     * @return array list of attribute types indexed by attribute names.
225
     */
226 755
    protected function collectAttributes(): array
227
    {
228 755
        $class = new ReflectionClass($this);
229 755
        $attributes = [];
230
231 755
        foreach ($class->getProperties() as $property) {
232 750
            if ($property->isStatic()) {
233 28
                continue;
234
            }
235
236
            /** @var ReflectionNamedType|null $type */
237 750
            $type = $property->getType();
238
239 750
            $attributes[$property->getName()] = $type !== null ? $type->getName() : '';
240
        }
241
242 755
        return $attributes;
243
    }
244
245
    /**
246
     * @psalm-param array<string, array<array-key, string>> $items
247
     */
248 24
    private function addErrors(array $items): void
249
    {
250 24
        foreach ($items as $attribute => $errors) {
251 24
            foreach ($errors as $error) {
252 24
                $this->formErrors->addError($attribute, $error);
253
            }
254
        }
255 24
    }
256
257 755
    private function createFormErrors(string $formErrorsClass): FormErrorsInterface
258
    {
259 755
        $formErrors = new $formErrorsClass();
260
261 755
        if (!$formErrors instanceof FormErrorsInterface) {
262
            throw new InvalidArgumentException('Form errors class must implement ' . FormErrorsInterface::class);
263
        }
264
265 755
        return $formErrors;
266
    }
267
268 375
    private function getInflector(): Inflector
269
    {
270 375
        if ($this->inflector === null) {
271 375
            $this->inflector = new Inflector();
272
        }
273 375
        return $this->inflector;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->inflector could return the type null which is incompatible with the type-hinted return Yiisoft\Strings\Inflector. Consider adding an additional type-check to rule them out.
Loading history...
274
    }
275
276
    /**
277
     * Generates a user friendly attribute label based on the give attribute name.
278
     *
279
     * This is done by replacing underscores, dashes and dots with blanks and changing the first letter of each word to
280
     * upper case.
281
     *
282
     * For example, 'department_name' or 'DepartmentName' will generate 'Department Name'.
283
     *
284
     * @param string $name the column name.
285
     *
286
     * @return string the attribute label.
287
     */
288 375
    private function generateAttributeLabel(string $name): string
289
    {
290 375
        return StringHelper::uppercaseFirstCharacterInEachWord(
291 375
            $this->getInflector()->toWords($name)
292
        );
293
    }
294
295
    /**
296
     * @return iterable|scalar|Stringable|null
297
     *
298
     * @psalm-suppress MixedReturnStatement
299
     * @psalm-suppress MixedInferredReturnType
300
     * @psalm-suppress MissingClosureReturnType
301
     */
302 687
    private function readProperty(string $attribute)
303
    {
304 687
        $class = static::class;
305
306 687
        [$attribute, $nested] = $this->getNestedAttribute($attribute);
307
308 686
        if (!property_exists($class, $attribute)) {
309 1
            throw new InvalidArgumentException("Undefined property: \"$class::$attribute\".");
310
        }
311
312
        /** @psalm-suppress MixedMethodCall */
313 685
        $getter = static fn (FormModelInterface $class, string $attribute) => $nested === null
314 685
            ? $class->$attribute
315 685
            : $class->$attribute->getAttributeValue($nested);
316
317 685
        $getter = Closure::bind($getter, null, $this);
318
319
        /** @var Closure $getter */
320 685
        return $getter($this, $attribute);
321
    }
322
323
    /**
324
     * @param string $attribute
325
     * @param iterable|object|scalar|Stringable|null $value
326
     *
327
     * @psalm-suppress MissingClosureReturnType
328
     */
329 71
    private function writeProperty(string $attribute, $value): void
330
    {
331 71
        [$attribute, $nested] = $this->getNestedAttribute($attribute);
332
333
        /**
334
         * @psalm-suppress MissingClosureParamType
335
         * @psalm-suppress MixedMethodCall
336
         */
337 71
        $setter = static fn (FormModelInterface $class, string $attribute, $value) => $nested === null
338 71
            ? $class->$attribute = $value
339 71
            : $class->$attribute->setAttribute($nested, $value);
340
341 71
        $setter = Closure::bind($setter, null, $this);
342
343
        /** @var Closure $setter */
344 71
        $setter($this, $attribute, $value);
345 71
    }
346
347
    /**
348
     * @return string[]
349
     *
350
     * @psalm-return array{0: string, 1: null|string}
351
     */
352 714
    private function getNestedAttribute(string $attribute): array
353
    {
354 714
        if (strpos($attribute, '.') === false) {
355 713
            return [$attribute, null];
356
        }
357
358 6
        [$attribute, $nested] = explode('.', $attribute, 2);
359
360
        /** @var object|string */
361 6
        $attributeNested = $this->attributes[$attribute] ?? null;
362
363 6
        if (!is_subclass_of($attributeNested, self::class)) {
364 1
            throw new InvalidArgumentException("Attribute \"$attribute\" is not a nested attribute.");
365
        }
366
367 5
        return [$attribute, $nested];
368
    }
369
370 409
    private function getNestedAttributeValue(string $method, string $attribute): string
371
    {
372 409
        $result = '';
373
374 409
        [$attribute, $nested] = $this->getNestedAttribute($attribute);
375
376 409
        if ($nested !== null) {
377
            /** @var FormModelInterface $attributeNestedValue */
378 3
            $attributeNestedValue = $this->getAttributeValue($attribute);
379
            /** @var string */
380 3
            $result = $attributeNestedValue->$method($nested);
381
        }
382
383 409
        return $result;
384
    }
385
386 5
    public function isValidated(): bool
387
    {
388 5
        return $this->validated;
389
    }
390
}
391