Passed
Pull Request — master (#57)
by Thomas
01:38
created

Entity::__set()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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