Completed
Push — master ( 4370c7...e76084 )
by Thomas
22s queued 10s
created

Entity::postPersist()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 0
dl 0
loc 2
ccs 0
cts 0
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 2
1
<?php
2
3
namespace ORM;
4
5
use ORM\Dbal\Error;
6
use ORM\Entity\EventHandlers;
7
use ORM\Entity\GeneratesPrimaryKeys;
8
use ORM\Entity\Naming;
9
use ORM\Entity\Relations;
10
use ORM\Entity\Validation;
11
use ORM\EntityManager as EM;
12
use ORM\Exception\IncompletePrimaryKey;
13
use ORM\Exception\UnknownColumn;
14
use ReflectionClass;
15
use Serializable;
16
17
/**
18
 * Definition of an entity
19
 *
20
 * The instance of an entity represents a row of the table and the statics variables and methods describe the database
21
 * table.
22
 *
23
 * This is the main part where your configuration efforts go. The following properties and methods are well documented
24
 * in the manual under [https://tflori.github.io/orm/entityDefinition.html](Entity Definition).
25
 *
26
 * @package ORM
27
 * @link    https://tflori.github.io/orm/entityDefinition.html Entity Definition
28
 * @author  Thomas Flori <[email protected]>
29
 */
30
abstract class Entity implements Serializable
31
{
32
    use Validation, Relations, Naming, EventHandlers;
0 ignored issues
show
Bug introduced by
The trait ORM\Entity\Validation requires the property $name which is not provided by ORM\Entity.
Loading history...
33
34
    /** @deprecated Use Relation::OPT_CLASS instead */
35
    const OPT_RELATION_CLASS       = 'class';
36
    /** @deprecated Use Relation::OPT_CARDINALITY instead */
37
    const OPT_RELATION_CARDINALITY = 'cardinality';
38
    /** @deprecated Use Relation::OPT_REFERENCE instead */
39
    const OPT_RELATION_REFERENCE   = 'reference';
40
    /** @deprecated Use Relation::OPT_OPPONENT instead */
41
    const OPT_RELATION_OPPONENT    = 'opponent';
42
    /** @deprecated Use Relation::OPT_TABLE instead */
43
    const OPT_RELATION_TABLE       = 'table';
44
45
    /** The variable(s) used for primary key.
46
     * @var string[]|string */
47
    protected static $primaryKey = ['id'];
48
49
    /** Whether or not the primary key is auto incremented.
50
     * @var bool */
51
    protected static $autoIncrement = true;
52
53
    /** Additional attributes to show in toArray method
54
     * @var array  */
55
    protected static $includedAttributes = [];
56
57
    /** Attributes to hide for toArray method (overruled by $attributes parameter)
58
     * @var array  */
59
    protected static $excludedAttributes = [];
60
61
    /** The reflections of the classes.
62
     * @internal
63
     * @var ReflectionClass[] */
64
    protected static $reflections = [];
65
66
    /** The current data of a row.
67
     * @var mixed[] */
68
    protected $data = [];
69
70
    /** The original data of the row.
71
     * @var mixed[] */
72
    protected $originalData = [];
73
74
    /** The entity manager from which this entity got created
75
     * @var EM */
76
    protected $entityManager;
77
78
    /**
79
     * Constructor
80
     *
81
     * It calls ::onInit() after initializing $data and $originalData.
82
     *
83
     * @param mixed[] $data          The current data
84
     * @param EM      $entityManager The EntityManager that created this entity
85
     * @param bool    $fromDatabase  Whether or not the data comes from database
86
     */
87
    final public function __construct(array $data = [], EM $entityManager = null, $fromDatabase = false)
88
    {
89
        if ($fromDatabase) {
90
            $this->originalData = $data;
91
        }
92
        $this->data          = array_merge($this->data, $data);
93
        $this->entityManager = $entityManager ?: EM::getInstance(static::class);
94
        $this->onInit(!$fromDatabase);
95
    }
96
97
    /**
98
     * Create an entityFetcher for this entity
99
     *
100
     * @return EntityFetcher
101
     */
102
    public static function query()
103
    {
104
        return EM::getInstance(static::class)->fetch(static::class);
105
    }
106
107
    /**
108
     * Get the primary key vars
109
     *
110
     * The primary key can consist of multiple columns. You should configure the vars that are translated to these
111
     * columns.
112
     *
113
     * @return array
114 122
     */
115
    public static function getPrimaryKeyVars()
116 122
    {
117 14
        return !is_array(static::$primaryKey) ? [ static::$primaryKey ] : static::$primaryKey;
118
    }
119 122
120 122
    /**
121 122
     * Check if the table has a auto increment column
122 122
     *
123
     * @return bool
124
     */
125
    public static function isAutoIncremented()
126
    {
127
        return count(static::getPrimaryKeyVars()) > 1 ? false : static::$autoIncrement;
128
    }
129
130
    /**
131
     * @param EM $entityManager
132
     * @return static
133
     */
134
    public function setEntityManager(EM $entityManager)
135
    {
136
        $this->entityManager = $entityManager;
137 166
        return $this;
138
    }
139 166
140 6
    /**
141
     * @param string $attribute
142
     * @return mixed|null
143 164
     * @see self::getAttribute
144 164
     * @codeCoverageIgnore Alias for getAttribute
145
     */
146
    public function __get($attribute)
147
    {
148
        return $this->getAttribute($attribute);
149
    }
150
151
    /**
152
     * Get the value from $attribute
153
     *
154
     * If there is a custom getter this method get called instead.
155 72
     *
156
     * @param string $attribute The variable to get
157 72
     * @return mixed|null
158
     * @link https://tflori.github.io/orm/entities.html Working with entities
159
     */
160
    public function getAttribute($attribute)
161
    {
162
        $em     = EM::getInstance(static::class);
163
        $getter = $em->getNamer()->getMethodName('get' . ucfirst($attribute), self::$namingSchemeMethods);
164
165
        if (method_exists($this, $getter) && is_callable([ $this, $getter ])) {
166
            return $this->$getter();
167
        } else {
168
            $col    = static::getColumnName($attribute);
169 154
            $result = isset($this->data[$col]) ? $this->data[$col] : null;
170
171 154
            if (!$result && isset(static::$relations[$attribute]) && isset($this->entityManager)) {
172 11
                return $this->getRelated($attribute);
173
            }
174
175 143
            return $result;
176 143
        }
177
    }
178
179
    /**
180
     * Check if a column is defined
181
     *
182
     * @param $attribute
183
     * @return bool
184 19
     */
185
    public function __isset($attribute)
186 19
    {
187
        $em     = EM::getInstance(static::class);
188
        $getter = $em->getNamer()->getMethodName('get' . ucfirst($attribute), self::$namingSchemeMethods);
189
190
        if (method_exists($this, $getter) && is_callable([ $this, $getter ])) {
191
            return $this->$getter() !== null;
192
        } else {
193 12
            $col = static::getColumnName($attribute);
194
            $isset = isset($this->data[$col]);
195 12
196 12
            if (!$isset && isset(static::$relations[$attribute])) {
197
                return !empty($this->getRelated($attribute));
198
            }
199
200
            return $isset;
201
        }
202
    }
203
204
    /**
205
     * @param string $attribute The variable to change
206
     * @param mixed $value The value to store
207
     * @see self::getAttribute
208
     * @codeCoverageIgnore Alias for getAttribute
209
     */
210 112
    public function __set($attribute, $value)
211
    {
212 112
        $this->setAttribute($attribute, $value);
213 112
    }
214
215 112
    /**
216 4
     * Set $attribute to $value
217
     *
218 108
     * Tries to call custom setter before it stores the data directly. If there is a setter the setter needs to store
219 108
     * data that should be updated in the database to $data. Do not store data in $originalData as it will not be
220
     * written and give wrong results for dirty checking.
221 108
     *
222 1
     * The onChange event is called after something got changed.
223
     *
224
     * The method throws an error when the validation fails (also when the column does not exist).
225 107
     *
226
     * @param string $attribute The variable to change
227
     * @param mixed $value The value to store
228
     * @return static
229
     * @link https://tflori.github.io/orm/entities.html Working with entities
230
     * @throws Error
231
     */
232
    public function setAttribute($attribute, $value)
233
    {
234
        $col = $this->getColumnName($attribute);
235 3
236
        $em     = EM::getInstance(static::class);
237 3
        $setter = $em->getNamer()->getMethodName('set' . ucfirst($attribute), self::$namingSchemeMethods);
238 3
239
        if (method_exists($this, $setter) && is_callable([ $this, $setter ])) {
240 3
            $oldValue   = $this->__get($attribute);
241 1
            $md5OldData = md5(serialize($this->data));
242
            $this->$setter($value);
243 2
            $changed = $md5OldData !== md5(serialize($this->data));
244 2
        } else {
245
            if (static::isValidatorEnabled()) {
246 2
                $error = static::validate($attribute, $value);
247 1
                if ($error instanceof Error) {
0 ignored issues
show
introduced by
$error is always a sub-type of ORM\Dbal\Error.
Loading history...
248
                    throw $error;
249
                }
250 1
            }
251
252
            $oldValue         = $this->__get($attribute);
253
            $changed          = (isset($this->data[$col]) ? $this->data[$col] : null) !== $value;
254
            $this->data[$col] = $value;
255
        }
256
257
        if ($changed) {
258
            $this->onChange($attribute, $oldValue, $this->__get($attribute));
259
        }
260
261
        return $this;
262
    }
263
264
    /**
265
     * Fill the entity with $data
266
     *
267
     * When $checkMissing is set to true it also proves that the absent columns are nullable.
268
     *
269
     * @param array $data
270 38
     * @param bool $ignoreUnknown
271
     * @param bool $checkMissing
272 38
     * @throws UnknownColumn
273
     * @throws Error
274 38
     */
275 38
    public function fill(array $data, $ignoreUnknown = false, $checkMissing = false)
276
    {
277 38
        foreach ($data as $attribute => $value) {
278 3
            try {
279 3
                $this->__set($attribute, $value);
280 3
            } catch (UnknownColumn $e) {
281 3
                if (!$ignoreUnknown) {
282
                    throw $e;
283 35
                }
284 35
            }
285
        }
286 1
287
        if ($checkMissing && is_array($errors = $this->isValid())) {
288
            throw $errors[0];
289 31
        }
290 31
    }
291 31
292
    /**
293
     * Resets the entity or $attribute to original data
294 34
     *
295 31
     * @param string $attribute Reset only this variable or all variables
296
     */
297 34
    public function reset($attribute = null)
298
    {
299
        if (!empty($attribute)) {
300
            $col = static::getColumnName($attribute);
301
            if (isset($this->originalData[$col])) {
302
                $this->data[$col] = $this->originalData[$col];
303
            } else {
304
                unset($this->data[$col]);
305
            }
306
            return;
307
        }
308
309
        $this->data = $this->originalData;
310 8
    }
311
312 8
    /**
313
     * Save the entity to EntityManager
314 7
     *
315 2
     * @return Entity
316 2
     * @throws IncompletePrimaryKey
317 7
     */
318
    public function save()
319
    {
320
        // @codeCoverageIgnoreStart
321
        if (func_num_args() === 1 && func_get_arg(0) instanceof EM) {
322 7
            trigger_error(
323 1
                'Passing EntityManager to save is deprecated. Use ->setEntityManager() to overwrite',
324
                E_USER_DEPRECATED
325 6
            );
326
        }
327
        // @codeCoverageIgnoreEnd
328
329
        $inserted = false;
330
        $updated  = false;
331
332
        try {
333 22
            // this may throw if the primary key is auto incremented but we using this to omit duplicated code
334
            if (!$this->entityManager->sync($this)) {
335 22
                $this->prePersist();
336 3
                $inserted = $this->entityManager->insert($this, false);
337 3
            } elseif ($this->isDirty()) {
338 2
                $this->preUpdate();
339
                $updated = $this->entityManager->update($this);
340 1
            }
341
        } catch (IncompletePrimaryKey $e) {
342 3
            if (static::isAutoIncremented()) {
343
                $this->prePersist();
344
                $inserted = $this->entityManager->insert($this);
345 19
            } elseif ($this instanceof GeneratesPrimaryKeys) {
346 19
                $this->generatePrimaryKey();
347
                $this->prePersist();
348
                $inserted = $this->entityManager->insert($this);
349
            } else {
350
                throw $e;
351
            }
352
        }
353
354
        $inserted && $this->postPersist();
355
        $updated && $this->postUpdate();
356
357
        return $this;
358
    }
359
360
    /**
361 16
     * Generates a primary key
362
     *
363
     * This method should only be executed from save method.
364
     * @codeCoverageIgnore no operations
365
     */
366
    protected function generatePrimaryKey()
367
    {
368
        // no operation by default
369
    }
370
371
    /**
372 16
     * Checks if entity or $attribute got changed
373 16
     *
374
     * @param string $attribute Check only this variable or all variables
375
     * @return bool
376
     */
377 16
    public function isDirty($attribute = null)
378 2
    {
379 2
        if (!empty($attribute)) {
380 4
            $col = static::getColumnName($attribute);
381 3
            return (isset($this->data[$col]) ? $this->data[$col] : null) !==
382 6
                   (isset($this->originalData[$col]) ? $this->originalData[$col] : null);
383
        }
384 10
385 10
        ksort($this->data);
386 8
        ksort($this->originalData);
387 8
388
        return serialize($this->data) !== serialize($this->originalData);
389 1
    }
390 1
391 1
    /**
392
     * Get the primary key
393 1
     *
394
     * @return array
395
     * @throws IncompletePrimaryKey
396
     */
397 14
    public function getPrimaryKey()
398 14
    {
399
        $primaryKey = [];
400 14
        foreach (static::getPrimaryKeyVars() as $attribute) {
401
            $value = $this->$attribute;
402
            if ($value === null) {
403
                throw new IncompletePrimaryKey('Incomplete primary key - missing ' . $attribute);
404
            }
405
            $primaryKey[$attribute] = $value;
406
        }
407
        return $primaryKey;
408
    }
409
410
    /**
411
     * Get current data
412
     *
413
     * @return array
414
     * @internal
415
     */
416
    public function getData()
417
    {
418
        return $this->data;
419
    }
420
421 20
    /**
422
     * Set new original data
423 20
     *
424 4
     * @param array $data
425 4
     * @internal
426 4
     */
427
    public function setOriginalData(array $data)
428
    {
429 17
        $this->originalData = $data;
430 17
    }
431
432 17
    /**
433
     * Get an array of the entity
434
     *
435
     * @param array $attributes
436
     * @param bool $includeRelations
437
     * @return array
438
     */
439
    public function toArray(array $attributes = [], $includeRelations = true)
440
    {
441
        if (empty($attributes)) {
442
            $attributes = array_keys(static::$columnAliases);
443
            $attributes = array_merge($attributes, array_map([$this, 'getAttributeName'], array_keys($this->data)));
444
            $attributes = array_merge($attributes, static::$includedAttributes);
445
            $attributes = array_diff($attributes, static::$excludedAttributes);
446
        }
447
448
        $values = array_map(function ($attribute) {
449
            return $this->getAttribute($attribute);
450
        }, $attributes);
451
452
        $result = (array)array_combine($attributes, $values);
453
454
        if ($includeRelations) {
455
            foreach ($this->relatedObjects as $relation => $relatedObject) {
456
                if (is_array($relatedObject)) {
457
                    $result[$relation] = array_map(function (Entity $relatedObject) {
458
                        return $relatedObject->toArray();
459
                    }, $relatedObject);
460
                } elseif ($relatedObject instanceof Entity) {
461
                    $result[$relation] = $relatedObject->toArray();
462
                }
463
            }
464
        }
465
466
        return $result;
467
    }
468
469
    /**
470
     * String representation of data
471
     *
472
     * @link http://php.net/manual/en/serializable.serialize.php
473
     * @return string
474
     */
475
    public function serialize()
476
    {
477
        return serialize([ $this->data, $this->relatedObjects ]);
478
    }
479
480
    /**
481
     * Constructs the object
482
     *
483
     * @link http://php.net/manual/en/serializable.unserialize.php
484
     * @param string $serialized The string representation of data
485
     */
486
    public function unserialize($serialized)
487
    {
488
        list($this->data, $this->relatedObjects) = unserialize($serialized);
489
        $this->entityManager = EM::getInstance(static::class);
490
        $this->onInit(false);
491
    }
492
}
493