AbstractEntity   C
last analyzed

Complexity

Total Complexity 57

Size/Duplication

Total Lines 329
Duplicated Lines 0 %

Test Coverage

Coverage 98.02%

Importance

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

27 Methods

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

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