Passed
Pull Request — master (#147)
by Wilmer
02:27
created

FormModel   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 378
Duplicated Lines 0 %

Test Coverage

Coverage 91.95%

Importance

Changes 10
Bugs 0 Features 0
Metric Value
eloc 121
dl 0
loc 378
ccs 137
cts 149
cp 0.9195
rs 4.5599
c 10
b 0
f 0
wmc 58

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