Completed
Branch feature/split-orm (60a911)
by Anton
03:15
created

AbstractEntity::create()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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