Completed
Branch feature/pre-split (bbd802)
by Anton
02:54
created

AbstractEntity::hasField()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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