Completed
Branch feature/pre-split (54fe62)
by Anton
03:12
created

AbstractEntity::getField()   D

Complexity

Conditions 9
Paths 10

Size

Total Lines 29
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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