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) |
|
|
|
|
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
|
|
|
} |
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.