Passed
Branch feature-validator (b67d02)
by Thomas
02:48
created

Entity::getNamingSchemeColumn()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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