Completed
Branch feature/pre-split (cb15b4)
by Anton
03:23
created

AbstractEntity::publicValue()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 11
nc 4
nop 0
dl 0
loc 21
rs 9.0534
c 0
b 0
f 0
1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
namespace Spiral\Models\Prototypes;
9
10
use Spiral\Models\AccessorInterface;
11
use Spiral\Models\EntityInterface;
12
use Spiral\Models\Exceptions\AccessorExceptionInterface;
13
use Spiral\Models\Exceptions\EntityException;
14
use Spiral\Models\Exceptions\FieldExceptionInterface;
15
use Spiral\Models\PublishableInterface;
16
use Spiral\Models\Traits\EventsTrait;
17
use Spiral\ODM\Exceptions\FieldException;
18
19
/**
20
 * AbstractEntity with ability to define field mutators and access
21
 */
22
abstract class AbstractEntity extends MutableObject implements
23
    EntityInterface,
24
    \JsonSerializable,
25
    \IteratorAggregate,
26
    AccessorInterface,
27
    PublishableInterface
28
{
29
    use EventsTrait;
30
31
    /**
32
     * Field mutators.
33
     *
34
     * @private
35
     */
36
    const MUTATOR_GETTER   = 'getter';
37
    const MUTATOR_SETTER   = 'setter';
38
    const MUTATOR_ACCESSOR = 'accessor';
39
40
    /**
41
     * @var array
42
     */
43
    private $fields = [];
44
45
    /**
46
     * @param array $fields
47
     */
48
    public function __construct(array $fields = [])
49
    {
50
        $this->fields = $fields;
51
52
        //Initiating mutable object
53
        static::initialize(false);
54
    }
55
56
    /**
57
     * AccessorInterface dependency.
58
     *
59
     * {@inheritdoc}
60
     */
61
    public function stateValue($data)
62
    {
63
        return $this->setFields($data);
64
    }
65
66
    /**
67
     * AccessorInterface dependency.
68
     *
69
     * {@inheritdoc}
70
     */
71
    public function packValue()
72
    {
73
        return $this->packFields();
74
    }
75
76
    /**
77
     * {@inheritdoc}
78
     */
79
    public function hasField(string $name): bool
80
    {
81
        return array_key_exists($name, $this->fields);
82
    }
83
84
    /**
85
     * {@inheritdoc}
86
     *
87
     * @param bool $filter If false, associated field setter or accessor will be ignored.
88
     *
89
     * @throws AccessorExceptionInterface
90
     * @throws FieldException
91
     */
92
    public function setField(string $name, $value, bool $filter = true)
93
    {
94
        if ($value instanceof AccessorInterface) {
95
            //In case of non scalar values filters must be bypassed (check accessor compatibility?)
96
            $this->fields[$name] = clone $value;
97
98
            return;
99
        }
100
101
        if (!$filter || (is_null($value) && $this->isNullable($name))) {
102
            //Bypassing all filters
103
            $this->fields[$name] = $value;
104
105
            return;
106
        }
107
108
        //Checking if field have accessor
109
        $accessor = $this->getMutator($name, self::MUTATOR_ACCESSOR);
110
111
        if (!empty($accessor)) {
112
            //Setting value thought associated accessor
113
            $this->setAccessed($accessor, $name, $value);
114
        } else {
115
            //Setting value thought setter filter (if any)
116
            $this->setMutated($name, $value);
117
        }
118
    }
119
120
    /**
121
     * {@inheritdoc}
122
     *
123
     * @param bool $filter If false, associated field getter will be ignored.
124
     *
125
     * @throws AccessorExceptionInterface
126
     */
127
    public function getField(string $name, $default = null, bool $filter = true)
128
    {
129
        $value = $this->hasField($name) ? $this->fields[$name] : $default;
130
131
        if ($value instanceof AccessorInterface || (is_null($value) && $this->isNullable($name))) {
132
            //Direct access to value when value is accessor or null and declared as nullable
133
            return $value;
134
        }
135
136
        //Checking if field have accessor (decorator)
137
        $accessor = $this->getMutator($name, self::MUTATOR_ACCESSOR);
138
139
        if (!empty($accessor)) {
140
            return $this->fields[$name] = $this->createAccessor($accessor, $name, $value);
141
        }
142
143
        //Getting value though getter
144
        return $this->getMutated($name, $filter, $value);
145
    }
146
147
    /**
148
     * {@inheritdoc}
149
     *
150
     * @see   $fillable
151
     * @see   $secured
152
     * @see   isFillable()
153
     *
154
     * @param array|\Traversable $fields
155
     * @param bool               $all Fill all fields including non fillable.
156
     *
157
     * @return $this
158
     *
159
     * @throws AccessorExceptionInterface
160
     */
161
    public function setFields($fields = [], bool $all = false)
162
    {
163
        if (!is_array($fields) && !$fields instanceof \Traversable) {
164
            return $this;
165
        }
166
167
        foreach ($fields as $name => $value) {
168
            if ($all || $this->isFillable($name)) {
169
                try {
170
                    $this->setField($name, $value, true);
171
                } catch (FieldExceptionInterface $e) {
172
                    //We are supressing field setting exceptions
173
                }
174
            }
175
        }
176
177
        return $this;
178
    }
179
180
    /**
181
     * @return array
182
     */
183
    protected function getKeys(): array
184
    {
185
        return array_keys($this->fields);
186
    }
187
188
    /**
189
     * {@inheritdoc}
190
     *
191
     * Every getter and accessor will be applied/constructed if filter argument set to true.
192
     *
193
     * @param bool $filter
194
     *
195
     * @throws AccessorExceptionInterface
196
     */
197
    public function getFields(bool $filter = true): array
198
    {
199
        $result = [];
200
        foreach ($this->fields as $name => $field) {
201
            $result[$name] = $this->getField($name, null, $filter);
202
        }
203
204
        return $result;
205
    }
206
207
    /**
208
     * @param mixed $offset
209
     *
210
     * @return bool
211
     */
212
    public function __isset($offset)
213
    {
214
        return $this->hasField($offset);
215
    }
216
217
    /**
218
     * @param mixed $offset
219
     *
220
     * @return mixed
221
     */
222
    public function __get($offset)
223
    {
224
        return $this->getField($offset);
225
    }
226
227
    /**
228
     * @param mixed $offset
229
     * @param mixed $value
230
     */
231
    public function __set($offset, $value)
232
    {
233
        $this->setField($offset, $value);
234
    }
235
236
    /**
237
     * @param mixed $offset
238
     */
239
    public function __unset($offset)
240
    {
241
        unset($this->fields[$offset]);
242
    }
243
244
    /**
245
     * {@inheritdoc}
246
     */
247
    public function offsetExists($offset)
248
    {
249
        return $this->__isset($offset);
250
    }
251
252
    /**
253
     * {@inheritdoc}
254
     */
255
    public function offsetGet($offset)
256
    {
257
        return $this->getField($offset);
258
    }
259
260
    /**
261
     * {@inheritdoc}
262
     */
263
    public function offsetSet($offset, $value)
264
    {
265
        $this->setField($offset, $value);
266
    }
267
268
    /**
269
     * {@inheritdoc}
270
     */
271
    public function offsetUnset($offset)
272
    {
273
        $this->__unset($offset);
274
    }
275
276
    /**
277
     * {@inheritdoc}
278
     */
279
    public function getIterator(): \Iterator
280
    {
281
        return new \ArrayIterator($this->getFields());
282
    }
283
284
    /**
285
     * Pack entity fields data into plain array.
286
     *
287
     * @return array
288
     *
289
     * @throws AccessorExceptionInterface
290
     */
291
    public function packFields(): array
292
    {
293
        $result = [];
294
        foreach ($this->fields as $field => $value) {
295
            if ($value instanceof AccessorInterface) {
296
                $result[$field] = $value->packValue();
297
            } else {
298
                $result[$field] = $value;
299
            }
300
        }
301
302
        return $result;
303
    }
304
305
    /**
306
     * {@inheritdoc}
307
     *
308
     * Include every composition public data into result.
309
     */
310
    public function publicValue(): array
311
    {
312
        $result = [];
313
314
        foreach ($this->getKeys() as $field) {
315
            if (!$this->isPublic($field)) {
316
                //We might need to use isset in future, for performance, for science
317
                continue;
318
            }
319
320
            $value = $this->getField($field);
321
322
            if ($value instanceof PublishableInterface) {
323
                $result[$field] = $value->publicValue();
324
            } else {
325
                $result[$field] = $value;
326
            }
327
        }
328
329
        return $result;
330
    }
331
332
    /**
333
     * Alias for packFields.
334
     *
335
     * @return array
336
     */
337
    public function toArray(): array
338
    {
339
        return $this->packFields();
340
    }
341
342
    /**
343
     * {@inheritdoc}
344
     *
345
     * By default use publicFields to be json serialized.
346
     */
347
    public function jsonSerialize()
348
    {
349
        return $this->publicValue();
350
    }
351
352
    /**
353
     * Destruct data entity.
354
     */
355
    public function __destruct()
356
    {
357
        $this->flushFields();
358
    }
359
360
    /**
361
     * Reset every field value.
362
     */
363
    protected function flushFields()
364
    {
365
        $this->fields = [];
366
    }
367
368
    /**
369
     * Indication that field in public and can be presented in published data.
370
     *
371
     * @param string $field
372
     *
373
     * @return bool
374
     */
375
    abstract protected function isPublic(string $field): bool;
376
377
    /**
378
     * Check if field is fillable.
379
     *
380
     * @param string $field
381
     *
382
     * @return bool
383
     */
384
    abstract protected function isFillable(string $field): bool;
385
386
    /**
387
     * Get mutator associated with given field.
388
     *
389
     * @param string $field
390
     * @param string $type See MUTATOR_* constants
391
     *
392
     * @return mixed
393
     */
394
    abstract protected function getMutator(string $field, string $type);
395
396
    /**
397
     * Nullable fields would not require automatic accessor creation.
398
     *
399
     * @param string $field
400
     *
401
     * @return bool
402
     */
403
    protected function isNullable(string $field): bool
404
    {
405
        return false;
406
    }
407
408
    /**
409
     * Create instance of field accessor.
410
     *
411
     * @param mixed|string $accessor Might be entity implementation specific.
412
     * @param string       $name
413
     * @param mixed        $value
414
     * @param array        $context  Custom accessor context.
415
     *
416
     * @return AccessorInterface|null
417
     *
418
     * @throws AccessorExceptionInterface
419
     * @throws EntityException
420
     */
421
    protected function createAccessor(
422
        $accessor,
423
        string $name,
424
        $value,
425
        array $context = []
426
    ): AccessorInterface {
427
        if (!is_string($accessor) || !class_exists($accessor)) {
428
            throw new EntityException(
429
                "Unable to create accessor for field {$name} in " . static::class
430
            );
431
        }
432
433
        //Field as a context
434
        return new $accessor($value, $context + ['field' => $name, 'entity' => $this]);
435
    }
436
437
    /**
438
     * Get value thought associated mutator.
439
     *
440
     * @param string $name
441
     * @param bool   $filter
442
     * @param mixed  $value
443
     *
444
     * @return mixed
445
     */
446
    private function getMutated(string $name, bool $filter, $value)
447
    {
448
        $getter = $this->getMutator($name, self::MUTATOR_GETTER);
449
450
        if ($filter && !empty($getter)) {
451
            try {
452
                return call_user_func($getter, $value);
453
            } catch (\Exception $e) {
454
                //Trying to filter null value, every filter must support it
455
                return call_user_func($getter, null);
456
            }
457
        }
458
459
        return $value;
460
    }
461
462
    /**
463
     * Set value thought associated mutator.
464
     *
465
     * @param string $name
466
     * @param mixed  $value
467
     */
468
    private function setMutated(string $name, $value)
469
    {
470
        $setter = $this->getMutator($name, self::MUTATOR_SETTER);
471
472
        if (!empty($setter)) {
473
            try {
474
                $this->fields[$name] = call_user_func($setter, $value);
475
            } catch (\Exception $e) {
476
                //Exceptional situation, we are choosing to keep original field value
477
            }
478
        } else {
479
            $this->fields[$name] = $value;
480
        }
481
    }
482
483
    /**
484
     * Set value in/thought associated accessor.
485
     *
486
     * @param string       $name
487
     * @param string|array $accessor Accessor definition (implementation specific).
488
     * @param mixed        $value
489
     */
490
    private function setAccessed($accessor, string $name, $value)
491
    {
492
        if (array_key_exists($name, $this->fields)) {
493
            $field = $this->fields[$name];
494
        } else {
495
            $field = null;
496
        }
497
498
        if (empty($field) || !($field instanceof AccessorInterface)) {
499
            //New field representation
500
            $field = $this->createAccessor($accessor, $name, $value);
501
502
            //Save accessor with other fields
503
            $this->fields[$name] = $field;
504
        }
505
506
        //Letting accessor to set value
507
        $field->stateValue($value);
508
    }
509
}