Completed
Pull Request — master (#57)
by Thomas
02:29 queued 50s
created

Entity::__get()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
namespace ORM;
4
5
use ORM\Dbal\Error;
6
use ORM\Entity\GeneratesPrimaryKeys;
7
use ORM\Entity\Relations;
8
use ORM\Entity\Validation;
9
use ORM\EntityManager as EM;
10
use ORM\Exception\IncompletePrimaryKey;
11
use ORM\Exception\UnknownColumn;
12
13
/**
14
 * Definition of an entity
15
 *
16
 * The instance of an entity represents a row of the table and the statics variables and methods describe the database
17
 * table.
18
 *
19
 * This is the main part where your configuration efforts go. The following properties and methods are well documented
20
 * in the manual under [https://tflori.github.io/orm/entityDefinition.html](Entity Definition).
21
 *
22
 * @package ORM
23
 * @link    https://tflori.github.io/orm/entityDefinition.html Entity Definition
24
 * @author  Thomas Flori <[email protected]>
25
 */
26
abstract class Entity implements \Serializable
27
{
28
    use Validation, Relations;
29
30
    /** @deprecated Use Relation::OPT_CLASS instead */
31
    const OPT_RELATION_CLASS       = 'class';
32
    /** @deprecated Use Relation::OPT_CARDINALITY instead */
33
    const OPT_RELATION_CARDINALITY = 'cardinality';
34
    /** @deprecated Use Relation::OPT_REFERENCE instead */
35
    const OPT_RELATION_REFERENCE   = 'reference';
36
    /** @deprecated Use Relation::OPT_OPPONENT instead */
37
    const OPT_RELATION_OPPONENT    = 'opponent';
38
    /** @deprecated Use Relation::OPT_TABLE instead */
39
    const OPT_RELATION_TABLE       = 'table';
40
41
    /** The template to use to calculate the table name.
42
     * @var string */
43
    protected static $tableNameTemplate;
44
45
    /** The naming scheme to use for table names.
46
     * @var string */
47
    protected static $namingSchemeTable;
48
49
    /** The naming scheme to use for column names.
50
     * @var string */
51
    protected static $namingSchemeColumn;
52
53
    /** The naming scheme to use for method names.
54
     * @var string */
55
    protected static $namingSchemeMethods;
56
57
    /** The naming scheme to use for attributes.
58
     * @var string */
59
    protected static $namingSchemeAttributes;
60
61
    /** Fixed table name (ignore other settings)
62
     * @var string */
63
    protected static $tableName;
64
65
    /** The variable(s) used for primary key.
66
     * @var string[]|string */
67
    protected static $primaryKey = ['id'];
68
69
    /** Fixed column names (ignore other settings)
70
     * @var string[] */
71
    protected static $columnAliases = [];
72
73
    /** A prefix for column names.
74
     * @var string */
75
    protected static $columnPrefix;
76
77
    /** Whether or not the primary key is auto incremented.
78
     * @var bool */
79
    protected static $autoIncrement = true;
80
81
    /** Additional attributes to show in toArray method
82
     * @var array  */
83
    protected static $includedAttributes = [];
84
85
    /** Attributes to hide for toArray method (overruled by $attributes parameter)
86
     * @var array  */
87
    protected static $excludedAttributes = [];
88
89
    /** The reflections of the classes.
90
     * @internal
91
     * @var \ReflectionClass[] */
92
    protected static $reflections = [];
93
94
    /** The current data of a row.
95
     * @var mixed[] */
96
    protected $data = [];
97
98
    /** The original data of the row.
99
     * @var mixed[] */
100
    protected $originalData = [];
101
102
    /** The entity manager from which this entity got created
103
     * @var EM */
104
    protected $entityManager;
105
106
    /**
107
     * Constructor
108
     *
109
     * It calls ::onInit() after initializing $data and $originalData.
110
     *
111
     * @param mixed[] $data          The current data
112
     * @param EM      $entityManager The EntityManager that created this entity
113
     * @param bool    $fromDatabase  Whether or not the data comes from database
114 122
     */
115
    final public function __construct(array $data = [], EM $entityManager = null, $fromDatabase = false)
116 122
    {
117 14
        if ($fromDatabase) {
118
            $this->originalData = $data;
119 122
        }
120 122
        $this->data          = array_merge($this->data, $data);
121 122
        $this->entityManager = $entityManager ?: EM::getInstance(static::class);
122 122
        $this->onInit(!$fromDatabase);
123
    }
124
125
    /**
126
     * Get the column name of $attribute
127
     *
128
     * The column names can not be specified by template. Instead they are constructed by $columnPrefix and enforced
129
     * to $namingSchemeColumn.
130
     *
131
     * **ATTENTION**: If your overwrite this method remember that getColumnName(getColumnName($name)) have to be exactly
132
     * the same as getColumnName($name).
133
     *
134
     * @param string $attribute
135
     * @return string
136
     */
137 166
    public static function getColumnName($attribute)
138
    {
139 166
        if (isset(static::$columnAliases[$attribute])) {
140 6
            return static::$columnAliases[$attribute];
141
        }
142
143 164
        return EM::getInstance(static::class)->getNamer()
144 164
            ->getColumnName(static::class, $attribute, static::$columnPrefix, static::$namingSchemeColumn);
145
    }
146
147
    /**
148
     * Get the column name of $attribute
149
     *
150
     * The column names can not be specified by template. Instead they are constructed by $columnPrefix and enforced
151
     * to $namingSchemeColumn.
152
     *
153
     * **ATTENTION**: If your overwrite this method remember that getColumnName(getColumnName($name)) have to be exactly
154
     * the same as getColumnName($name).
155 72
     *
156
     * @param string $column
157 72
     * @return string
158
     */
159
    public static function getAttributeName($column)
160
    {
161
        $attributeName = array_search($column, static::$columnAliases);
162
        if ($attributeName !== false) {
163
            return $attributeName;
164
        }
165
166
        return EM::getInstance(static::class)->getNamer()
167
            ->getAttributeName($column, static::$columnPrefix, static::$namingSchemeAttributes);
168
    }
169 154
170
    /**
171 154
     * Create an entityFetcher for this entity
172 11
     *
173
     * @return EntityFetcher
174
     */
175 143
    public static function query()
176 143
    {
177
        return EM::getInstance(static::class)->fetch(static::class);
178
    }
179
180
    /**
181
     * Get the primary key vars
182
     *
183
     * The primary key can consist of multiple columns. You should configure the vars that are translated to these
184 19
     * columns.
185
     *
186 19
     * @return array
187
     */
188
    public static function getPrimaryKeyVars()
189
    {
190
        return !is_array(static::$primaryKey) ? [ static::$primaryKey ] : static::$primaryKey;
191
    }
192
193 12
    /**
194
     * Get the table name
195 12
     *
196 12
     * The table name is constructed by $tableNameTemplate and $namingSchemeTable. It can be overwritten by
197
     * $tableName.
198
     *
199
     * @return string
200
     */
201
    public static function getTableName()
202
    {
203
        if (static::$tableName) {
204
            return static::$tableName;
205
        }
206
207
        return EM::getInstance(static::class)->getNamer()
208
            ->getTableName(static::class, static::$tableNameTemplate, static::$namingSchemeTable);
209
    }
210 112
211
    /**
212 112
     * Check if the table has a auto increment column
213 112
     *
214
     * @return bool
215 112
     */
216 4
    public static function isAutoIncremented()
217
    {
218 108
        return count(static::getPrimaryKeyVars()) > 1 ? false : static::$autoIncrement;
219 108
    }
220
221 108
    /**
222 1
     * @param EM $entityManager
223
     * @return static
224
     */
225 107
    public function setEntityManager(EM $entityManager)
226
    {
227
        $this->entityManager = $entityManager;
228
        return $this;
229
    }
230
231
    /**
232
     * @param string $attribute
233
     * @return mixed|null
234
     * @see self::getAttribute
235 3
     * @codeCoverageIgnore Alias for getAttribute
236
     */
237 3
    public function __get($attribute)
238 3
    {
239
        return $this->getAttribute($attribute);
240 3
    }
241 1
242
    /**
243 2
     * Get the value from $attribute
244 2
     *
245
     * If there is a custom getter this method get called instead.
246 2
     *
247 1
     * @param string $attribute The variable to get
248
     * @return mixed|null
249
     * @link https://tflori.github.io/orm/entities.html Working with entities
250 1
     */
251
    public function getAttribute($attribute)
252
    {
253
        $em     = EM::getInstance(static::class);
254
        $getter = $em->getNamer()->getMethodName('get' . ucfirst($attribute), self::$namingSchemeMethods);
255
256
        if (method_exists($this, $getter) && is_callable([ $this, $getter ])) {
257
            return $this->$getter();
258
        } else {
259
            $col    = static::getColumnName($attribute);
260
            $result = isset($this->data[$col]) ? $this->data[$col] : null;
261
262
            if (!$result && isset(static::$relations[$attribute]) && isset($this->entityManager)) {
263
                return $this->getRelated($attribute);
264
            }
265
266
            return $result;
267
        }
268
    }
269
270 38
    /**
271
     * Check if a column is defined
272 38
     *
273
     * @param $attribute
274 38
     * @return bool
275 38
     */
276
    public function __isset($attribute)
277 38
    {
278 3
        $em     = EM::getInstance(static::class);
279 3
        $getter = $em->getNamer()->getMethodName('get' . ucfirst($attribute), self::$namingSchemeMethods);
280 3
281 3
        if (method_exists($this, $getter) && is_callable([ $this, $getter ])) {
282
            return $this->$getter() !== null;
283 35
        } else {
284 35
            $col = static::getColumnName($attribute);
285
            $isset = isset($this->data[$col]);
286 1
287
            if (!$isset && isset(static::$relations[$attribute])) {
288
                return !empty($this->getRelated($attribute));
289 31
            }
290 31
291 31
            return $isset;
292
        }
293
    }
294 34
295 31
    /**
296
     * @param string $attribute The variable to change
297 34
     * @param mixed $value The value to store
298
     * @see self::getAttribute
299
     * @codeCoverageIgnore Alias for getAttribute
300
     */
301
    public function __set($attribute, $value)
302
    {
303
        $this->setAttribute($attribute, $value);
304
    }
305
306
    /**
307
     * Set $attribute to $value
308
     *
309
     * Tries to call custom setter before it stores the data directly. If there is a setter the setter needs to store
310 8
     * data that should be updated in the database to $data. Do not store data in $originalData as it will not be
311
     * written and give wrong results for dirty checking.
312 8
     *
313
     * The onChange event is called after something got changed.
314 7
     *
315 2
     * The method throws an error when the validation fails (also when the column does not exist).
316 2
     *
317 7
     * @param string $attribute The variable to change
318
     * @param mixed $value The value to store
319
     * @return static
320
     * @link https://tflori.github.io/orm/entities.html Working with entities
321
     * @throws Error
322 7
     */
323 1
    public function setAttribute($attribute, $value)
324
    {
325 6
        $col = $this->getColumnName($attribute);
326
327
        $em     = EM::getInstance(static::class);
328
        $setter = $em->getNamer()->getMethodName('set' . ucfirst($attribute), self::$namingSchemeMethods);
329
330
        if (method_exists($this, $setter) && is_callable([ $this, $setter ])) {
331
            $oldValue   = $this->__get($attribute);
332
            $md5OldData = md5(serialize($this->data));
333 22
            $this->$setter($value);
334
            $changed = $md5OldData !== md5(serialize($this->data));
335 22
        } else {
336 3
            if (static::isValidatorEnabled()) {
337 3
                $error = static::validate($attribute, $value);
338 2
                if ($error instanceof Error) {
0 ignored issues
show
introduced by
$error is always a sub-type of ORM\Dbal\Error.
Loading history...
339
                    throw $error;
340 1
                }
341
            }
342 3
343
            $oldValue         = $this->__get($attribute);
344
            $changed          = (isset($this->data[$col]) ? $this->data[$col] : null) !== $value;
345 19
            $this->data[$col] = $value;
346 19
        }
347
348
        if ($changed) {
349
            $this->onChange($attribute, $oldValue, $this->__get($attribute));
350
        }
351
352
        return $this;
353
    }
354
355
    /**
356
     * Fill the entity with $data
357
     *
358
     * When $checkMissing is set to true it also proves that the absent columns are nullable.
359
     *
360
     * @param array $data
361 16
     * @param bool $ignoreUnknown
362
     * @param bool $checkMissing
363
     * @throws UnknownColumn
364
     */
365
    public function fill(array $data, $ignoreUnknown = false, $checkMissing = false)
366
    {
367
        foreach ($data as $attribute => $value) {
368
            try {
369
                $this->__set($attribute, $value);
370
            } catch (UnknownColumn $e) {
371
                if (!$ignoreUnknown) {
372 16
                    throw $e;
373 16
                }
374
            }
375
        }
376
377 16
        if ($checkMissing && is_array($errors = $this->isValid())) {
378 2
            throw $errors[0];
379 2
        }
380 4
    }
381 3
382 6
    /**
383
     * Resets the entity or $attribute to original data
384 10
     *
385 10
     * @param string $attribute Reset only this variable or all variables
386 8
     */
387 8
    public function reset($attribute = null)
388
    {
389 1
        if (!empty($attribute)) {
390 1
            $col = static::getColumnName($attribute);
391 1
            if (isset($this->originalData[$col])) {
392
                $this->data[$col] = $this->originalData[$col];
393 1
            } else {
394
                unset($this->data[$col]);
395
            }
396
            return;
397 14
        }
398 14
399
        $this->data = $this->originalData;
400 14
    }
401
402
    /**
403
     * Save the entity to EntityManager
404
     *
405
     * @return Entity
406
     * @throws IncompletePrimaryKey
407
     */
408
    public function save()
409
    {
410
        // @codeCoverageIgnoreStart
411
        if (func_num_args() === 1 && func_get_arg(0) instanceof EM) {
412
            trigger_error(
413
                'Passing EntityManager to save is deprecated. Use ->setEntityManager() to overwrite',
414
                E_USER_DEPRECATED
415
            );
416
        }
417
        // @codeCoverageIgnoreEnd
418
419
        $inserted = false;
420
        $updated  = false;
421 20
422
        try {
423 20
            // this may throw if the primary key is auto incremented but we using this to omit duplicated code
424 4
            if (!$this->entityManager->sync($this)) {
425 4
                $this->prePersist();
426 4
                $inserted = $this->entityManager->insert($this, false);
427
            } elseif ($this->isDirty()) {
428
                $this->preUpdate();
429 17
                $updated = $this->entityManager->update($this);
430 17
            }
431
        } catch (IncompletePrimaryKey $e) {
432 17
            if (static::isAutoIncremented()) {
433
                $this->prePersist();
434
                $inserted = $this->entityManager->insert($this);
435
            } elseif ($this instanceof GeneratesPrimaryKeys) {
436
                $this->generatePrimaryKey();
437
                $this->prePersist();
438
                $inserted = $this->entityManager->insert($this);
439
            } else {
440
                throw $e;
441
            }
442
        }
443
444
        $inserted && $this->postPersist();
445
        $updated && $this->postUpdate();
446
447
        return $this;
448
    }
449
450
    /**
451
     * Generates a primary key
452
     *
453
     * This method should only be executed from save method.
454
     * @codeCoverageIgnore no operations
455
     */
456
    protected function generatePrimaryKey()
457
    {
458
        // no operation by default
459
    }
460
461
    /**
462
     * Checks if entity or $attribute got changed
463
     *
464
     * @param string $attribute Check only this variable or all variables
465
     * @return bool
466
     */
467
    public function isDirty($attribute = null)
468
    {
469
        if (!empty($attribute)) {
470
            $col = static::getColumnName($attribute);
471
            return (isset($this->data[$col]) ? $this->data[$col] : null) !==
472
                   (isset($this->originalData[$col]) ? $this->originalData[$col] : null);
473
        }
474
475
        ksort($this->data);
476
        ksort($this->originalData);
477
478
        return serialize($this->data) !== serialize($this->originalData);
479
    }
480
481
    /**
482
     * Empty event handler
483
     *
484
     * Get called when the entity get initialized.
485
     *
486
     * @param bool $new Whether or not the entity is new or from database
487
     * @codeCoverageIgnore dummy event handler
488
     */
489
    public function onInit($new)
490
    {
491
    }
492
493
    /**
494
     * Empty event handler
495
     *
496
     * Get called when something is changed with magic setter.
497
     *
498
     * @param string $attribute The variable that got changed.merge(node.inheritedProperties)
499
     * @param mixed  $oldValue  The old value of the variable
500
     * @param mixed  $value     The new value of the variable
501
     * @codeCoverageIgnore dummy event handler
502
     */
503
    public function onChange($attribute, $oldValue, $value)
504
    {
505
    }
506
507
    /**
508
     * Empty event handler
509
     *
510
     * Get called before the entity get inserted in database.
511
     *
512 53
     * @codeCoverageIgnore dummy event handler
513
     */
514 53
    public function prePersist()
515 53
    {
516 53
    }
517 53
518 9
    /**
519
     * Empty event handler
520 49
     *
521
     * Get called before the entity get updated in database.
522 47
     *
523
     * @codeCoverageIgnore dummy event handler
524
     */
525
    public function preUpdate()
526
    {
527
    }
528
529
    /**
530
     * Empty event handler
531 31
     *
532
     * Get called after the entity got inserted in database.
533 31
     *
534
     * @codeCoverageIgnore dummy event handler
535
     */
536
    public function postPersist()
537
    {
538
    }
539
540
    /**
541
     * Empty event handler
542 38
     *
543
     * Get called after the entity got updated in database.
544 38
     *
545 38
     * @codeCoverageIgnore dummy event handler
546
     */
547
    public function postUpdate()
548
    {
549
    }
550
551
    /**
552
     * Get the primary key
553 2
     *
554
     * @return array
555 2
     * @throws IncompletePrimaryKey
556
     */
557
    public function getPrimaryKey()
558
    {
559
        $primaryKey = [];
560
        foreach (static::getPrimaryKeyVars() as $attribute) {
561
            $value = $this->$attribute;
562
            if ($value === null) {
563
                throw new IncompletePrimaryKey('Incomplete primary key - missing ' . $attribute);
564 3
            }
565
            $primaryKey[$attribute] = $value;
566 3
        }
567 3
        return $primaryKey;
568 3
    }
569 3
570
    /**
571
     * Get current data
572
     *
573
     * @return array
574
     * @internal
575
     */
576
    public function getData()
577
    {
578
        return $this->data;
579
    }
580
581
    /**
582
     * Set new original data
583
     *
584
     * @param array $data
585
     * @internal
586
     */
587
    public function setOriginalData(array $data)
588
    {
589
        $this->originalData = $data;
590
    }
591
592
    /**
593
     * Get an array of the entity
594
     *
595
     * @param array $attributes
596
     * @param bool $includeRelations
597
     * @return array
598
     */
599
    public function toArray(array $attributes = [], $includeRelations = true)
600
    {
601
        if (empty($attributes)) {
602
            $attributes = array_keys(static::$columnAliases);
603
            $attributes = array_merge($attributes, array_map([$this, 'getAttributeName'], array_keys($this->data)));
604
            $attributes = array_merge($attributes, static::$includedAttributes);
605
            $attributes = array_diff($attributes, static::$excludedAttributes);
606
        }
607
608
        $values = array_map(function ($attribute) {
609
            return $this->getAttribute($attribute);
610
        }, $attributes);
611
612
        $result = array_combine($attributes, $values);
613
614
        if ($includeRelations) {
615
            foreach ($this->relatedObjects as $relation => $relatedObject) {
616
                if (is_array($relatedObject)) {
617
                    $result[$relation] = array_map(function (Entity $relatedObject) {
618
                        return $relatedObject->toArray();
619
                    }, $relatedObject);
620
                } elseif ($relatedObject instanceof Entity) {
621
                    $result[$relation] = $relatedObject->toArray();
622
                }
623
            }
624
        }
625
626
        return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result could also return false which is incompatible with the documented return type array. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
627
    }
628
629
    /**
630
     * String representation of data
631
     *
632
     * @link http://php.net/manual/en/serializable.serialize.php
633
     * @return string
634
     */
635
    public function serialize()
636
    {
637
        return serialize([ $this->data, $this->relatedObjects ]);
638
    }
639
640
    /**
641
     * Constructs the object
642
     *
643
     * @link http://php.net/manual/en/serializable.unserialize.php
644
     * @param string $serialized The string representation of data
645
     */
646
    public function unserialize($serialized)
647
    {
648
        list($this->data, $this->relatedObjects) = unserialize($serialized);
649
        $this->entityManager = EM::getInstance(static::class);
650
        $this->onInit(false);
651
    }
652
653
    // DEPRECATED stuff
654
655
    /**
656
     * @return string
657
     * @deprecated         use getOption from EntityManager
658
     * @codeCoverageIgnore deprecated
659
     */
660
    public static function getTableNameTemplate()
661
    {
662
        return static::$tableNameTemplate;
663
    }
664
665
    /**
666
     * @param string $tableNameTemplate
667
     * @deprecated         use setOption from EntityManager
668
     * @codeCoverageIgnore deprecated
669
     */
670
    public static function setTableNameTemplate($tableNameTemplate)
671
    {
672
        static::$tableNameTemplate = $tableNameTemplate;
673
    }
674
675
    /**
676
     * @return string
677
     * @deprecated         use getOption from EntityManager
678
     * @codeCoverageIgnore deprecated
679
     */
680
    public static function getNamingSchemeTable()
681
    {
682
        return static::$namingSchemeTable;
683
    }
684
685
    /**
686
     * @param string $namingSchemeTable
687
     * @deprecated         use setOption from EntityManager
688
     * @codeCoverageIgnore deprecated
689
     */
690
    public static function setNamingSchemeTable($namingSchemeTable)
691
    {
692
        static::$namingSchemeTable = $namingSchemeTable;
693
    }
694
695
    /**
696
     * @return string
697
     * @deprecated         use getOption from EntityManager
698
     * @codeCoverageIgnore deprecated
699
     */
700
    public static function getNamingSchemeColumn()
701
    {
702
        return static::$namingSchemeColumn;
703
    }
704
705
    /**
706
     * @param string $namingSchemeColumn
707
     * @deprecated         use setOption from EntityManager
708
     * @codeCoverageIgnore deprecated
709
     */
710
    public static function setNamingSchemeColumn($namingSchemeColumn)
711
    {
712
        static::$namingSchemeColumn = $namingSchemeColumn;
713
    }
714
715
    /**
716
     * @return string
717
     * @deprecated         use getOption from EntityManager
718
     * @codeCoverageIgnore deprecated
719
     */
720
    public static function getNamingSchemeMethods()
721
    {
722
        return static::$namingSchemeMethods;
723
    }
724
725
    /**
726
     * @param string $namingSchemeMethods
727
     * @deprecated         use setOption from EntityManager
728
     * @codeCoverageIgnore deprecated
729
     */
730
    public static function setNamingSchemeMethods($namingSchemeMethods)
731
    {
732
        static::$namingSchemeMethods = $namingSchemeMethods;
733
    }
734
}
735