Completed
Branch feature/pre-split (afd44c)
by Anton
07:02
created

AbstractEntity::packFields()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 13
c 0
b 0
f 0
cc 3
eloc 8
nc 3
nop 0
rs 9.4285
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 Doctrine\Common\Inflector\Inflector;
11
use Spiral\Models\AccessorInterface;
12
use Spiral\Models\EntityInterface;
13
use Spiral\Models\Events\EntityEvent;
14
use Spiral\Models\Exceptions\AccessorExceptionInterface;
15
use Spiral\Models\Exceptions\EntityException;
16
use Spiral\Models\Exceptions\FieldExceptionInterface;
17
use Spiral\Models\PublishableInterface;
18
use Spiral\Models\Traits\EventsTrait;
19
use Spiral\Validation\ValueInterface;
20
21
/**
22
 * AbstractEntity with ability to define field mutators and access
23
 */
24
abstract class AbstractEntity extends MutableObject implements
25
    EntityInterface,
26
    \JsonSerializable,
27
    \IteratorAggregate,
28
    AccessorInterface,
29
    PublishableInterface
30
{
31
    use EventsTrait;
32
33
    /**
34
     * When option is set to true, entity will throw an event "constructed" after initiating object
35
     * paramaters.
36
     *
37
     * @protected
38
     */
39
    const EVENT_ON_CONSTRUCT = false;
40
41
    /**
42
     * Field format declares how entity must process magic setters and getters. Available values:
43
     * camelCase, tableize.
44
     *
45
     * @protected
46
     */
47
    const FIELD_FORMAT = 'camelCase';
48
49
    /**
50
     * Field mutators.
51
     *
52
     * @private
53
     */
54
    const MUTATOR_GETTER   = 'getter';
55
    const MUTATOR_SETTER   = 'setter';
56
    const MUTATOR_ACCESSOR = 'accessor';
57
58
    /**
59
     * @var array
60
     */
61
    private $fields = [];
62
63
    /**
64
     * @param array $fields
65
     *
66
     * @event constructed($entity)
67
     */
68
    public function __construct(array $fields = [])
69
    {
70
        $this->fields = $fields;
71
72
        //Initiating mutable object
73
        static::initialize(false);
74
75
        //Optional, firing event after entity construction
76
        static::EVENT_ON_CONSTRUCT && self::events()->dispatch('construct', new EntityEvent($this));
77
    }
78
79
    /**
80
     * Routes user function in format of (get|set)FieldName into (get|set)Field(fieldName, value).
81
     *
82
     * @see getFeld()
83
     * @see setField()
84
     *
85
     * @param string $method
86
     * @param array  $arguments
87
     *
88
     * @return $this|mixed|null|AccessorInterface
89
     *
90
     * @throws EntityException
91
     */
92
    public function __call(string $method, array $arguments)
93
    {
94
        if (method_exists($this, $method)) {
95
            throw new EntityException(
96
                "Method name '{$method}' is ambiguous and can not be used as magic setter"
97
            );
98
        }
99
100
        if (strlen($method) <= 3) {
101
            //Get/set needs exactly 0-1 argument
102
            throw new EntityException("Undefined method {$method}");
103
        }
104
105
        $field = substr($method, 3);
106
107
        switch (static::FIELD_FORMAT) {
108
            case 'camelCase':
109
                $field = Inflector::camelize($field);
110
                break;
111
            case 'tableize':
112
                $field = Inflector::tableize($field);
113
                break;
114
            default:
115
                throw new EntityException(
116
                    "Undefined field format '" . static::FIELD_FORMAT . "'"
117
                );
118
        }
119
120
        switch (substr($method, 0, 3)) {
121
            case 'get':
122
                return $this->getField($field);
123
            case 'set':
124
                if (count($arguments) === 1) {
125
                    $this->setField($field, $arguments[0]);
126
127
                    //setFieldA($a)->setFieldB($b)
1 ignored issue
show
Unused Code Comprehensibility introduced by
78% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
128
                    return $this;
129
                }
130
        }
131
132
        throw new EntityException("Undefined method {$method}");
133
    }
134
135
    /**
136
     * AccessorInterface dependency.
137
     *
138
     * {@inheritdoc}
139
     */
140
    public function setValue($data)
141
    {
142
        return $this->setFields($data);
143
    }
144
145
    /**
146
     * AccessorInterface dependency.
147
     *
148
     * {@inheritdoc}
149
     */
150
    public function packValue()
151
    {
152
        return $this->packFields();
153
    }
154
155
    /**
156
     * {@inheritdoc}
157
     */
158
    public function hasField(string $name): bool
159
    {
160
        return array_key_exists($name, $this->fields);
161
    }
162
163
    /**
164
     * {@inheritdoc}
165
     *
166
     * @param bool $filter If false, associated field setter or accessor will be ignored.
167
     *
168
     * @throws AccessorExceptionInterface
169
     */
170
    public function setField(string $name, $value, bool $filter = true)
171
    {
172
        if ($value instanceof AccessorInterface) {
173
            //In case of non scalar values filters must be bypassed
174
            $this->fields[$name] = clone $value;
175
176
            return;
177
        }
178
179
        if (!$filter) {
180
181
            //Bypassing all filters
182
            $this->fields[$name] = $value;
183
184
            return;
185
        }
186
187
        //Checking if field have accessor
188
        $accessor = $this->getMutator($name, self::MUTATOR_ACCESSOR);
189
190
        if (!empty($accessor)) {
191
            $field = $this->fields[$name];
192
            if (empty($field) || !($field instanceof AccessorInterface)) {
193
                $this->fields[$name] = $field = $this->createAccessor($accessor, $value);
194
            }
195
196
            //Letting accessor to set value
197
            $field->setValue($value);
198
199
            return;
200
        }
201
202
        //Checking field setter if any
203
        $setter = $this->getMutator($name, self::MUTATOR_SETTER);
204
205
        if (!empty($setter)) {
206
            try {
207
                $this->fields[$name] = call_user_func($setter, $value);
208
            } catch (\Exception $e) {
209
                //Exceptional situation, we are choosing to keep original field value
210
            }
211
        } else {
212
            $this->fields[$name] = $value;
213
        }
214
    }
215
216
    /**
217
     * {@inheritdoc}
218
     *
219
     * @param bool $filter If false, associated field getter will be ignored.
220
     *
221
     * @throws AccessorExceptionInterface
222
     */
223
    public function getField(string $name, $default = null, bool $filter = true)
224
    {
225
        $value = $this->hasField($name) ? $this->fields[$name] : $default;
226
227
        if ($value instanceof AccessorInterface) {
228
            return $value;
229
        }
230
231
        //Checking if field have accessor (decorator)
232
        $accessor = $this->getMutator($name, self::MUTATOR_ACCESSOR);
233
234
        if (!empty($accessor)) {
235
            return $this->fields[$name] = $this->createAccessor($accessor, $value);
236
        }
237
238
        //Checking for getter
239
        $getter = $this->getMutator($name, self::MUTATOR_GETTER);
240
241
        if ($filter && !empty($getter)) {
242
            try {
243
                return call_user_func($getter, $value);
244
            } catch (\Exception $e) {
245
                //Trying to filter null value, every filter must support it
246
                return call_user_func($getter, null);
247
            }
248
        }
249
250
        return $value;
251
    }
252
253
    /**
254
     * {@inheritdoc}
255
     *
256
     * @see   $fillable
257
     * @see   $secured
258
     * @see   isFillable()
259
     *
260
     * @param array|\Traversable $fields
261
     * @param bool               $all Fill all fields including non fillable.
262
     *
263
     * @return $this
264
     *
265
     * @throws AccessorExceptionInterface
266
     */
267
    public function setFields($fields = [], bool $all = false)
268
    {
269
        if (!is_array($fields) && !$fields instanceof \Traversable) {
270
            return $this;
271
        }
272
273
        foreach ($fields as $name => $value) {
274
            if ($all || $this->isFillable($name)) {
275
                try {
276
                    $this->setField($name, $value, true);
277
                } catch (FieldExceptionInterface $e) {
278
                    //We are supressing field setting exceptions
279
                }
280
            }
281
        }
282
283
        return $this;
284
    }
285
286
    /**
287
     * @return array
288
     */
289
    protected function getKeys(): array
290
    {
291
        return array_keys($this->fields);
292
    }
293
294
    /**
295
     * {@inheritdoc}
296
     *
297
     * Every getter and accessor will be applied/constructed if filter argument set to true.
298
     *
299
     * @param bool $filter
300
     *
301
     * @throws AccessorExceptionInterface
302
     */
303
    public function getFields(bool $filter = true): array
304
    {
305
        $result = [];
306
        foreach ($this->fields as $name => $field) {
307
            $result[$name] = $this->getField($name, null, $filter);
308
        }
309
310
        return $result;
311
    }
312
313
    /**
314
     * @param mixed $offset
315
     *
316
     * @return bool
317
     */
318
    public function __isset($offset)
319
    {
320
        return $this->hasField($offset);
321
    }
322
323
    /**
324
     * @param mixed $offset
325
     *
326
     * @return mixed
327
     */
328
    public function __get($offset)
329
    {
330
        return $this->getField($offset);
331
    }
332
333
    /**
334
     * @param mixed $offset
335
     * @param mixed $value
336
     */
337
    public function __set($offset, $value)
338
    {
339
        $this->setField($offset, $value);
340
    }
341
342
    /**
343
     * @param mixed $offset
344
     */
345
    public function __unset($offset)
346
    {
347
        unset($this->fields[$offset]);
348
    }
349
350
    /**
351
     * {@inheritdoc}
352
     */
353
    public function offsetExists($offset)
354
    {
355
        return $this->__isset($offset);
356
    }
357
358
    /**
359
     * {@inheritdoc}
360
     */
361
    public function offsetGet($offset)
362
    {
363
        return $this->getField($offset);
364
    }
365
366
    /**
367
     * {@inheritdoc}
368
     */
369
    public function offsetSet($offset, $value)
370
    {
371
        $this->setField($offset, $value);
372
    }
373
374
    /**
375
     * {@inheritdoc}
376
     */
377
    public function offsetUnset($offset)
378
    {
379
        $this->__unset($offset);
380
    }
381
382
    /**
383
     * {@inheritdoc}
384
     */
385
    public function getIterator(): \Iterator
386
    {
387
        return new \ArrayIterator($this->getFields());
388
    }
389
390
    /**
391
     * Pack entity fields data into plain array.
392
     *
393
     * @return array
394
     *
395
     * @throws AccessorExceptionInterface
396
     */
397
    public function packFields(): array
398
    {
399
        $result = [];
400
        foreach ($this->fields as $field => $value) {
401
            if ($value instanceof ValueInterface) {
402
                $result[$field] = $value->packValue();
403
            } else {
404
                $result[$field] = $value;
405
            }
406
        }
407
408
        return $result;
409
    }
410
411
    /**
412
     * {@inheritdoc}
413
     *
414
     * Include every composition public data into result.
415
     */
416
    public function publicFields(): array
417
    {
418
        $result = [];
419
420
        foreach ($this->getKeys() as $field => $value) {
421
            if (!$this->isPublic($field)) {
422
                //We might need to use isset in future, for performance, for science
423
                continue;
424
            }
425
426
            $value = $this->getField($field);
427
428
            if ($value instanceof PublishableInterface) {
429
                $result[$field] = $value->publicFields();
430
            } else {
431
                $result[$field] = $value;
432
            }
433
        }
434
435
        return $result;
436
    }
437
438
    /**
439
     * Alias for packFields.
440
     *
441
     * @return array
442
     */
443
    public function toArray(): array
444
    {
445
        return $this->packFields();
446
    }
447
448
    /**
449
     * {@inheritdoc}
450
     *
451
     * By default use publicFields to be json serialized.
452
     */
453
    public function jsonSerialize()
454
    {
455
        return $this->publicFields();
456
    }
457
458
    /**
459
     * Destruct data entity.
460
     */
461
    public function __destruct()
462
    {
463
        $this->fields = [];
464
    }
465
466
    /**
467
     * Indication that field in public and can be presented in published data.
468
     *
469
     * @param string $field
470
     *
471
     * @return bool
472
     */
473
    abstract protected function isPublic(string $field): bool;
474
475
    /**
476
     * Check if field is fillable.
477
     *
478
     * @param string $field
479
     *
480
     * @return bool
481
     */
482
    abstract protected function isFillable(string $field): bool;
483
484
    /**
485
     * Get mutator associated with given field.
486
     *
487
     * @param string $field
488
     * @param string $type See MUTATOR_* constants
489
     *
490
     * @return mixed
491
     */
492
    abstract protected function getMutator(string $field, string $type);
493
494
    /**
495
     * Create instance of field accessor.
496
     *
497
     * @param string $accessor
498
     * @param mixed  $value
499
     *
500
     * @return AccessorInterface
501
     *
502
     * @throws AccessorExceptionInterface
503
     */
504
    protected function createAccessor(string $accessor, $value): AccessorInterface
505
    {
506
        return new $accessor($value);
507
    }
508
509
    /**
510
     * Reset every field value.
511
     */
512
    protected function flushValues()
513
    {
514
        $this->fields = [];
515
    }
516
517
    /**
518
     * Create entity by passing fields thought setFields method
519
     *
520
     * @param array $fields
521
     *
522
     * @return AbstractEntity
523
     */
524
    public static function create($fields = [])
525
    {
526
        /**
527
         * @var self $entity
528
         */
529
        return (new static([]))->setFields($fields);
530
    }
531
}