Completed
Push — master ( db5f77...4370c7 )
by Thomas
30s queued 10s
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)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;
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