Passed
Push — master ( db5c4e...adaaa2 )
by Kirill
03:03
created

AbstractEntity::flushFields()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 0
1
<?php
2
3
/**
4
 * Spiral Framework.
5
 *
6
 * @license   MIT
7
 * @author    Anton Titov (Wolfy-J)
8
 */
9
10
declare(strict_types=1);
11
12
namespace Spiral\Models;
13
14
use Spiral\Models\Exception\AccessException;
15
use Spiral\Models\Exception\AccessExceptionInterface;
16
use Spiral\Models\Exception\EntityException;
17
18
/**
19
 * AbstractEntity with ability to define field mutators and access
20
 */
21
abstract class AbstractEntity implements EntityInterface, ValueInterface, \IteratorAggregate
22
{
23
    /** @var array */
24
    private $fields = [];
25
26
    /**
27
     * @param array $data
28
     */
29
    public function __construct(array $data = [])
30
    {
31
        $this->fields = $data;
32
    }
33
34
    /**
35
     * Destruct data entity.
36
     */
37
    public function __destruct()
38
    {
39
        $this->flushFields();
40
    }
41
42
    /**
43
     * @param mixed $offset
44
     * @return bool
45
     */
46
    public function __isset($offset)
47
    {
48
        return $this->hasField($offset);
49
    }
50
51
    /**
52
     * @param mixed $offset
53
     * @return mixed
54
     */
55
    public function __get($offset)
56
    {
57
        return $this->getField($offset);
58
    }
59
60
    /**
61
     * @param mixed $offset
62
     * @param mixed $value
63
     */
64
    public function __set($offset, $value): void
65
    {
66
        $this->setField($offset, $value);
67
    }
68
69
    /**
70
     * @param mixed $offset
71
     */
72
    public function __unset($offset): void
73
    {
74
        unset($this->fields[$offset]);
75
    }
76
77
    /**
78
     * {@inheritdoc}
79
     */
80
    public function hasField(string $name): bool
81
    {
82
        if (!array_key_exists($name, $this->fields)) {
83
            return false;
84
        }
85
86
        return $this->fields[$name] !== null || $this->isNullable($name);
87
    }
88
89
    /**
90
     * {@inheritdoc}
91
     *
92
     * @param bool $filter If false, associated field setter or accessor will be ignored.
93
     *
94
     * @throws AccessException
95
     */
96
    public function setField(string $name, $value, bool $filter = true): void
97
    {
98
        if ($value instanceof ValueInterface) {
99
            //In case of non scalar values filters must be bypassed (check accessor compatibility?)
100
            $this->fields[$name] = clone $value;
101
102
            return;
103
        }
104
105
        if (!$filter || (is_null($value) && $this->isNullable($name))) {
106
            //Bypassing all filters
107
            $this->fields[$name] = $value;
108
109
            return;
110
        }
111
112
        //Checking if field have accessor
113
        $accessor = $this->getMutator($name, ModelSchema::MUTATOR_ACCESSOR);
114
115
        if ($accessor !== null) {
116
            //Setting value thought associated accessor
117
            $this->thoughValue($accessor, $name, $value);
118
        } else {
119
            //Setting value thought setter filter (if any)
120
            $this->setMutated($name, $value);
121
        }
122
    }
123
124
    /**
125
     * {@inheritdoc}
126
     *
127
     * @param bool $filter If false, associated field getter will be ignored.
128
     *
129
     * @throws AccessException
130
     */
131
    public function getField(string $name, $default = null, bool $filter = true)
132
    {
133
        $value = $this->hasField($name) ? $this->fields[$name] : $default;
134
135
        if ($value instanceof ValueInterface || (is_null($value) && $this->isNullable($name))) {
136
            //Direct access to value when value is accessor or null and declared as nullable
137
            return $value;
138
        }
139
140
        //Checking if field have accessor (decorator)
141
        $accessor = $this->getMutator($name, ModelSchema::MUTATOR_ACCESSOR);
142
143
        if (!empty($accessor)) {
144
            return $this->fields[$name] = $this->createValue($accessor, $name, $value);
145
        }
146
147
        //Getting value though getter
148
        return $this->getMutated($name, $filter, $value);
149
    }
150
151
    /**
152
     * {@inheritdoc}
153
     *
154
     * @param array|\Traversable $fields
155
     * @param bool               $all Fill all fields including non fillable.
156
     * @return $this
157
     *
158
     * @throws AccessException
159
     *
160
     * @see   $secured
161
     * @see   isFillable()
162
     * @see   $fillable
163
     */
164
    public function setFields($fields = [], bool $all = false)
165
    {
166
        if (!is_array($fields) && !$fields instanceof \Traversable) {
0 ignored issues
show
introduced by
$fields is always a sub-type of Traversable.
Loading history...
167
            return $this;
168
        }
169
170
        foreach ($fields as $name => $value) {
171
            if ($all || $this->isFillable($name)) {
172
                try {
173
                    $this->setField($name, $value, true);
174
                } catch (AccessExceptionInterface $e) {
175
                    //We are suppressing field setting exceptions
176
                }
177
            }
178
        }
179
180
        return $this;
181
    }
182
183
    /**
184
     * {@inheritdoc}
185
     *
186
     * Every getter and accessor will be applied/constructed if filter argument set to true.
187
     *
188
     * @param bool $filter
189
     *
190
     * @throws AccessException
191
     */
192
    public function getFields(bool $filter = true): array
193
    {
194
        $result = [];
195
        foreach ($this->fields as $name => $field) {
196
            $result[$name] = $this->getField($name, null, $filter);
197
        }
198
199
        return $result;
200
    }
201
202
    /**
203
     * {@inheritdoc}
204
     */
205
    public function offsetExists($offset)
206
    {
207
        return $this->__isset($offset);
208
    }
209
210
    /**
211
     * {@inheritdoc}
212
     */
213
    public function offsetGet($offset)
214
    {
215
        return $this->getField($offset);
216
    }
217
218
    /**
219
     * {@inheritdoc}
220
     */
221
    public function offsetSet($offset, $value): void
222
    {
223
        $this->setField($offset, $value);
224
    }
225
226
    /**
227
     * {@inheritdoc}
228
     */
229
    public function offsetUnset($offset): void
230
    {
231
        $this->__unset($offset);
232
    }
233
234
    /**
235
     * {@inheritdoc}
236
     */
237
    public function getIterator(): \Iterator
238
    {
239
        return new \ArrayIterator($this->getFields());
240
    }
241
242
    /**
243
     * AccessorInterface dependency.
244
     *
245
     * {@inheritdoc}
246
     */
247
    public function setValue($data)
248
    {
249
        return $this->setFields($data);
250
    }
251
252
    /**
253
     * Pack entity fields into plain array.
254
     *
255
     * @return array
256
     *
257
     * @throws AccessException
258
     */
259
    public function getValue(): array
260
    {
261
        $result = [];
262
        foreach ($this->fields as $field => $value) {
263
            if ($value instanceof ValueInterface) {
264
                $result[$field] = $value->getValue();
265
            } else {
266
                $result[$field] = $value;
267
            }
268
        }
269
270
        return $result;
271
    }
272
273
    /**
274
     * Alias for packFields.
275
     *
276
     * @return array
277
     */
278
    public function toArray(): array
279
    {
280
        return $this->getValue();
281
    }
282
283
    /**
284
     * {@inheritdoc}
285
     *
286
     * By default use publicFields to be json serialized.
287
     */
288
    public function jsonSerialize()
289
    {
290
        return $this->getValue();
291
    }
292
293
    /**
294
     * @return array
295
     */
296
    protected function getKeys(): array
297
    {
298
        return array_keys($this->fields);
299
    }
300
301
    /**
302
     * Reset every field value.
303
     */
304
    protected function flushFields(): void
305
    {
306
        $this->fields = [];
307
    }
308
309
    /**
310
     * Check if field is fillable.
311
     *
312
     * @param string $field
313
     *
314
     * @return bool
315
     */
316
    abstract protected function isFillable(string $field): bool;
317
318
    /**
319
     * Get mutator associated with given field.
320
     *
321
     * @param string $field
322
     * @param string $type See MUTATOR_* constants
323
     *
324
     * @return mixed
325
     */
326
    abstract protected function getMutator(string $field, string $type);
327
328
    /**
329
     * Nullable fields would not require automatic accessor creation.
330
     *
331
     * @param string $field
332
     *
333
     * @return bool
334
     */
335
    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

335
    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...
336
    {
337
        return false;
338
    }
339
340
    /**
341
     * Create instance of field accessor.
342
     *
343
     * @param mixed|string $type    Might be entity implementation specific.
344
     * @param string       $name
345
     * @param mixed        $value
346
     * @param array        $context Custom accessor context.
347
     * @return ValueInterface|null
348
     *
349
     * @throws AccessException
350
     * @throws EntityException
351
     */
352
    protected function createValue(
353
        $type,
354
        string $name,
355
        $value,
356
        array $context = []
357
    ): ValueInterface {
358
        if (!is_string($type) || !class_exists($type)) {
359
            throw new EntityException(
360
                "Unable to create accessor for field `{$name}` in " . static::class
361
            );
362
        }
363
364
        // field as a context, this is the default convention
365
        return new $type($value, $context + ['field' => $name, 'entity' => $this]);
366
    }
367
368
    /**
369
     * Get value thought associated mutator.
370
     *
371
     * @param string $name
372
     * @param bool   $filter
373
     * @param mixed  $value
374
     * @return mixed
375
     */
376
    private function getMutated(string $name, bool $filter, $value)
377
    {
378
        $getter = $this->getMutator($name, ModelSchema::MUTATOR_GETTER);
379
380
        if ($filter && !empty($getter)) {
381
            try {
382
                return call_user_func($getter, $value);
383
            } catch (\Exception $e) {
384
                //Trying to filter null value, every filter must support it
385
                return call_user_func($getter, null);
386
            }
387
        }
388
389
        return $value;
390
    }
391
392
    /**
393
     * Set value thought associated mutator.
394
     *
395
     * @param string $name
396
     * @param mixed  $value
397
     */
398
    private function setMutated(string $name, $value): void
399
    {
400
        $setter = $this->getMutator($name, ModelSchema::MUTATOR_SETTER);
401
402
        if (!empty($setter)) {
403
            try {
404
                $this->fields[$name] = call_user_func($setter, $value);
405
            } catch (\Exception $e) {
406
                //Exceptional situation, we are choosing to keep original field value
407
            }
408
        } else {
409
            $this->fields[$name] = $value;
410
        }
411
    }
412
413
    /**
414
     * Set value in/thought associated accessor.
415
     *
416
     * @param string       $name
417
     * @param string|array $type Accessor definition (implementation specific).
418
     * @param mixed        $value
419
     */
420
    private function thoughValue($type, string $name, $value): void
421
    {
422
        if (array_key_exists($name, $this->fields)) {
423
            $field = $this->fields[$name];
424
        } else {
425
            $field = null;
426
        }
427
428
        if (empty($field) || !($field instanceof ValueInterface)) {
429
            //New field representation
430
            $field = $this->createValue($type, $name, $value);
431
432
            //Save accessor with other fields
433
            $this->fields[$name] = $field;
434
        }
435
436
        //Letting accessor to set value
437
        $field->setValue($value);
438
    }
439
}
440