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

Entity::fill()   A

Complexity

Conditions 6
Paths 7

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 8
c 1
b 0
f 1
dl 0
loc 14
ccs 4
cts 4
cp 1
rs 9.2222
cc 6
nc 7
nop 3
crap 6
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
     * @throws Exception
599
     */
600
    public function toArray(array $attributes = [], $includeRelations = true)
601
    {
602
        if (empty($attributes)) {
603
            $attributes = array_keys(static::$columnAliases);
604
            $attributes = array_merge($attributes, array_map([$this, 'getAttributeName'], array_keys($this->data)));
605
            $attributes = array_merge($attributes, static::$includedAttributes);
606
            $attributes = array_diff($attributes, static::$excludedAttributes);
607
        }
608
609
        $values = array_map(function ($attribute) {
610
            return $this->getAttribute($attribute);
611
        }, $attributes);
612
613
        $result = array_combine($attributes, $values);
614
615
        if ($includeRelations) {
616
            foreach ($this->relatedObjects as $relation => $relatedObject) {
617
                if (is_array($relatedObject)) {
618
                    $result[$relation] = array_map(function (Entity $relatedObject) {
619
                        return $relatedObject->toArray();
620
                    }, $relatedObject);
621
                } elseif ($relatedObject instanceof Entity) {
622
                    $result[$relation] = $relatedObject->toArray();
623
                }
624
            }
625
        }
626
627
        if (!is_array($result)) {
628
            throw new Exception('Unable to build array from ' . static::class);
629
        }
630
631
        return $result;
632
    }
633
634
    /**
635
     * String representation of data
636
     *
637
     * @link http://php.net/manual/en/serializable.serialize.php
638
     * @return string
639
     */
640
    public function serialize()
641
    {
642
        return serialize([ $this->data, $this->relatedObjects ]);
643
    }
644
645
    /**
646
     * Constructs the object
647
     *
648
     * @link http://php.net/manual/en/serializable.unserialize.php
649
     * @param string $serialized The string representation of data
650
     */
651
    public function unserialize($serialized)
652
    {
653
        list($this->data, $this->relatedObjects) = unserialize($serialized);
654
        $this->entityManager = EM::getInstance(static::class);
655
        $this->onInit(false);
656
    }
657
658
    // DEPRECATED stuff
659
660
    /**
661
     * @return string
662
     * @deprecated         use getOption from EntityManager
663
     * @codeCoverageIgnore deprecated
664
     */
665
    public static function getTableNameTemplate()
666
    {
667
        return static::$tableNameTemplate;
668
    }
669
670
    /**
671
     * @param string $tableNameTemplate
672
     * @deprecated         use setOption from EntityManager
673
     * @codeCoverageIgnore deprecated
674
     */
675
    public static function setTableNameTemplate($tableNameTemplate)
676
    {
677
        static::$tableNameTemplate = $tableNameTemplate;
678
    }
679
680
    /**
681
     * @return string
682
     * @deprecated         use getOption from EntityManager
683
     * @codeCoverageIgnore deprecated
684
     */
685
    public static function getNamingSchemeTable()
686
    {
687
        return static::$namingSchemeTable;
688
    }
689
690
    /**
691
     * @param string $namingSchemeTable
692
     * @deprecated         use setOption from EntityManager
693
     * @codeCoverageIgnore deprecated
694
     */
695
    public static function setNamingSchemeTable($namingSchemeTable)
696
    {
697
        static::$namingSchemeTable = $namingSchemeTable;
698
    }
699
700
    /**
701
     * @return string
702
     * @deprecated         use getOption from EntityManager
703
     * @codeCoverageIgnore deprecated
704
     */
705
    public static function getNamingSchemeColumn()
706
    {
707
        return static::$namingSchemeColumn;
708
    }
709
710
    /**
711
     * @param string $namingSchemeColumn
712
     * @deprecated         use setOption from EntityManager
713
     * @codeCoverageIgnore deprecated
714
     */
715
    public static function setNamingSchemeColumn($namingSchemeColumn)
716
    {
717
        static::$namingSchemeColumn = $namingSchemeColumn;
718
    }
719
720
    /**
721
     * @return string
722
     * @deprecated         use getOption from EntityManager
723
     * @codeCoverageIgnore deprecated
724
     */
725
    public static function getNamingSchemeMethods()
726
    {
727
        return static::$namingSchemeMethods;
728
    }
729
730
    /**
731
     * @param string $namingSchemeMethods
732
     * @deprecated         use setOption from EntityManager
733
     * @codeCoverageIgnore deprecated
734
     */
735
    public static function setNamingSchemeMethods($namingSchemeMethods)
736
    {
737
        static::$namingSchemeMethods = $namingSchemeMethods;
738
    }
739
}
740