Passed
Pull Request — master (#160)
by Wilmer
03:13
created

FormModel   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 365
Duplicated Lines 0 %

Test Coverage

Coverage 99.3%

Importance

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

26 Methods

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