Completed
Branch feature/pre-split (6ce9be)
by Anton
03:09
created

AbstractEntity::stateValue()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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