Completed
Branch feature/pre-split (364e4e)
by Anton
03:31
created

AbstractEntity::isFillable()

Size

Total Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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