AbstractEntity   C
last analyzed

Complexity

Total Complexity 57

Size/Duplication

Total Lines 330
Duplicated Lines 0 %

Test Coverage

Coverage 98.02%

Importance

Changes 0
Metric Value
wmc 57
eloc 78
dl 0
loc 330
ccs 99
cts 101
cp 0.9802
rs 5.04
c 0
b 0
f 0

27 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A setValue() 0 3 1
A offsetSet() 0 3 1
A isNullable() 0 3 1
A offsetUnset() 0 3 1
A createValue() 0 14 3
A __unset() 0 3 1
A getFields() 0 8 2
A offsetExists() 0 3 1
A __set() 0 3 1
A flushFields() 0 3 1
A setMutated() 0 12 3
A jsonSerialize() 0 3 1
A getKeys() 0 3 1
A getMutated() 0 14 4
A __destruct() 0 3 1
A getValue() 0 8 3
A thoughValue() 0 14 3
B setFields() 0 17 7
A __get() 0 3 1
A hasField() 0 7 3
A setField() 0 28 6
A __isset() 0 3 1
A toArray() 0 3 1
A getIterator() 0 3 1
A getField() 0 18 6
A offsetGet() 0 3 1

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Spiral\Models;
6
7
use Spiral\Models\Exception\AccessException;
8
use Spiral\Models\Exception\AccessExceptionInterface;
9
use Spiral\Models\Exception\EntityException;
10
11
/**
12
 * AbstractEntity with ability to define field mutators and access
13
 *
14
 * @implements \IteratorAggregate<string, mixed>
15
 */
16
abstract class AbstractEntity implements EntityInterface, ValueInterface, \IteratorAggregate
17
{
18 43
    public function __construct(
19
        private array $fields = []
20
    ) {
21 43
    }
22
23
    /**
24
     * Destruct data entity.
25
     */
26 43
    public function __destruct()
27
    {
28 43
        $this->flushFields();
29
    }
30
31 2
    public function __isset(string $offset): bool
32
    {
33 2
        return $this->hasField($offset);
34
    }
35
36 5
    public function __get(string $offset): mixed
37
    {
38 5
        return $this->getField($offset);
39
    }
40
41 6
    public function __set(string $offset, mixed $value): void
42
    {
43 6
        $this->setField($offset, $value);
44
    }
45
46 1
    public function __unset(string $offset): void
47
    {
48 1
        unset($this->fields[$offset]);
49
    }
50
51 10
    public function hasField(string $name): bool
52
    {
53 10
        if (!\array_key_exists($name, $this->fields)) {
54 2
            return false;
55
        }
56
57 10
        return $this->fields[$name] !== null || $this->isNullable($name);
58
    }
59
60
    /**
61
     * @param bool $filter If false, associated field setter or accessor will be ignored.
62
     *
63
     * @throws AccessException
64
     */
65 12
    public function setField(string $name, mixed $value, bool $filter = true): self
66
    {
67 12
        if ($value instanceof ValueInterface) {
68
            //In case of non scalar values filters must be bypassed (check accessor compatibility?)
69 1
            $this->fields[$name] = clone $value;
70
71 1
            return $this;
72
        }
73
74 12
        if (!$filter || (\is_null($value) && $this->isNullable($name))) {
75
            //Bypassing all filters
76 1
            $this->fields[$name] = $value;
77
78 1
            return $this;
79
        }
80
81
        //Checking if field have accessor
82 12
        $accessor = $this->getMutator($name, ModelSchema::MUTATOR_ACCESSOR);
83
84 12
        if ($accessor !== null) {
85
            //Setting value thought associated accessor
86 2
            $this->thoughValue($accessor, $name, $value);
87
        } else {
88
            //Setting value thought setter filter (if any)
89 10
            $this->setMutated($name, $value);
90
        }
91
92 11
        return $this;
93
    }
94
95
    /**
96
     * @param bool $filter If false, associated field getter will be ignored.
97
     *
98
     * @throws AccessException
99
     */
100 10
    public function getField(string $name, mixed $default = null, bool $filter = true): mixed
101
    {
102 10
        $value = $this->hasField($name) ? $this->fields[$name] : $default;
103
104 10
        if ($value instanceof ValueInterface || (\is_null($value) && $this->isNullable($name))) {
105
            //Direct access to value when value is accessor or null and declared as nullable
106 2
            return $value;
107
        }
108
109
        //Checking if field have accessor (decorator)
110 9
        $accessor = $this->getMutator($name, ModelSchema::MUTATOR_ACCESSOR);
111
112 9
        if (!empty($accessor)) {
113 1
            return $this->fields[$name] = $this->createValue($accessor, $name, $value);
114
        }
115
116
        //Getting value though getter
117 8
        return $this->getMutated($name, $filter, $value);
118
    }
119
120
    /**
121
     * @param bool $all Fill all fields including non fillable.
122
     *
123
     * @throws AccessException
124
     *
125
     * @see   $secured
126
     * @see   isFillable()
127
     * @see   $fillable
128
     */
129 10
    public function setFields(iterable $fields = [], bool $all = false): self
130
    {
131 10
        if (!\is_array($fields) && !$fields instanceof \Traversable) {
132
            return $this;
133
        }
134
135 10
        foreach ($fields as $name => $value) {
136 9
            if ($all || $this->isFillable($name)) {
137
                try {
138 8
                    $this->setField($name, $value, true);
139
                } catch (AccessExceptionInterface) {
140
                    // We are suppressing field setting exceptions
141
                }
142
            }
143
        }
144
145 10
        return $this;
146
    }
147
148
    /**
149
     * Every getter and accessor will be applied/constructed if filter argument set to true.
150
     *
151
     * @throws AccessException
152
     */
153 8
    public function getFields(bool $filter = true): array
154
    {
155 8
        $result = [];
156 8
        foreach (\array_keys($this->fields) as $name) {
157 7
            $result[$name] = $this->getField($name, null, $filter);
158
        }
159
160 8
        return $result;
161
    }
162
163 1
    public function offsetExists(mixed $offset): bool
164
    {
165 1
        return $this->__isset($offset);
166
    }
167
168 1
    public function offsetGet(mixed $offset): mixed
169
    {
170 1
        return $this->getField($offset);
171
    }
172
173 1
    public function offsetSet(mixed $offset, mixed $value): void
174
    {
175 1
        $this->setField($offset, $value);
176
    }
177
178 1
    public function offsetUnset(mixed $offset): void
179
    {
180 1
        $this->__unset($offset);
181
    }
182
183 1
    public function getIterator(): \Iterator
184
    {
185 1
        return new \ArrayIterator($this->getFields());
186
    }
187
188
    /**
189
     * AccessorInterface dependency.
190
     */
191 4
    public function setValue(mixed $data): self
192
    {
193 4
        return $this->setFields($data);
194
    }
195
196
    /**
197
     * Pack entity fields into plain array.
198
     *
199
     * @throws AccessException
200
     */
201 31
    public function getValue(): array
202
    {
203 31
        $result = [];
204 31
        foreach ($this->fields as $field => $value) {
205 28
            $result[$field] = $value instanceof ValueInterface ? $value->getValue() : $value;
206
        }
207
208 31
        return $result;
209
    }
210
211
    /**
212
     * Alias for packFields.
213
     */
214 24
    public function toArray(): array
215
    {
216 24
        return $this->getValue();
217
    }
218
219
    /**
220
     * By default use publicFields to be json serialized.
221
     */
222 2
    public function jsonSerialize(): array
223
    {
224 2
        return $this->getValue();
225
    }
226
227
    /**
228
     * @return int[]|string[]
229
     *
230
     * @psalm-return list<array-key>
231
     */
232 1
    protected function getKeys(): array
233
    {
234 1
        return \array_keys($this->fields);
235
    }
236
237
    /**
238
     * Reset every field value.
239
     */
240 43
    protected function flushFields(): void
241
    {
242 43
        $this->fields = [];
243
    }
244
245
    /**
246
     * Check if field is fillable.
247
     */
248
    abstract protected function isFillable(string $field): bool;
249
250
    /**
251
     * Get mutator associated with given field.
252
     *
253
     * @param string $type See MUTATOR_* constants
254
     */
255
    abstract protected function getMutator(string $field, string $type): mixed;
256
257
    /**
258
     * Nullable fields would not require automatic accessor creation.
259
     */
260 2
    protected function isNullable(string $field): bool
0 ignored issues
show
Unused Code introduced by
The parameter $field is not used and could be removed. ( Ignorable by Annotation )

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

260
    protected function isNullable(/** @scrutinizer ignore-unused */ string $field): bool

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
261
    {
262 2
        return false;
263
    }
264
265
    /**
266
     * Create instance of field accessor.
267
     *
268
     * @param mixed|string $type    Might be entity implementation specific.
269
     * @param array        $context Custom accessor context.
270
     *
271
     * @throws AccessException
272
     * @throws EntityException
273
     */
274 3
    protected function createValue(
275
        $type,
276
        string $name,
277
        mixed $value,
278
        array $context = []
279
    ): ValueInterface {
280 3
        if (!\is_string($type) || !\class_exists($type)) {
281 1
            throw new EntityException(
282 1
                \sprintf('Unable to create accessor for field `%s` in ', $name) . static::class
283 1
            );
284
        }
285
286
        // field as a context, this is the default convention
287 2
        return new $type($value, $context + ['field' => $name, 'entity' => $this]);
288
    }
289
290
    /**
291
     * Get value thought associated mutator.
292
     */
293 8
    private function getMutated(string $name, bool $filter, mixed $value): mixed
294
    {
295 8
        $getter = $this->getMutator($name, ModelSchema::MUTATOR_GETTER);
296
297 8
        if ($filter && !empty($getter)) {
298
            try {
299 2
                return \call_user_func($getter, $value);
300 1
            } catch (\Exception) {
301
                //Trying to filter null value, every filter must support it
302 1
                return \call_user_func($getter, null);
303
            }
304
        }
305
306 6
        return $value;
307
    }
308
309
    /**
310
     * Set value thought associated mutator.
311
     */
312 10
    private function setMutated(string $name, mixed $value): void
313
    {
314 10
        $setter = $this->getMutator($name, ModelSchema::MUTATOR_SETTER);
315
316 10
        if (!empty($setter)) {
317
            try {
318 2
                $this->fields[$name] = \call_user_func($setter, $value);
319 2
            } catch (\Exception) {
320
                //Exceptional situation, we are choosing to keep original field value
321
            }
322
        } else {
323 9
            $this->fields[$name] = $value;
324
        }
325
    }
326
327
    /**
328
     * Set value in/thought associated accessor.
329
     *
330
     * @param string|array $type Accessor definition (implementation specific).
331
     */
332 2
    private function thoughValue(array|string $type, string $name, mixed $value): void
333
    {
334 2
        $field = $this->fields[$name] ?? null;
335
336 2
        if (empty($field) || !($field instanceof ValueInterface)) {
337
            //New field representation
338 2
            $field = $this->createValue($type, $name, $value);
339
340
            //Save accessor with other fields
341 1
            $this->fields[$name] = $field;
342
        }
343
344
        //Letting accessor to set value
345 1
        $field->setValue($value);
346
    }
347
}
348