Completed
Push — master ( 125592...50d3ce )
by Maxim
10:57 queued 07:00
created

AbstractForm   D

Complexity

Total Complexity 61

Size/Duplication

Total Lines 334
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 334
rs 4.054
c 0
b 0
f 0
wmc 61

16 Methods

Rating   Name   Duplication   Size   Complexity  
A setData() 0 5 1
A getValue() 0 3 1
A getErrors() 0 7 2
A getFirstErrors() 0 10 3
B normalize() 0 21 8
B filter() 0 24 6
A getData() 0 3 1
A setValue() 0 7 2
A isEmpty() 0 3 4
A resetErrors() 0 6 2
A getFirstError() 0 3 3
B __construct() 0 12 5
A addError() 0 6 2
A hasErrors() 0 3 1
C validate() 0 29 12
B call() 0 18 8

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
namespace WebComplete\form;
4
5
abstract class AbstractForm
6
{
7
    const REQUIRED = 'required';
8
9
    protected $data = [];
10
    protected $errors = [];
11
    protected $defaultError = 'error';
12
    protected $filtersObject;
13
    protected $validatorsObject;
14
    private $rules;
15
    private $filters;
16
17
    /**
18
     * AbstractForm constructor.
19
     *
20
     * @param null|array $rules
21
     * @param null|array $filters
22
     * @param $filtersObject
23
     * @param $validatorsObject
24
     */
25
    public function __construct($rules = null, $filters = null, $validatorsObject = null, $filtersObject = null)
26
    {
27
        if (is_object($filtersObject)) {
28
            $this->filtersObject = $filtersObject;
29
        }
30
        if (is_object($validatorsObject)) {
31
            $this->validatorsObject = $validatorsObject;
32
        }
33
34
        $this->rules = is_array($rules) ? array_merge($this->rules(), $rules) : $this->rules();
35
36
        $this->filters = is_array($filters) ? array_merge($this->filters(), $filters) : $this->filters();
37
    }
38
39
    /**
40
     * @return array [[field, validator, params, message], ...]
41
     *
42
     * validator - is a string equals to method of ValidatorsObject, method of the form or callable.
43
     * Validator should be declared as ($value, $params) : bool
44
     * @see \WebComplete\form\Validators
45
     *
46
     * required - is an internal validator, checks the field is not empty
47
     * * (asterisk) - rule for all fields (declared in rules or filter)
48
     *
49
     * example
50
     * ```
51
     * [
52
     *      [['name', 'email'], 'required', [], 'Field is required'],
53
     *      ['name', 'string', ['min' => 2, 'max' => 50], 'Incorrect name'],
54
     *      ['email', 'email', [], 'Incorrect email'],
55
     *      [['description', 'label']], // safe fields
56
     *      ['price', 'methodValidator', [], 'Incorrect'],
57
     *      ['some', [SomeValidator::class, 'method'], ['customParam' => 100], 'Incorrect'],
58
     *      [['*'], 'regex', ['pattern' => '/^[a-z]$/'], 'Field is required'],
59
     * ]
60
     * ```
61
     */
62
    abstract protected function rules(): array;
63
64
    /**
65
     * @return array [[field, filter, params], ...]
66
     *
67
     * filter - is a string equals to method of FiltersObject, method of the form or callable
68
     * Filter should be declared as ($value, $params) : mixed, and return filtered value
69
     * @see \WebComplete\form\Filters
70
     *
71
     * * (asterisk) - rule for all fields (declared in rules or filter)
72
     *
73
     * example
74
     * ```
75
     * [
76
     *      [['first_name', 'last_name'], 'capitalize'],
77
     *      ['description', 'stripTags'],
78
     *      ['content', 'stripJs'],
79
     *      ['email', 'replace', ['pattern' => 'email.com', 'to' => 'gmail.com']],
80
     *      ['*', 'trim'],
81
     * ]
82
     * ```
83
     *
84
     */
85
    abstract protected function filters(): array;
86
87
    /**
88
     * @return bool
89
     * @throws \WebComplete\form\FormException
90
     */
91
    public function validate(): bool
92
    {
93
        /** @var array[] $definitions */
94
        $definitions = $this->normalize($this->rules);
95
96
        $this->resetErrors();
97
        foreach ($definitions as $field => $fieldDefinitions) {
98
            foreach ($fieldDefinitions as $definition) {
99
                if ($definition[0] === self::REQUIRED && $this->isEmpty($this->getValue($field))) {
100
                    $this->addError($field, $definition[3] ?? $this->defaultError);
101
                }
102
            }
103
        }
104
        foreach ($this->getData() as $field => $value) {
105
            if (isset($definitions[$field])) {
106
                foreach ($definitions[$field] as $definition) {
107
                    $defName = array_shift($definition);
108
                    $defParams = array_merge([$value], [array_shift($definition)], [$this]);
109
                    $defMessage = array_shift($definition) ?: $this->defaultError;
110
111
                    if ($defName !== self::REQUIRED && !$this->isEmpty($value)
112
                        && !$this->call($defName, $defParams, $this->validatorsObject, true)) {
113
                        $this->addError($field, $defMessage);
114
                    }
115
                }
116
            }
117
        }
118
119
        return !$this->hasErrors();
120
    }
121
122
    /**
123
     * @return array
124
     */
125
    public function getData(): array
126
    {
127
        return $this->data;
128
    }
129
130
    /**
131
     * @param array $data
132
     *
133
     * @return $this
134
     * @throws \WebComplete\form\FormException
135
     */
136
    public function setData(array $data)
137
    {
138
        $this->data = $this->filter($data);
139
140
        return $this;
141
    }
142
143
    /**
144
     * @param $field
145
     *
146
     * @return mixed|null
147
     */
148
    public function getValue($field)
149
    {
150
        return $this->data[$field] ?? null;
151
    }
152
153
    /**
154
     * @param $field
155
     * @param $value
156
     * @param bool $filter
157
     *
158
     * @throws \WebComplete\form\FormException
159
     */
160
    public function setValue($field, $value, $filter = true): void
161
    {
162
        if ($filter) {
163
            $data = $this->filter([$field => $value]);
164
            $value = $data[$field] ?? null;
165
        }
166
        $this->data[$field] = $value;
167
    }
168
169
    /**
170
     * @param string $field
171
     */
172
    public function resetErrors($field = null): void
173
    {
174
        if ($field) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $field of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
175
            unset($this->errors[$field]);
176
        } else {
177
            $this->errors = [];
178
        }
179
    }
180
181
    /**
182
     * @param $field
183
     * @param $error
184
     */
185
    public function addError($field, $error): void
186
    {
187
        if (!isset($this->errors[$field])) {
188
            $this->errors[$field] = [];
189
        }
190
        $this->errors[$field][] = $error;
191
    }
192
193
    /**
194
     * @param string|null $field
195
     *
196
     * @return bool
197
     */
198
    public function hasErrors($field = null): bool
199
    {
200
        return count($this->getErrors($field)) > 0;
201
    }
202
203
    /**
204
     * @param string|null $field
205
     *
206
     * @return array
207
     */
208
    public function getErrors($field = null): array
209
    {
210
        if ($field) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $field of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
211
            return $this->errors[$field] ?? [];
212
        }
213
214
        return $this->errors;
215
    }
216
217
    /**
218
     * @return array
219
     */
220
    public function getFirstErrors(): array
221
    {
222
        $result = [];
223
        foreach ($this->getErrors() as $field => $errors) {
224
            if ($errors) {
225
                $result[$field] = reset($errors);
226
            }
227
        }
228
229
        return $result;
230
    }
231
232
    /**
233
     * @param $field
234
     *
235
     * @return string|null
236
     */
237
    public function getFirstError($field): ?string
238
    {
239
        return isset($this->errors[$field]) && $this->errors[$field] ? reset($this->errors[$field]) : null;
240
    }
241
242
    /**
243
     * @param array $data
244
     *
245
     * @return array
246
     * @throws \WebComplete\form\FormException
247
     */
248
    protected function filter(array $data): array
249
    {
250
        $filtersDefinitions = $this->normalize($this->filters);
251
        $rulesDefinitions = $this->normalize($this->rules);
252
253
        foreach ($data as $field => $value) {
254
            if (!isset($rulesDefinitions[$field]) && !isset($filtersDefinitions[$field])) {
255
                unset($data[$field]);
256
                continue;
257
            }
258
259
            $fieldDefinitions = $filtersDefinitions[$field] ?? [];
260
            if (isset($filtersDefinitions['*'])) {
261
                $fieldDefinitions = array_merge($fieldDefinitions, $filtersDefinitions['*']);
262
            }
263
264
            foreach ($fieldDefinitions as $definition) {
265
                $defName = array_shift($definition);
266
                $defParams = array_merge([$value], [array_shift($definition)], [$this]);
267
                $data[$field] = $this->call($defName, $defParams, $this->filtersObject, $value);
268
            }
269
        }
270
271
        return $data;
272
    }
273
274
    /**
275
     * @param $value
276
     *
277
     * @return bool
278
     */
279
    protected function isEmpty($value): bool
280
    {
281
        return $value === null || $value === '' || (is_array($value) && !count($value));
282
    }
283
284
    /**
285
     * @param $definitions
286
     *
287
     * @return array
288
     */
289
    private function normalize($definitions): array
290
    {
291
        $normalized = [];
292
        /** @var array[] $definitions */
293
        foreach ($definitions as $definition) {
294
            $fields = array_shift($definition);
295
            $defName = $definition ? array_shift($definition) : null;
296
            $defParams = $definition ? array_shift($definition) : [];
297
            $defMessage = $definition ? array_shift($definition) : '';
298
            if (!is_array($fields)) {
299
                $fields = [$fields];
300
            }
301
            foreach ($fields as $field) {
302
                if (!isset($normalized[$field])) {
303
                    $normalized[$field] = [];
304
                }
305
                $normalized[$field][] = [$defName, $defParams, $defMessage];
306
            }
307
        }
308
309
        return $normalized;
310
    }
311
312
    /**
313
     * @param $defName
314
     * @param $defParams
315
     * @param $object
316
     * @param $default
317
     *
318
     * @return mixed|null
319
     * @throws FormException
320
     */
321
    private function call($defName, $defParams, $object, $default)
322
    {
323
        $callable = $defName;
324
        if ($defName) {
325
            if (!is_array($defName)) {
326
                if (method_exists($this, $defName)) {
327
                    $callable = [$this, $defName];
328
                } elseif ($object && method_exists($object, $defName)) {
329
                    $callable = [$object, $defName];
330
                }
331
            }
332
333
            if (!is_callable($callable)) {
334
                throw new FormException('Callable not found: ' . json_encode($callable));
335
            }
336
        }
337
338
        return $callable ? call_user_func_array($callable, $defParams) : $default;
339
    }
340
}
341