Test Failed
Push — validate-by-validator ( a015d2 )
by Dmitriy
03:11
created

FormModel   F

Complexity

Total Complexity 74

Size/Duplication

Total Lines 378
Duplicated Lines 0 %

Importance

Changes 8
Bugs 0 Features 0
Metric Value
eloc 144
dl 0
loc 378
rs 2.48
c 8
b 0
f 0
wmc 74

30 Methods

Rating   Name   Duplication   Size   Complexity  
A getAttributeValue() 0 3 1
A writeProperty() 0 19 4
A attributeLabels() 0 3 1
A isPublicAttribute() 0 3 1
A __construct() 0 4 1
A generateAttributeLabel() 0 4 1
A errorSummary() 0 10 3
A getRules() 0 3 1
A processValidationResult() 0 9 3
A addErrors() 0 5 3
A errors() 0 3 1
A setAttribute() 0 20 6
A hasErrors() 0 3 2
A collectAttributes() 0 23 4
A firstError() 0 7 2
A attributeLabel() 0 3 1
A attributeHints() 0 3 1
A formName() 0 12 3
A rules() 0 3 1
A error() 0 3 1
A isAttributeRequired() 0 14 5
A clearErrors() 0 9 2
A firstErrors() 0 15 4
A isValidated() 0 3 1
A readProperty() 0 23 5
A getAttributeLabel() 0 7 2
A getNestedAttribute() 0 13 3
A attributeHint() 0 10 2
A hasAttribute() 0 3 1
A load() 0 20 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 Yiisoft\Form\HtmlOptions\HtmlOptionsProvider;
11
use Yiisoft\Strings\Inflector;
12
use Yiisoft\Strings\StringHelper;
13
use Yiisoft\Validator\PostValidationHookInterface;
0 ignored issues
show
Bug introduced by
The type Yiisoft\Validator\PostValidationHookInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
14
use Yiisoft\Validator\ResultSet;
15
use Yiisoft\Validator\Rule\Required;
16
use Yiisoft\Validator\RulesProviderInterface;
0 ignored issues
show
Bug introduced by
The type Yiisoft\Validator\RulesProviderInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
17
use function array_key_exists;
18
use function array_merge;
19
use function explode;
20
use function get_object_vars;
21
use function is_subclass_of;
22
use function reset;
23
use function sprintf;
24
use function strpos;
25
26
/**
27
 * Form model represents an HTML form: its data, validation and presentation.
28
 */
29
abstract class FormModel implements FormModelInterface, PostValidationHookInterface, RulesProviderInterface
30
{
31
    private array $attributes;
32
    private array $attributesLabels;
33
    private array $attributesErrors = [];
34
    private ?Inflector $inflector = null;
35
    private bool $validated = false;
36
37
    public function __construct()
38
    {
39
        $this->attributes = $this->collectAttributes();
40
        $this->attributesLabels = $this->attributeLabels();
41
    }
42
43
    public function isAttributeRequired(string $attribute): bool
44
    {
45
        $validators = $this->rules()[$attribute] ?? [];
46
47
        foreach ($validators as $validator) {
48
            if ($validator instanceof Required) {
49
                return true;
50
            }
51
            if ($validator instanceof HtmlOptionsProvider && (bool)($validator->getHtmlOptions()['required'] ?? false)) {
52
                return true;
53
            }
54
        }
55
56
        return false;
57
    }
58
59
    public function getAttributeValue(string $attribute)
60
    {
61
        return $this->readProperty($attribute);
62
    }
63
64
    public function attributeLabels(): array
65
    {
66
        return [];
67
    }
68
69
    public function attributeHint(string $attribute): string
70
    {
71
        [$attribute, $nested] = $this->getNestedAttribute($attribute);
72
        if ($nested !== null) {
73
            return $this->readProperty($attribute)->attributeHint($nested);
74
        }
75
76
        $hints = $this->attributeHints();
77
78
        return $hints[$attribute] ?? '';
79
    }
80
81
    public function attributeHints(): array
82
    {
83
        return [];
84
    }
85
86
    public function attributeLabel(string $attribute): string
87
    {
88
        return $this->attributesLabels[$attribute] ?? $this->getAttributeLabel($attribute);
89
    }
90
91
    /**
92
     * @return string Returns classname without a namespace part or empty string when class is anonymous
93
     */
94
    public function formName(): string
95
    {
96
        if (strpos(static::class, '@anonymous') !== false) {
97
            return '';
98
        }
99
100
        $className = strrchr(static::class, '\\');
101
        if ($className === false) {
102
            return static::class;
103
        }
104
105
        return substr($className, 1);
106
    }
107
108
    public function hasAttribute(string $attribute): bool
109
    {
110
        return array_key_exists($attribute, $this->attributes);
111
    }
112
113
    public function error(string $attribute): array
114
    {
115
        return $this->attributesErrors[$attribute] ?? [];
116
    }
117
118
    public function errors(): array
119
    {
120
        return $this->attributesErrors;
121
    }
122
123
    public function errorSummary(bool $showAllErrors): array
124
    {
125
        $lines = [];
126
        $errors = $showAllErrors ? $this->errors() : [$this->firstErrors()];
127
128
        foreach ($errors as $error) {
129
            $lines = array_merge($lines, $error);
130
        }
131
132
        return $lines;
133
    }
134
135
    public function firstError(string $attribute): string
136
    {
137
        if (empty($this->attributesErrors[$attribute])) {
138
            return '';
139
        }
140
141
        return reset($this->attributesErrors[$attribute]);
142
    }
143
144
    public function firstErrors(): array
145
    {
146
        if (empty($this->attributesErrors)) {
147
            return [];
148
        }
149
150
        $errors = [];
151
152
        foreach ($this->attributesErrors as $name => $es) {
153
            if (!empty($es)) {
154
                $errors[$name] = reset($es);
155
            }
156
        }
157
158
        return $errors;
159
    }
160
161
    public function hasErrors(?string $attribute = null): bool
162
    {
163
        return $attribute === null ? !empty($this->attributesErrors) : isset($this->attributesErrors[$attribute]);
164
    }
165
166
    /**
167
     * @param array $data
168
     * @param string|null $formName
169
     *
170
     * @return bool
171
     */
172
    public function load(array $data, ?string $formName = null): bool
173
    {
174
        $scope = $formName ?? $this->formName();
175
176
        /**
177
         * @psalm-var array<string,mixed>
178
         */
179
        $values = [];
180
181
        if ($scope === '' && !empty($data)) {
182
            $values = $data;
183
        } elseif (isset($data[$scope])) {
184
            $values = $data[$scope];
185
        }
186
187
        foreach ($values as $name => $value) {
188
            $this->setAttribute($name, $value);
189
        }
190
191
        return $values !== [];
192
    }
193
194
    public function setAttribute(string $name, $value): void
195
    {
196
        [$realName] = $this->getNestedAttribute($name);
197
        if (isset($this->attributes[$realName])) {
198
            switch ($this->attributes[$realName]) {
199
                case 'bool':
200
                    $this->writeProperty($name, (bool) $value);
201
                    break;
202
                case 'float':
203
                    $this->writeProperty($name, (float) $value);
204
                    break;
205
                case 'int':
206
                    $this->writeProperty($name, (int) $value);
207
                    break;
208
                case 'string':
209
                    $this->writeProperty($name, (string) $value);
210
                    break;
211
                default:
212
                    $this->writeProperty($name, $value);
213
                    break;
214
            }
215
        }
216
    }
217
218
    public function processValidationResult(ResultSet $resultSet): void
219
    {
220
        $this->clearErrors();
221
        foreach ($resultSet as $attribute => $result) {
222
            if ($result->isValid() === false) {
223
                $this->addErrors([$attribute => $result->getErrors()]);
224
            }
225
        }
226
        $this->validated = true;
227
    }
228
229
    public function addError(string $attribute, string $error): void
230
    {
231
        $this->attributesErrors[$attribute][] = $error;
232
    }
233
234
    public function rules(): array
235
    {
236
        return [];
237
    }
238
239
    public function getRules(): array
240
    {
241
        return $this->rules();
242
    }
243
244
    /**
245
     * @param string[][] $items
246
     */
247
    private function addErrors(array $items): void
248
    {
249
        foreach ($items as $attribute => $errors) {
250
            foreach ($errors as $error) {
251
                $this->attributesErrors[$attribute][] = $error;
252
            }
253
        }
254
    }
255
256
    /**
257
     * Returns the list of attribute types indexed by attribute names.
258
     *
259
     * By default, this method returns all non-static properties of the class.
260
     *
261
     * @throws \ReflectionException
262
     *
263
     * @return array list of attribute types indexed by attribute names.
264
     */
265
    private function collectAttributes(): array
266
    {
267
        $class = new ReflectionClass($this);
268
        $attributes = [];
269
270
        foreach ($class->getProperties() as $property) {
271
            if ($property->isStatic()) {
272
                continue;
273
            }
274
275
            $type = $property->getType();
276
            if ($type === null) {
277
                throw new InvalidArgumentException(sprintf(
278
                    'You must specify the type hint for "%s" property in "%s" class.',
279
                    $property->getName(),
280
                    $property->getDeclaringClass()->getName(),
281
                ));
282
            }
283
284
            $attributes[$property->getName()] = $type->getName();
0 ignored issues
show
Bug introduced by
The method getName() does not exist on ReflectionType. It seems like you code against a sub-type of ReflectionType such as ReflectionNamedType. ( Ignorable by Annotation )

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

284
            /** @scrutinizer ignore-call */ 
285
            $attributes[$property->getName()] = $type->getName();
Loading history...
285
        }
286
287
        return $attributes;
288
    }
289
290
    private function clearErrors(?string $attribute = null): void
291
    {
292
        if ($attribute === null) {
293
            $this->attributesErrors = [];
294
        } else {
295
            unset($this->attributesErrors[$attribute]);
296
        }
297
298
        $this->validated = false;
299
    }
300
301
    private function getInflector(): Inflector
302
    {
303
        if ($this->inflector === null) {
304
            $this->inflector = new Inflector();
305
        }
306
        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...
307
    }
308
309
    private function getAttributeLabel(string $attribute): string
310
    {
311
        [$attribute, $nested] = $this->getNestedAttribute($attribute);
312
313
        return $nested !== null
314
            ? $this->readProperty($attribute)->attributeLabel($nested)
315
            : $this->generateAttributeLabel($attribute);
316
    }
317
318
    /**
319
     * Generates a user friendly attribute label based on the give attribute name.
320
     *
321
     * This is done by replacing underscores, dashes and dots with blanks and changing the first letter of each word to
322
     * upper case.
323
     *
324
     * For example, 'department_name' or 'DepartmentName' will generate 'Department Name'.
325
     *
326
     * @param string $name the column name.
327
     *
328
     * @return string the attribute label.
329
     */
330
    private function generateAttributeLabel(string $name): string
331
    {
332
        return StringHelper::uppercaseFirstCharacterInEachWord(
333
            $this->getInflector()->toWords($name)
334
        );
335
    }
336
337
    private function readProperty(string $attribute)
338
    {
339
        $class = static::class;
340
341
        [$attribute, $nested] = $this->getNestedAttribute($attribute);
342
343
        if (!property_exists($class, $attribute)) {
344
            throw new InvalidArgumentException("Undefined property: \"$class::$attribute\".");
345
        }
346
347
        if ($this->isPublicAttribute($attribute)) {
348
            return $nested === null ? $this->$attribute : $this->$attribute->getAttributeValue($nested);
349
        }
350
351
        $getter = fn (FormModel $class, $attribute) => $nested === null
352
            ? $class->$attribute
353
            : $class->$attribute->getAttributeValue($nested);
354
        $getter = Closure::bind($getter, null, $this);
355
356
        /**
357
         * @psalm-var Closure $getter
358
         */
359
        return $getter($this, $attribute);
360
    }
361
362
    private function writeProperty(string $attribute, $value): void
363
    {
364
        [$attribute, $nested] = $this->getNestedAttribute($attribute);
365
        if ($this->isPublicAttribute($attribute)) {
366
            if ($nested === null) {
367
                $this->$attribute = $value;
368
            } else {
369
                $this->$attribute->setAttribute($attribute, $value);
370
            }
371
        } else {
372
            $setter = fn (FormModel $class, $attribute, $value) => $nested === null
373
                ? $class->$attribute = $value
374
                : $class->$attribute->setAttribute($nested, $value);
375
            $setter = Closure::bind($setter, null, $this);
376
377
            /**
378
             * @psalm-var Closure $setter
379
             */
380
            $setter($this, $attribute, $value);
381
        }
382
    }
383
384
    private function isPublicAttribute(string $attribute): bool
385
    {
386
        return array_key_exists($attribute, get_object_vars($this));
387
    }
388
389
    private function getNestedAttribute(string $attribute): array
390
    {
391
        if (strpos($attribute, '.') === false) {
392
            return [$attribute, null];
393
        }
394
395
        [$attribute, $nested] = explode('.', $attribute, 2);
396
397
        if (!is_subclass_of($this->attributes[$attribute], self::class)) {
398
            throw new InvalidArgumentException('Nested attribute can only be of ' . self::class . ' type.');
399
        }
400
401
        return [$attribute, $nested];
402
    }
403
404
    public function isValidated(): bool
405
    {
406
        return $this->validated;
407
    }
408
}
409