Completed
Push — master ( c62ba6...02d004 )
by Thomas
02:20
created

Entity::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 9
ccs 7
cts 7
cp 1
rs 9.6666
cc 2
eloc 6
nc 2
nop 3
crap 2
1
<?php
2
3
namespace ORM;
4
5
use ORM\Exceptions\IncompletePrimaryKey;
6
use ORM\Exceptions\InvalidConfiguration;
7
use ORM\Exceptions\InvalidRelation;
8
use ORM\Exceptions\InvalidName;
9
use ORM\Exceptions\NoEntityManager;
10
use ORM\Exceptions\UndefinedRelation;
11
use ORM\QueryBuilder\QueryBuilder;
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
    const OPT_RELATION_CLASS       = 'class';
29
    const OPT_RELATION_CARDINALITY = 'cardinality';
30
    const OPT_RELATION_REFERENCE   = 'reference';
31
    const OPT_RELATION_OPPONENT    = 'opponent';
32
    const OPT_RELATION_TABLE       = 'table';
33
34
    const CARDINALITY_ONE          = 'one';
35
    const CARDINALITY_MANY         = 'many';
36
37
    /** The template to use to calculate the table name.
38
     * @var string */
39
    protected static $tableNameTemplate = '%short%';
40
41
    /** The naming scheme to use for table names.
42
     * @var string */
43
    protected static $namingSchemeTable = 'snake_lower';
44
45
    /** The naming scheme to use for column names.
46
     * @var string */
47
    protected static $namingSchemeColumn = 'snake_lower';
48
49
    /** The naming scheme to use for method names.
50
     * @var string */
51
    protected static $namingSchemeMethods = 'camelCase';
52
53
    /** Whether or not the naming got used
54
     * @var bool */
55
    protected static $namingUsed = false;
56
57
    /** Fixed table name (ignore other settings)
58
     * @var string */
59
    protected static $tableName;
60
61
    /** The variable(s) used for primary key.
62
     * @var string[]|string */
63
    protected static $primaryKey = ['id'];
64
65
    /** Fixed column names (ignore other settings)
66
     * @var string[] */
67
    protected static $columnAliases = [];
68
69
    /** A prefix for column names.
70
     * @var string */
71
    protected static $columnPrefix;
72
73
    /** Whether or not the primary key is auto incremented.
74
     * @var bool */
75
    protected static $autoIncrement = true;
76
77
    /** Relation definitions
78
     * @var array */
79
    protected static $relations = [];
80
81
    /** The current data of a row.
82
     * @var mixed[] */
83
    protected $data = [];
84
85
    /** The original data of the row.
86
     * @var mixed[] */
87
    protected $originalData = [];
88
89
    /** The entity manager from which this entity got created
90
     * @var EntityManager*/
91
    protected $entityManager;
92
93
    /** Related objects for getRelated
94
     * @var array */
95
    protected $relatedObjects = [];
96
97
    /** Calculated table names.
98
     * @internal
99
     * @var string[] */
100
    protected static $calculatedTableNames = [];
101
102
    /** Calculated column names.
103
     * @internal
104
     * @var string[][] */
105
    protected static $calculatedColumnNames = [];
106
107
    /** The reflections of the classes.
108
     * @internal
109
     * @var \ReflectionClass[] */
110
    protected static $reflections = [];
111
112
    /**
113
     * Get the table name
114
     *
115
     * The table name is constructed by $tableNameTemplate and $namingSchemeTable. It can be overwritten by
116
     * $tableName.
117
     *
118
     * @return string
119
     * @throws InvalidName|InvalidConfiguration
120
     */
121 120
    public static function getTableName()
122
    {
123 120
        if (static::$tableName) {
124 11
            return static::$tableName;
125
        }
126
127 109
        if (!isset(self::$calculatedTableNames[static::class])) {
128 109
            static::$namingUsed = true;
129 109
            $reflection = self::getReflection();
130
131
            $tableName = preg_replace_callback('/%([a-z]+)(\[(-?\d+)(\*)?\])?%/', function ($match) use ($reflection) {
132 109
                switch ($match[1]) {
133 109
                    case 'short':
134 95
                        $words = [$reflection->getShortName()];
135 95
                        break;
136
137 14
                    case 'namespace':
138 4
                        $words = explode('\\', $reflection->getNamespaceName());
139 4
                        break;
140
141 10
                    case 'name':
142 9
                        $words = preg_split('/[\\\\_]+/', $reflection->getName());
143 9
                        break;
144
145
                    default:
146 1
                        throw new InvalidConfiguration(
147 1
                            'Template invalid: Placeholder %' . $match[1] . '% is not allowed'
148
                        );
149
                }
150
151 108
                if (!isset($match[2])) {
152 97
                    return implode('_', $words);
153
                }
154 11
                $from = $match[3][0] === '-' ? count($words) - substr($match[3], 1) : $match[3];
155 11
                if (isset($words[$from])) {
156 9
                    return !isset($match[4]) ?
157 6
                        $words[$from] :
158 9
                        implode('_', array_slice($words, $from));
159
                }
160 2
                return '';
161 109
            }, static::getTableNameTemplate());
162
163 108
            if (empty($tableName)) {
164 2
                throw new InvalidName('Table name can not be empty');
165
            }
166 106
            self::$calculatedTableNames[static::class] =
167 106
                self::forceNamingScheme($tableName, static::getNamingSchemeTable());
168
        }
169
170 105
        return self::$calculatedTableNames[static::class];
171
    }
172
173
    /**
174
     * Get the column name of $name
175
     *
176
     * The column names can not be specified by template. Instead they are constructed by $columnPrefix and enforced
177
     * to $namingSchemeColumn.
178
     *
179
     * **ATTENTION**: If your overwrite this method remember that getColumnName(getColumnName($name)) have to exactly
180
     * the same as getColumnName($name).
181
     *
182
     * @param string $var
183
     * @return string
184
     * @throws InvalidConfiguration
185
     */
186 122
    public static function getColumnName($var)
187
    {
188 122
        if (isset(static::$columnAliases[$var])) {
189 7
            return static::$columnAliases[$var];
190
        }
191
192 119
        if (!isset(self::$calculatedColumnNames[static::class][$var])) {
193 119
            static::$namingUsed = true;
194 119
            $colName = $var;
195
196 119
            if (static::$columnPrefix &&
197 23
                strpos(
198
                    $colName,
199 23
                    self::forceNamingScheme(static::$columnPrefix, static::getNamingSchemeColumn())
200 119
                ) !== 0) {
201 22
                $colName = static::$columnPrefix . $colName;
202
            }
203
204 119
            self::$calculatedColumnNames[static::class][$var] =
205 119
                self::forceNamingScheme($colName, static::getNamingSchemeColumn());
206
        }
207
208 119
        return self::$calculatedColumnNames[static::class][$var];
209
    }
210
211
    /**
212
     * Get the definition for $relation
213
     *
214
     * It will normalize the definition before.
215
     *
216
     * The resulting array will have at least `class` and `cardinality`. It may also have the following keys:
217
     * `class`, `cardinality`, `reference`, `opponent` and `table`
218
     *
219
     * @param string $relation
220
     * @return array
221
     * @throws InvalidConfiguration
222
     * @throws UndefinedRelation
223
     */
224 56
    public static function getRelationDefinition($relation)
225
    {
226 56
        if (!isset(static::$relations[$relation])) {
227 2
            throw new UndefinedRelation('Relation ' . $relation . ' is not defined');
228
        }
229
230 55
        $relationDefinition = &static::$relations[$relation];
231
232 55
        if (isset($relationDefinition[0])) {
233
            // convert the short form
234 8
            $length = count($relationDefinition);
235
236 8
            if ($length === 2 && gettype($relationDefinition[1]) === 'array') {
237
                // owner of one-to-many or one-to-one
238 2
                static::$relations[$relation] = [
239 2
                    self::OPT_RELATION_CARDINALITY => self::CARDINALITY_ONE,
240 2
                    self::OPT_RELATION_CLASS       => $relationDefinition[0],
241 2
                    self::OPT_RELATION_REFERENCE   => $relationDefinition[1],
242
                ];
243 6
            } elseif ($length === 3 && $relationDefinition[0] === self::CARDINALITY_ONE) {
244
                // non-owner of one-to-one
245 2
                static::$relations[$relation] = [
246 2
                    self::OPT_RELATION_CARDINALITY => self::CARDINALITY_ONE,
247 2
                    self::OPT_RELATION_CLASS       => $relationDefinition[1],
248 2
                    self::OPT_RELATION_OPPONENT    => $relationDefinition[2],
249
                ];
250 4
            } elseif ($length === 2) {
251
                // non-owner of one-to-many
252 1
                static::$relations[$relation] = [
253 1
                    self::OPT_RELATION_CARDINALITY => self::CARDINALITY_MANY,
254 1
                    self::OPT_RELATION_CLASS       => $relationDefinition[0],
255 1
                    self::OPT_RELATION_OPPONENT    => $relationDefinition[1],
256
                ];
257 3
            } elseif ($length === 4 && gettype($relationDefinition[1]) === 'array') {
258 2
                static::$relations[$relation] = [
259 2
                    self::OPT_RELATION_CARDINALITY => self::CARDINALITY_MANY,
260 2
                    self::OPT_RELATION_CLASS       => $relationDefinition[0],
261 2
                    self::OPT_RELATION_REFERENCE   => $relationDefinition[1],
262 2
                    self::OPT_RELATION_OPPONENT    => $relationDefinition[2],
263 2
                    self::OPT_RELATION_TABLE       => $relationDefinition[3],
264
                ];
265
            } else {
266 8
                throw new InvalidConfiguration('Invalid short form for relation ' . $relation);
267
            }
268 48
        } elseif (empty($relationDefinition[self::OPT_RELATION_CARDINALITY])) {
269
            // default cardinality
270 2
            $relationDefinition[self::OPT_RELATION_CARDINALITY] =
271 2
                !empty($relationDefinition[self::OPT_RELATION_OPPONENT]) ?
272 2
                    self::CARDINALITY_MANY : self::CARDINALITY_ONE;
273 46
        } elseif (isset($relationDefinition[self::OPT_RELATION_REFERENCE]) &&
274 46
                  !isset($relationDefinition[self::OPT_RELATION_TABLE]) &&
275 46
                  $relationDefinition[self::OPT_RELATION_CARDINALITY] === self::CARDINALITY_MANY) {
276
            // overwrite wrong cardinality for owner
277 1
            $relationDefinition[self::OPT_RELATION_CARDINALITY] = self::CARDINALITY_ONE;
278
        }
279
280 54
        return $relationDefinition;
281
    }
282
283
    /**
284
     * @return string
285
     */
286 110
    public static function getTableNameTemplate()
287
    {
288 110
        return static::$tableNameTemplate;
289
    }
290
291
    /**
292
     * @param string $tableNameTemplate
293
     * @throws InvalidConfiguration
294
     */
295 52
    public static function setTableNameTemplate($tableNameTemplate)
296
    {
297 52
        if (static::$namingUsed) {
298 1
            throw new InvalidConfiguration('Template can not be changed afterwards');
299
        }
300
301 51
        static::$tableNameTemplate = $tableNameTemplate;
302 51
    }
303
304
    /**
305
     * @return string
306
     */
307 107
    public static function getNamingSchemeTable()
308
    {
309 107
        return static::$namingSchemeTable;
310
    }
311
312
    /**
313
     * @param string $namingSchemeTable
314
     * @throws InvalidConfiguration
315
     */
316 52
    public static function setNamingSchemeTable($namingSchemeTable)
317
    {
318 52
        if (static::$namingUsed) {
319 1
            throw new InvalidConfiguration('Naming scheme can not be changed afterwards');
320
        }
321
322 51
        static::$namingSchemeTable = $namingSchemeTable;
323 51
    }
324
325
    /**
326
     * @return string
327
     */
328 120
    public static function getNamingSchemeColumn()
329
    {
330 120
        return static::$namingSchemeColumn;
331
    }
332
333
    /**
334
     * @param string $namingSchemeColumn
335
     * @throws InvalidConfiguration
336
     */
337 27
    public static function setNamingSchemeColumn($namingSchemeColumn)
338
    {
339 27
        if (static::$namingUsed) {
340 1
            throw new InvalidConfiguration('Naming scheme can not be changed afterwards');
341
        }
342
343 26
        static::$namingSchemeColumn = $namingSchemeColumn;
344 26
    }
345
346
    /**
347
     * @return string
348
     */
349 84
    public static function getNamingSchemeMethods()
350
    {
351 84
        return static::$namingSchemeMethods;
352
    }
353
354
    /**
355
     * @param string $namingSchemeMethods
356
     * @throws InvalidConfiguration
357
     */
358 3
    public static function setNamingSchemeMethods($namingSchemeMethods)
359
    {
360 3
        if (static::$namingUsed) {
361 1
            throw new InvalidConfiguration('Naming scheme can not be changed afterwards');
362
        }
363
364 3
        static::$namingSchemeMethods = $namingSchemeMethods;
365 3
    }
366
367
    /**
368
     * Get the primary key vars
369
     *
370
     * The primary key can consist of multiple columns. You should configure the vars that are translated to these
371
     * columns.
372
     *
373
     * @return array
374
     */
375 56
    public static function getPrimaryKeyVars()
376
    {
377 56
        return !is_array(static::$primaryKey) ? [static::$primaryKey] : static::$primaryKey;
378
    }
379
380
    /**
381
     * Check if the table has a auto increment column.
382
     *
383
     * @return bool
384
     */
385 14
    public static function isAutoIncremented()
386
    {
387 14
        return count(static::getPrimaryKeyVars()) > 1 ? false : static::$autoIncrement;
388
    }
389
390
    /**
391
     * Enforce $namingScheme to $name
392
     *
393
     * Supported naming schemes: snake_case, snake_lower, SNAKE_UPPER, Snake_Ucfirst, camelCase, StudlyCaps, lower
394
     * and UPPER.
395
     *
396
     * @param string $name         The name of the var / column
397
     * @param string $namingScheme The naming scheme to use
398
     * @return string
399
     * @throws InvalidConfiguration
400
     */
401 193
    protected static function forceNamingScheme($name, $namingScheme)
402
    {
403 193
        $words = explode('_', preg_replace(
404 193
            '/([a-z0-9])([A-Z])/',
405 193
            '$1_$2',
406 193
            preg_replace_callback('/([a-z0-9])?([A-Z]+)([A-Z][a-z])/', function ($d) {
407 17
                return ($d[1] ? $d[1] . '_' : '') . $d[2] . '_' . $d[3];
408 193
            }, $name)
409
        ));
410
411
        switch ($namingScheme) {
412 193
            case 'snake_case':
413 27
                $newName = implode('_', $words);
414 27
                break;
415
416 166
            case 'snake_lower':
417 133
                $newName = implode('_', array_map('strtolower', $words));
418 133
                break;
419
420 112
            case 'SNAKE_UPPER':
421 4
                $newName = implode('_', array_map('strtoupper', $words));
422 4
                break;
423
424 108
            case 'Snake_Ucfirst':
425 4
                $newName = implode('_', array_map('ucfirst', $words));
426 4
                break;
427
428 104
            case 'camelCase':
429 85
                $newName = lcfirst(implode('', array_map('ucfirst', array_map('strtolower', $words))));
430 85
                break;
431
432 19
            case 'StudlyCaps':
433 10
                $newName = implode('', array_map('ucfirst', array_map('strtolower', $words)));
434 10
                break;
435
436 9
            case 'lower':
437 4
                $newName = implode('', array_map('strtolower', $words));
438 4
                break;
439
440 5
            case 'UPPER':
441 4
                $newName = implode('', array_map('strtoupper', $words));
442 4
                break;
443
444
            default:
445 1
                throw new InvalidConfiguration('Naming scheme ' . $namingScheme . ' unknown');
446
        }
447
448 192
        return $newName;
449
    }
450
451
    /**
452
     * Get reflection of the entity
453
     *
454
     * @return \ReflectionClass
455
     */
456 109
    protected static function getReflection()
457
    {
458 109
        if (!isset(self::$reflections[static::class])) {
459 109
            self::$reflections[static::class] = new \ReflectionClass(static::class);
460
        }
461 109
        return self::$reflections[static::class];
462
    }
463
464
    /**
465
     * Constructor
466
     *
467
     * It calls ::onInit() after initializing $data and $originalData.
468
     *
469
     * @param mixed[]       $data          The current data
470
     * @param EntityManager $entityManager The EntityManager that created this entity
471
     * @param bool          $fromDatabase  Whether or not the data comes from database
472
     */
473 99
    final public function __construct(array $data = [], EntityManager $entityManager = null, $fromDatabase = false)
474
    {
475 99
        if ($fromDatabase) {
476 14
            $this->originalData = $data;
477
        }
478 99
        $this->data = array_merge($this->data, $data);
479 99
        $this->entityManager = $entityManager;
480 99
        $this->onInit(!$fromDatabase);
481 99
    }
482
483
    /**
484
     * @param EntityManager $entityManager
485
     * @return self
486
     */
487 1
    public function setEntityManager(EntityManager $entityManager)
488
    {
489 1
        $this->entityManager = $entityManager;
490 1
        return $this;
491
    }
492
493
    /**
494
     * Set $var to $value
495
     *
496
     * Tries to call custom setter before it stores the data directly. If there is a setter the setter needs to store
497
     * data that should be updated in the database to $data. Do not store data in $originalData as it will not be
498
     * written and give wrong results for dirty checking.
499
     *
500
     * The onChange event is called after something got changed.
501
     *
502
     * @param string $var   The variable to change
503
     * @param mixed  $value The value to store
504
     * @throws IncompletePrimaryKey
505
     * @throws InvalidConfiguration
506
     * @link https://tflori.github.io/orm/entities.html Working with entities
507
     */
508 18
    public function __set($var, $value)
509
    {
510 18
        $col = $this->getColumnName($var);
511
512 18
        static::$namingUsed = true;
513 18
        $setter = self::forceNamingScheme('set' . ucfirst($var), static::getNamingSchemeMethods());
514 18
        if (method_exists($this, $setter) && is_callable([$this, $setter])) {
515 4
            $oldValue = $this->__get($var);
516 4
            $md5OldData = md5(serialize($this->data));
517 4
            $this->$setter($value);
518 4
            $changed = $md5OldData !== md5(serialize($this->data));
519
        } else {
520 14
            $oldValue = $this->__get($var);
521 14
            $changed = @$this->data[$col] !== $value;
522 14
            $this->data[$col] = $value;
523
        }
524
525 18
        if ($changed) {
526 14
            $this->onChange($var, $oldValue, $this->__get($var));
527
        }
528 18
    }
529
530
    /**
531
     * Get the value from $var
532
     *
533
     * If there is a custom getter this method get called instead.
534
     *
535
     * @param string $var The variable to get
536
     * @return mixed|null
537
     * @throws IncompletePrimaryKey
538
     * @throws InvalidConfiguration
539
     * @link https://tflori.github.io/orm/entities.html Working with entities
540
     */
541 83
    public function __get($var)
542
    {
543 83
        $getter = self::forceNamingScheme('get' . ucfirst($var), static::getNamingSchemeMethods());
544 83
        if (method_exists($this, $getter) && is_callable([$this, $getter])) {
545 5
            return $this->$getter();
546
        } else {
547 78
            $col = static::getColumnName($var);
548 78
            $result = isset($this->data[$col]) ? $this->data[$col] : null;
549
550 78
            if (!$result && isset(static::$relations[$var]) && isset($this->entityManager)) {
551 1
                return $this->getRelated($var);
552
            }
553
554 77
            return $result;
555
        }
556
    }
557
558
    /**
559
     * Get related objects
560
     *
561
     * The difference between getRelated and fetch is that getRelated stores the fetched entities. To refresh set
562
     * $refresh to true.
563
     *
564
     * @param string $relation
565
     * @param bool   $refresh
566
     * @return mixed
567
     * @throws Exceptions\NoConnection
568
     * @throws Exceptions\NoEntity
569
     * @throws IncompletePrimaryKey
570
     * @throws InvalidConfiguration
571
     * @throws NoEntityManager
572
     * @throws UndefinedRelation
573
     */
574 10
    public function getRelated($relation, $refresh = false)
575
    {
576 10
        if ($refresh || !isset($this->relatedObjects[$relation])) {
577 9
            $this->relatedObjects[$relation] = $this->fetch($relation, null, true);
578
        }
579
580 10
        return $this->relatedObjects[$relation];
581
    }
582
583
    /**
584
     * Set $relation to $entity
585
     *
586
     * This method is only for the owner of a relation.
587
     *
588
     * @param string $relation
589
     * @param Entity $entity
590
     * @throws IncompletePrimaryKey
591
     * @throws InvalidRelation
592
     */
593 6
    public function setRelation($relation, Entity $entity = null)
594
    {
595 6
        $relDef = static::getRelationDefinition($relation);
596
597 6
        if ($relDef[self::OPT_RELATION_CARDINALITY] !== self::CARDINALITY_ONE ||
598 6
            !isset($relDef[self::OPT_RELATION_REFERENCE])
599
        ) {
600 1
            throw new InvalidRelation('This is not the owner of the relation');
601
        }
602
603 5
        if ($entity !== null && !$entity instanceof $relDef[self::OPT_RELATION_CLASS]) {
604 1
            throw new InvalidRelation('Invalid entity for relation ' . $relation);
605
        }
606
607 4
        $reference = $relDef[self::OPT_RELATION_REFERENCE];
608 4
        foreach ($reference as $fkVar => $var) {
609 4
            if ($entity === null) {
610 1
                $this->__set($fkVar, null);
611 1
                continue;
612
            }
613
614 4
            $value = $entity->__get($var);
615
616 4
            if ($value === null) {
617 1
                throw new IncompletePrimaryKey('Key incomplete to save foreign key');
618
            }
619
620 3
            $this->__set($fkVar, $value);
621
        }
622
623 3
        $this->relatedObjects[$relation] = $entity;
624 3
    }
625
626
    /**
627
     * Add relations for $relation to $entities
628
     *
629
     * This method is only for many-to-many relations.
630
     *
631
     * This method does not take care about already existing relations and will fail hard.
632
     *
633
     * @param string $relation
634
     * @param Entity[] $entities
635
     * @throws IncompletePrimaryKey
636
     * @throws InvalidRelation
637
     */
638 7
    public function addRelations($relation, $entities)
639
    {
640 7
        $myRelDef = static::getRelationDefinition($relation);
641
642 7
        if ($myRelDef[self::OPT_RELATION_CARDINALITY] !== self::CARDINALITY_MANY ||
643 7
            !isset($myRelDef[self::OPT_RELATION_TABLE])
644
        ) {
645 1
            throw new InvalidRelation('This is not a many-to-many relation');
646
        }
647
648 6
        if (empty($entities)) {
649 1
            return;
650
        }
651
652 5
        $class = $myRelDef[self::OPT_RELATION_CLASS];
653 5
        $oppRelDef = $class::getRelationDefinition($myRelDef[self::OPT_RELATION_OPPONENT]);
654 5
        $table = $this->entityManager->escapeIdentifier($myRelDef[self::OPT_RELATION_TABLE]);
655
656 5
        $cols = [];
657 5
        $baseAssociation = [];
658 5
        foreach ($myRelDef[self::OPT_RELATION_REFERENCE] as $myVar => $fkCol) {
659 5
            $cols[]            = $this->entityManager->escapeIdentifier($fkCol);
660 5
            $value             = $this->__get($myVar);
661
662 5
            if ($value === null) {
663 1
                throw new IncompletePrimaryKey('Key incomplete to save foreign key');
664
            }
665
666 4
            $baseAssociation[] = $this->entityManager->escapeValue($value);
667
        }
668
669 4
        $associations = [];
670 4
        foreach ($entities as $entity) {
671 4
            if (!$entity instanceof $myRelDef[self::OPT_RELATION_CLASS]) {
672 1
                throw new InvalidRelation('Invalid entity for relation ' . $relation);
673
            }
674
675 4
            $association = $baseAssociation;
676 4
            foreach ($oppRelDef[self::OPT_RELATION_REFERENCE] as $hisVar => $fkCol) {
677 4
                if (empty($associations)) {
678 4
                    $cols[] = $this->entityManager->escapeIdentifier($fkCol);
679
                }
680 4
                $value        = $entity->__get($hisVar);
681
682 4
                if ($value === null) {
683 1
                    throw new IncompletePrimaryKey('Key incomplete to save foreign key');
684
                }
685
686 4
                $association[] = $this->entityManager->escapeValue($value);
687
            }
688 4
            $associations[] = implode(',', $association);
689
        }
690
691 2
        $statement = 'INSERT INTO ' . $table . ' (' . implode(',', $cols) . ') ' .
692 2
                     'VALUES (' . implode('),(', $associations) . ')';
693 2
        $this->entityManager->getConnection()->query($statement);
694 2
    }
695
696
    /**
697
     * Delete relations for $relation to $entities
698
     *
699
     * This method is only for many-to-many relations.
700
     *
701
     * @param string $relation
702
     * @param Entity[] $entities
703
     * @throws IncompletePrimaryKey
704
     * @throws InvalidRelation
705
     */
706 7
    public function deleteRelations($relation, $entities)
707
    {
708 7
        $myRelDef = static::getRelationDefinition($relation);
709
710 7
        if ($myRelDef[self::OPT_RELATION_CARDINALITY] !== self::CARDINALITY_MANY ||
711 7
            !isset($myRelDef[self::OPT_RELATION_TABLE])
712
        ) {
713 1
            throw new InvalidRelation('This is not a many-to-many relation');
714
        }
715
716 6
        if (empty($entities)) {
717 1
            return;
718
        }
719
720 5
        $class = $myRelDef[self::OPT_RELATION_CLASS];
721 5
        $oppRelDef = $class::getRelationDefinition($myRelDef[self::OPT_RELATION_OPPONENT]);
722 5
        $table = $this->entityManager->escapeIdentifier($myRelDef[self::OPT_RELATION_TABLE]);
723 5
        $where = [];
724
725 5
        foreach ($myRelDef[self::OPT_RELATION_REFERENCE] as $myVar => $fkCol) {
726 5
            $value             = $this->__get($myVar);
727
728 5
            if ($value === null) {
729 1
                throw new IncompletePrimaryKey('Key incomplete to save foreign key');
730
            }
731
732 4
            $where[] = $this->entityManager->escapeIdentifier($fkCol) . ' = ' .
733 4
                       $this->entityManager->escapeValue($value);
734
        }
735
736 4
        foreach ($entities as $entity) {
737 4
            if (!$entity instanceof $myRelDef[self::OPT_RELATION_CLASS]) {
738 1
                throw new InvalidRelation('Invalid entity for relation ' . $relation);
739
            }
740
741 4
            $condition = [];
742 4
            foreach ($oppRelDef[self::OPT_RELATION_REFERENCE] as $hisVar => $fkCol) {
743 4
                $value        = $entity->__get($hisVar);
744
745 4
                if ($value === null) {
746 1
                    throw new IncompletePrimaryKey('Key incomplete to save foreign key');
747
                }
748
749 4
                $condition[] = $this->entityManager->escapeIdentifier($fkCol) .' = ' .
750 4
                        $this->entityManager->escapeValue($value);
751
            }
752 4
            $where[] = implode(' AND ', $condition);
753
        }
754
755 2
        $statement = 'DELETE FROM ' . $table . ' WHERE ' . array_shift($where) . ' ' .
756 2
                     'AND (' . implode(' OR ', $where) . ')';
757 2
        $this->entityManager->getConnection()->query($statement);
758 2
    }
759
760
    /**
761
     * Checks if entity or $var got changed
762
     *
763
     * @param string $var Check only this variable or all variables
764
     * @return bool
765
     * @throws InvalidConfiguration
766
     */
767 18
    public function isDirty($var = null)
768
    {
769 18
        if (!empty($var)) {
770 4
            $col = static::getColumnName($var);
771 4
            return @$this->data[$col] !== @$this->originalData[$col];
772
        }
773
774 15
        ksort($this->data);
775 15
        ksort($this->originalData);
776
777 15
        return serialize($this->data) !== serialize($this->originalData);
778
    }
779
780
    /**
781
     * Resets the entity or $var to original data
782
     *
783
     * @param string $var Reset only this variable or all variables
784
     * @throws InvalidConfiguration
785
     */
786 8
    public function reset($var = null)
787
    {
788 8
        if (!empty($var)) {
789 3
            $col = static::getColumnName($var);
790 3
            if (isset($this->originalData[$col])) {
791 2
                $this->data[$col] = $this->originalData[$col];
792
            } else {
793 1
                unset($this->data[$col]);
794
            }
795 3
            return;
796
        }
797
798 5
        $this->data = $this->originalData;
799 5
    }
800
801
    /**
802
     * Save the entity to $entityManager
803
     *
804
     * @param EntityManager $entityManager
805
     * @return Entity
806
     * @throws Exceptions\NoConnection
807
     * @throws Exceptions\NoEntity
808
     * @throws Exceptions\NotScalar
809
     * @throws Exceptions\UnsupportedDriver
810
     * @throws IncompletePrimaryKey
811
     * @throws InvalidConfiguration
812
     * @throws InvalidName
813
     * @throws NoEntityManager
814
     */
815 13
    public function save(EntityManager $entityManager = null)
816
    {
817 13
        $entityManager = $entityManager ?: $this->entityManager;
818
819 13
        if (!$entityManager) {
820 1
            throw new NoEntityManager('No entity manager given');
821
        }
822
823 12
        $inserted = false;
824 12
        $updated = false;
825
826
        try {
827
            // this may throw if the primary key is implemented but we using this to omit duplicated code
828 12
            if (!$entityManager->sync($this)) {
829 2
                $entityManager->insert($this, false);
830 2
                $inserted = true;
831 5
            } elseif ($this->isDirty()) {
832 4
                $this->preUpdate();
833 4
                $entityManager->update($this);
834 7
                $updated = true;
835
            }
836 5
        } catch (IncompletePrimaryKey $e) {
837 5
            if (static::isAutoIncremented()) {
838 4
                $this->prePersist();
839 4
                $id = $entityManager->insert($this);
840 4
                $this->data[static::getColumnName(static::getPrimaryKeyVars()[0])] = $id;
841 4
                $inserted = true;
842
            } else {
843 1
                throw $e;
844
            }
845
        }
846
847 11
        if ($inserted || $updated) {
848 10
            $inserted && $this->postPersist();
849 10
            $updated && $this->postUpdate();
850 10
            $entityManager->sync($this, true);
851
        }
852
853 11
        return $this;
854
    }
855
856
    /**
857
     * Fetches related objects
858
     *
859
     * For relations with cardinality many it returns an EntityFetcher. Otherwise it returns the entity.
860
     *
861
     * It will throw an error for non owner when the key is incomplete.
862
     *
863
     * @param string $relation The relation to fetch
864
     * @param EntityManager $entityManager The EntityManager to use
865
     * @return Entity|EntityFetcher|Entity[]
866
     * @throws Exceptions\NoConnection
867
     * @throws Exceptions\NoEntity
868
     * @throws IncompletePrimaryKey
869
     * @throws InvalidConfiguration
870
     * @throws NoEntityManager
871
     * @throws UndefinedRelation
872
     */
873 16
    public function fetch($relation, EntityManager $entityManager = null, $getAll = false)
874
    {
875 16
        $entityManager = $entityManager ?: $this->entityManager;
876
877 16
        if (!$entityManager) {
878 1
            throw new NoEntityManager('No entity manager given');
879
        }
880
881 15
        $myRelDef = static::getRelationDefinition($relation);
882
883
        // the owner can directly fetch by primary key
884 15
        if (isset($myRelDef[self::OPT_RELATION_REFERENCE]) &&
885 15
            !isset($myRelDef[self::OPT_RELATION_TABLE])) {
886 3
            $key = array_map([$this, '__get'], array_keys($myRelDef[self::OPT_RELATION_REFERENCE]));
887
888 3
            if (in_array(null, $key)) {
889 2
                return null;
890
            }
891
892 1
            return $entityManager->fetch($myRelDef[self::OPT_RELATION_CLASS], $key);
893
        }
894
895 12
        $class = $myRelDef[self::OPT_RELATION_CLASS];
896 12
        $oppRelDef = $class::getRelationDefinition($myRelDef[self::OPT_RELATION_OPPONENT]);
897
898 11
        if (!isset($oppRelDef[self::OPT_RELATION_REFERENCE])) {
899 1
            throw new InvalidConfiguration('Reference is not defined in opponent');
900
        }
901
902 10
        $reference = !isset($myRelDef[self::OPT_RELATION_TABLE]) ?
903 4
            array_flip($oppRelDef[self::OPT_RELATION_REFERENCE]) :
904 10
            $myRelDef[self::OPT_RELATION_REFERENCE];
905 10
        $foreignKey = $this->getForeignKey($reference);
906
907 8
        if (!isset($myRelDef[self::OPT_RELATION_TABLE])) {
908
            /** @var EntityFetcher $fetcher */
909 3
            $fetcher = $entityManager->fetch($class);
910 3
            foreach ($foreignKey as $col => $value) {
911 3
                $fetcher->where($col, $value);
912
            }
913
914 3
            if ($myRelDef[self::OPT_RELATION_CARDINALITY] === self::CARDINALITY_ONE) {
915 1
                return $fetcher->one();
916 2
            } elseif ($getAll) {
917 1
                return $fetcher->all();
918
            }
919
920 1
            return $fetcher;
921
        }
922
923 5
        $table = $entityManager->escapeIdentifier($myRelDef[self::OPT_RELATION_TABLE]);
924
925 5
        if ($getAll) {
926 1
            $query = new QueryBuilder($table, '', $entityManager);
927
928 1
            foreach ($oppRelDef[self::OPT_RELATION_REFERENCE] as $t0Var => $fkCol) {
929 1
                $query->column($entityManager->escapeIdentifier($fkCol));
930
            }
931
932 1
            foreach ($foreignKey as $col => $value) {
933 1
                $query->where($entityManager->escapeIdentifier($col), $value);
934
            }
935
936 1
            $result = $entityManager->getConnection()->query($query->getQuery());
937 1
            $primaryKeys = $result->fetchAll(\PDO::FETCH_NUM);
938
939
            /** @var Entity[] $result */
940 1
            $result = [];
941 1
            foreach ($primaryKeys as $primaryKey) {
942 1
                if ($entity = $entityManager->fetch($class, $primaryKey)) {
943 1
                    $result[] = $entity;
944
                }
945
            }
946
947 1
            return $result;
948
        } else {
949
            /** @var EntityFetcher $fetcher */
950 4
            $fetcher = $entityManager->fetch($class);
951
952 4
            $expression = [];
953 4
            foreach ($oppRelDef[self::OPT_RELATION_REFERENCE] as $t0Var => $fkCol) {
954 4
                $expression[] = $table . '.' . $entityManager->escapeIdentifier($fkCol) . ' = t0.' . $t0Var;
955
            }
956
957 4
            $fetcher->join($table, implode(' AND ', $expression));
958
959 4
            foreach ($foreignKey as $col => $value) {
960 4
                $fetcher->where($table . '.' . $entityManager->escapeIdentifier($col), $value);
961
            }
962 4
            return $fetcher;
963
        }
964
    }
965
966
    /**
967
     * Get the foreign key for the given reference
968
     *
969
     * @param array $reference
970
     * @return array
971
     * @throws IncompletePrimaryKey
972
     */
973 10
    private function getForeignKey($reference)
974
    {
975 10
        $foreignKey = [];
976
977 10
        foreach ($reference as $var => $fkCol) {
978 10
            $value = $this->__get($var);
979
980 10
            if ($value === null) {
981 2
                throw new IncompletePrimaryKey('Key incomplete for join');
982
            }
983
984 8
            $foreignKey[$fkCol] = $value;
985
        }
986
987 8
        return $foreignKey;
988
    }
989
990
    /**
991
     * Get the primary key
992
     *
993
     * @return array
994
     * @throws IncompletePrimaryKey
995
     */
996 38
    public function getPrimaryKey()
997
    {
998 38
        $primaryKey = [];
999 38
        foreach (static::getPrimaryKeyVars() as $var) {
1000 38
            $value = $this->$var;
1001 38
            if ($value === null) {
1002 4
                throw new IncompletePrimaryKey('Incomplete primary key - missing ' . $var);
1003
            }
1004 36
            $primaryKey[$var] = $value;
1005
        }
1006 34
        return $primaryKey;
1007
    }
1008
1009
    /**
1010
     * Get current data
1011
     *
1012
     * @return array
1013
     * @internal
1014
     */
1015 17
    public function getData()
1016
    {
1017 17
        return $this->data;
1018
    }
1019
1020
    /**
1021
     * Set new original data
1022
     *
1023
     * @param array $data
1024
     * @internal
1025
     */
1026 18
    public function setOriginalData(array $data)
1027
    {
1028 18
        $this->originalData = $data;
1029 18
    }
1030
1031
    /**
1032
     * Empty event handler
1033
     *
1034
     * Get called when something is changed with magic setter.
1035
     *
1036
     * @param string $var The variable that got changed.merge(node.inheritedProperties)
1037
     * @param mixed  $oldValue The old value of the variable
1038
     * @param mixed  $value The new value of the variable
1039
     */
1040 5
    public function onChange($var, $oldValue, $value)
1041
    {
1042 5
    }
1043
1044
    /**
1045
     * Empty event handler
1046
     *
1047
     * Get called when the entity get initialized.
1048
     *
1049
     * @param bool $new Whether or not the entity is new or from database
1050
     */
1051 98
    public function onInit($new)
1052
    {
1053 98
    }
1054
1055
    /**
1056
     * Empty event handler
1057
     *
1058
     * Get called before the entity get inserted in database.
1059
     */
1060 3
    public function prePersist()
1061
    {
1062 3
    }
1063
1064
    /**
1065
     * Empty event handler
1066
     *
1067
     * Get called after the entity got inserted in database.
1068
     */
1069 5
    public function postPersist()
1070
    {
1071 5
    }
1072
1073
    /**
1074
     * Empty event handler
1075
     *
1076
     * Get called before the entity get updated in database.
1077
     */
1078 3
    public function preUpdate()
1079
    {
1080 3
    }
1081
1082
    /**
1083
     * Empty event handler
1084
     *
1085
     * Get called after the entity got updated in database.
1086
     */
1087 3
    public function postUpdate()
1088
    {
1089 3
    }
1090
1091
    /**
1092
     * String representation of data
1093
     *
1094
     * @link http://php.net/manual/en/serializable.serialize.php
1095
     * @return string
1096
     */
1097 1
    public function serialize()
1098
    {
1099 1
        return serialize($this->data);
1100
    }
1101
1102
    /**
1103
     * Constructs the object
1104
     *
1105
     * @link http://php.net/manual/en/serializable.unserialize.php
1106
     * @param string $serialized The string representation of data
1107
     */
1108 2
    public function unserialize($serialized)
1109
    {
1110 2
        $this->data = unserialize($serialized);
1111 2
        $this->onInit(false);
1112 2
    }
1113
}
1114