Test Failed
Pull Request — master (#147)
by Wilmer
02:42
created

FormModel   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 360
Duplicated Lines 0 %

Test Coverage

Coverage 99.29%

Importance

Changes 10
Bugs 0 Features 0
Metric Value
eloc 114
c 10
b 0
f 0
dl 0
loc 360
ccs 140
cts 141
cp 0.9929
rs 6
wmc 55

26 Methods

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

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