Passed
Push — master ( f605de...e0e33d )
by Thomas
01:28
created

EntityManager::buildChecksum()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 3
ccs 0
cts 0
cp 0
rs 10
cc 1
nc 1
nop 1
crap 2
1
<?php
2
3
namespace ORM;
4
5
use ORM\Dbal\Column;
6
use ORM\Dbal\Dbal;
7
use ORM\Dbal\Other;
8
use ORM\Dbal\Table;
9
use ORM\Exception\IncompletePrimaryKey;
10
use ORM\Exception\InvalidConfiguration;
11
use ORM\Exception\NoConnection;
12
use ORM\Exception\NoEntity;
13
14
/**
15
 * The EntityManager that manages the instances of Entities.
16
 *
17
 * @package ORM
18
 * @author  Thomas Flori <[email protected]>
19
 */
20
class EntityManager
21
{
22
    const OPT_CONNECTION            = 'connection';
23
    const OPT_TABLE_NAME_TEMPLATE   = 'tableNameTemplate';
24
    const OPT_NAMING_SCHEME_TABLE   = 'namingSchemeTable';
25
    const OPT_NAMING_SCHEME_COLUMN  = 'namingSchemeColumn';
26
    const OPT_NAMING_SCHEME_METHODS = 'namingSchemeMethods';
27
    const OPT_QUOTING_CHARACTER     = 'quotingChar';
28
    const OPT_IDENTIFIER_DIVIDER    = 'identifierDivider';
29
    const OPT_BOOLEAN_TRUE          = 'true';
30
    const OPT_BOOLEAN_FALSE         = 'false';
31
    const OPT_DBAL_CLASS            = 'dbalClass';
32
33
    /** @deprecated */
34
    const OPT_MYSQL_BOOLEAN_TRUE = 'mysqlTrue';
35
    /** @deprecated */
36
    const OPT_MYSQL_BOOLEAN_FALSE = 'mysqlFalse';
37
    /** @deprecated */
38
    const OPT_SQLITE_BOOLEAN_TRUE = 'sqliteTrue';
39
    /** @deprecated */
40
    const OPT_SQLITE_BOOLEAN_FALSE = 'sqliteFalse';
41
    /** @deprecated */
42
    const OPT_PGSQL_BOOLEAN_TRUE = 'pgsqlTrue';
43
    /** @deprecated */
44
    const OPT_PGSQL_BOOLEAN_FALSE = 'pgsqlFalse';
45
46
    /** Connection to database
47
     * @var \PDO|callable|DbConfig */
48
    protected $connection;
49
50
    /** The Database Abstraction Layer
51
     * @var Dbal */
52
    protected $dbal;
53
54
    /** The Namer instance
55
     * @var Namer */
56
    protected $namer;
57
58
    /** The Entity map
59
     * @var Entity[][] */
60
    protected $map = [];
61
62
    /** The options set for this instance
63
     * @var array */
64
    protected $options = [];
65
66
    /** Already fetched column descriptions
67
     * @var Table[]|Column[][] */
68
    protected $descriptions = [];
69
70
    /** Classes forcing bulk insert
71
     * @var BulkInsert[] */
72
    protected $bulkInserts = [];
73
74
    /** Mapping for EntityManager instances
75
     * @var EntityManager[string]|EntityManager[string][string] */
0 ignored issues
show
Documentation Bug introduced by
The doc comment EntityManager[string]|En...Manager[string][string] at position 1 could not be parsed: Expected ']' at position 1, but found '['.
Loading history...
76
    protected static $emMapping = [
77
        'byClass'     => [],
78
        'byNameSpace' => [],
79
        'byParent'    => [],
80
        'last'        => null,
81
    ];
82
83
    /**
84
     * Constructor
85 755
     *
86
     * @param array $options Options for the new EntityManager
87 755
     */
88 11
    public function __construct($options = [])
89
    {
90
        foreach ($options as $option => $value) {
91 755
            $this->setOption($option, $value);
92 755
        }
93
94
        self::$emMapping['last'] = $this;
95
    }
96
97
    /**
98
     * Get an instance of the EntityManager.
99
     *
100
     * If no class is given it gets $class from backtrace.
101
     *
102
     * It first gets tries the EntityManager for the Namespace of $class, then for the parents of $class. If no
103
     * EntityManager is found it returns the last created EntityManager (null if no EntityManager got created).
104
     *
105 312
     * @param string $class
106
     * @return EntityManager
107 312
     */
108 42
    public static function getInstance($class = null)
109 42
    {
110 1
        if (empty($class)) {
111
            $trace = debug_backtrace();
112 41
            if (empty($trace[1]['class'])) {
113
                return self::$emMapping['last'];
114
            }
115 311
            $class = $trace[1]['class'];
116 311
        }
117 309
118
        if (!isset(self::$emMapping['byClass'][$class])) {
119
            if (!($em = self::getInstanceByParent($class)) && !($em = self::getInstanceByNameSpace($class))) {
120 2
                return self::$emMapping['last'];
121
            }
122
123 2
            self::$emMapping['byClass'][$class] = $em;
124
        }
125
126
        return self::$emMapping['byClass'][$class];
127
    }
128
129
    /**
130
     * Get the instance by NameSpace mapping
131
     *
132 310
     * @param $class
133
     * @return EntityManager
134 310
     */
135 2
    private static function getInstanceByNameSpace($class)
136 2
    {
137
        foreach (self::$emMapping['byNameSpace'] as $nameSpace => $em) {
138
            if (strpos($class, $nameSpace) === 0) {
139
                return $em;
140 309
            }
141
        }
142
143
        return null;
144
    }
145
146
    /**
147
     * Get the instance by Parent class mapping
148
     *
149 311
     * @param $class
150
     * @return EntityManager
151
     */
152 311
    private static function getInstanceByParent($class)
153 309
    {
154
        // we don't need a reflection when we don't have mapping byParent
155
        if (empty(self::$emMapping['byParent'])) {
156 2
            return null;
157 2
        }
158 2
159 2
        /** @noinspection PhpUnhandledExceptionInspection */
160
        $reflection = new \ReflectionClass($class);
161
        foreach (self::$emMapping['byParent'] as $parentClass => $em) {
162
            if ($reflection->isSubclassOf($parentClass)) {
163 1
                return $em;
164
            }
165
        }
166
167
        return null;
168
    }
169
170
    /**
171
     * Define $this EntityManager as the default EntityManager for $nameSpace
172 2
     *
173
     * @param $nameSpace
174 2
     * @return static
175 2
     */
176
    public function defineForNamespace($nameSpace)
177
    {
178
        self::$emMapping['byNameSpace'][$nameSpace] = $this;
179
        return $this;
180
    }
181
182
    /**
183
     * Define $this EntityManager as the default EntityManager for subClasses of $class
184 2
     *
185
     * @param $class
186 2
     * @return static
187 2
     */
188
    public function defineForParent($class)
189
    {
190
        self::$emMapping['byParent'][$class] = $this;
191
        return $this;
192
    }
193
194
    /**
195
     * Set $option to $value
196
     *
197 15
     * @param string $option One of OPT_* constants
198
     * @param mixed $value
199
     * @return static
200 15
     */
201 1
    public function setOption($option, $value)
202 1
    {
203
        switch ($option) {
204 14
            case self::OPT_CONNECTION:
205 13
                $this->setConnection($value);
206 12
                break;
207 3
208 3
            case self::OPT_SQLITE_BOOLEAN_TRUE:
0 ignored issues
show
introduced by
The constant ORM\EntityManager::OPT_SQLITE_BOOLEAN_TRUE has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

208
            case /** @scrutinizer ignore-deprecated */ self::OPT_SQLITE_BOOLEAN_TRUE:
Loading history...
209
            case self::OPT_MYSQL_BOOLEAN_TRUE:
0 ignored issues
show
introduced by
The constant ORM\EntityManager::OPT_MYSQL_BOOLEAN_TRUE has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

209
            case /** @scrutinizer ignore-deprecated */ self::OPT_MYSQL_BOOLEAN_TRUE:
Loading history...
210 11
            case self::OPT_PGSQL_BOOLEAN_TRUE:
0 ignored issues
show
introduced by
The constant ORM\EntityManager::OPT_PGSQL_BOOLEAN_TRUE has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

210
            case /** @scrutinizer ignore-deprecated */ self::OPT_PGSQL_BOOLEAN_TRUE:
Loading history...
211 10
                $option = self::OPT_BOOLEAN_TRUE;
212 9
                break;
213 3
214 3
            case self::OPT_SQLITE_BOOLEAN_FALSE:
0 ignored issues
show
introduced by
The constant ORM\EntityManager::OPT_SQLITE_BOOLEAN_FALSE has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

214
            case /** @scrutinizer ignore-deprecated */ self::OPT_SQLITE_BOOLEAN_FALSE:
Loading history...
215
            case self::OPT_MYSQL_BOOLEAN_FALSE:
0 ignored issues
show
introduced by
The constant ORM\EntityManager::OPT_MYSQL_BOOLEAN_FALSE has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

215
            case /** @scrutinizer ignore-deprecated */ self::OPT_MYSQL_BOOLEAN_FALSE:
Loading history...
216
            case self::OPT_PGSQL_BOOLEAN_FALSE:
0 ignored issues
show
introduced by
The constant ORM\EntityManager::OPT_PGSQL_BOOLEAN_FALSE has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

216
            case /** @scrutinizer ignore-deprecated */ self::OPT_PGSQL_BOOLEAN_FALSE:
Loading history...
217 15
                $option = self::OPT_BOOLEAN_FALSE;
218 15
                break;
219
        }
220
221
        $this->options[$option] = $value;
222
        return $this;
223
    }
224
225
    /**
226
     * Get $option
227 10
     *
228
     * @param $option
229 10
     * @return mixed
230
     */
231
    public function getOption($option)
232
    {
233
        return isset($this->options[$option]) ? $this->options[$option] : null;
234
    }
235
236
    /**
237
     * Add connection after instantiation
238
     *
239
     * The connection can be an array of parameters for DbConfig::__construct(), a callable function that returns a PDO
240
     * instance, an instance of DbConfig or a PDO instance itself.
241
     *
242
     * When it is not a PDO instance the connection get established on first use.
243 36
     *
244
     * @param mixed $connection A configuration for (or a) PDO instance
245 36
     * @throws InvalidConfiguration
246 6
     */
247
    public function setConnection($connection)
248 30
    {
249 27
        if (is_callable($connection) || $connection instanceof DbConfig) {
250 27
            $this->connection = $connection;
251 3
        } else {
252 2
            if ($connection instanceof \PDO) {
253 2
                $connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
254
                $this->connection = $connection;
255 1
            } elseif (is_array($connection)) {
256 1
                $this->connection = new DbConfig(...$connection);
257
            } else {
258
                throw new InvalidConfiguration(
259
                    'Connection must be callable, DbConfig, PDO or an array of parameters for DbConfig::__constructor'
260 35
                );
261
            }
262
        }
263
    }
264
265
    /**
266
     * Get the pdo connection.
267
     *
268 20
     * @return \PDO
269
     * @throws NoConnection
270 20
     * @throws NoConnection
271 1
     */
272
    public function getConnection()
273
    {
274 19
        if (!$this->connection) {
275 7
            throw new NoConnection('No database connection');
276
        }
277 4
278 4
        if (!$this->connection instanceof \PDO) {
279 4
            if ($this->connection instanceof DbConfig) {
280 4
                /** @var DbConfig $dbConfig */
281 4
                $dbConfig         = $this->connection;
282 4
                $this->connection = new \PDO(
283
                    $dbConfig->getDsn(),
284
                    $dbConfig->user,
285 3
                    $dbConfig->pass,
286 3
                    $dbConfig->attributes
287 1
                );
288
            } else {
289 2
                $pdo = call_user_func($this->connection);
290
                if (!$pdo instanceof \PDO) {
291 6
                    throw new NoConnection('Getter does not return PDO instance');
292
                }
293
                $this->connection = $pdo;
294 18
            }
295
            $this->connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
296
        }
297
298
        return $this->connection;
299
    }
300
301
    /**
302 18
     * Get the Datbase Abstraction Layer
303
     *
304 18
     * @return Dbal
305 18
     */
306 18
    public function getDbal()
307 18
    {
308 18
        if (!$this->dbal) {
309
            $connectionType = $this->getConnection()->getAttribute(\PDO::ATTR_DRIVER_NAME);
310 18
            $options        = &$this->options;
311 2
            $dbalClass      = isset($options[self::OPT_DBAL_CLASS]) ?
312
                $options[self::OPT_DBAL_CLASS] : __NAMESPACE__ . '\\Dbal\\' . ucfirst($connectionType);
313
314 18
            if (!class_exists($dbalClass)) {
315
                $dbalClass = Other::class;
316
            }
317 18
318
            $this->dbal = new $dbalClass($this, $options);
319
        }
320
321
        return $this->dbal;
322
    }
323
324
    /**
325
     * Get the Namer instance
326
     *
327
     * @return Namer
328
     * @codeCoverageIgnore trivial code...
329
     */
330
    public function getNamer()
331
    {
332
        if (!$this->namer) {
333
            $this->namer = new Namer($this->options);
334
        }
335
336
        return $this->namer;
337
    }
338
339
    /**
340
     * Synchronizing $entity with database
341
     *
342
     * If $reset is true it also calls reset() on $entity.
343
     *
344
     * @param Entity $entity
345
     * @param bool $reset Reset entities current data
346
     * @return bool
347
     */
348 14
    public function sync(Entity $entity, $reset = false)
349
    {
350 14
        $this->map($entity, true);
351
352
        /** @var EntityFetcher $fetcher */
353 10
        $fetcher = $this->fetch(get_class($entity));
354 10
        foreach ($entity->getPrimaryKey() as $attribute => $value) {
355 10
            $fetcher->where($attribute, $value);
356
        }
357
358 10
        $result = $this->getConnection()->query($fetcher->getQuery());
359 7
        if ($originalData = $result->fetch(\PDO::FETCH_ASSOC)) {
360 5
            $entity->setOriginalData($originalData);
361 5
            if ($reset) {
362 2
                $entity->reset();
363
            }
364 5
            return true;
365
        }
366 2
        return false;
367
    }
368
369
    /**
370
     * Insert $entity in database
371
     *
372
     * Returns boolean if it is not auto incremented or the value of auto incremented column otherwise.
373
     *
374
     * @param Entity $entity
375
     * @param bool $useAutoIncrement
376
     * @return bool
377
     * @internal
378
     */
379 12
    public function insert(Entity $entity, $useAutoIncrement = true)
380
    {
381 12
        if (isset($this->bulkInserts[get_class($entity)])) {
382
            $this->bulkInserts[get_class($entity)]->add($entity);
383
            return true;
384
        }
385
386
        return $useAutoIncrement && $entity::isAutoIncremented() ?
387
            $this->getDbal()->insertAndSyncWithAutoInc($entity) :
388
            $this->getDbal()->insertAndSync($entity);
389
    }
390
391 6
    /**
392
     * Force $class to use bulk insert.
393 6
     *
394
     * At the end you should call finish bulk insert otherwise you may loose data.
395
     *
396
     * @param string $class
397
     * @param int $limit Maximum number of rows per insert
398
     * @return BulkInsert
399
     */
400
    public function useBulkInserts($class, $limit = 20)
401
    {
402
        if (!isset($this->bulkInserts[$class])) {
403
            $this->bulkInserts[$class] = new BulkInsert($this->getDbal(), $class, $limit);
404 6
        }
405
        return $this->bulkInserts[$class];
406 6
    }
407 4
408 4
    /**
409
     * Finish the bulk insert for $class.
410
     *
411
     * Returns an array of entities added.
412
     *
413
     * @param $class
414
     * @return Entity[]
415
     */
416
    public function finishBulkInserts($class)
417
    {
418
        $bulkInsert = $this->bulkInserts[$class];
419
        unset($this->bulkInserts[$class]);
420
        return $bulkInsert->finish();
421
    }
422
423
    /**
424
     * Update $entity in database
425
     *
426 41
     * @param Entity $entity
427
     * @return bool
428 41
     * @internal
429 41
     */
430
    public function update(Entity $entity)
431 35
    {
432 35
        return $this->getDbal()->update($entity);
433
    }
434
435 35
    /**
436
     * Delete $entity from database
437
     *
438
     * This method does not delete from the map - you can still receive the entity via fetch.
439
     *
440
     * @param Entity $entity
441
     * @return bool
442
     */
443
    public function delete(Entity $entity)
444
    {
445
        $this->getDbal()->delete($entity);
446
        $entity->setOriginalData([]);
447
        return true;
448
    }
449
450
    /**
451
     * Map $entity in the entity map
452
     *
453 67
     * Returns the given entity or an entity that previously got mapped. This is useful to work in every function with
454
     * the same object.
455 67
     *
456 67
     * ```php?start_inline=true
457 1
     * $user = $enitityManager->map(new User(['id' => 42]));
458
     * ```
459
     *
460 66
     * @param Entity $entity
461 52
     * @param bool $update Update the entity map
462
     * @param string $class Overwrite the class
463
     * @return Entity
464 15
     */
465 13
    public function map(Entity $entity, $update = false, $class = null)
466
    {
467
        $class = $class ?: get_class($entity);
468 15
        $key   = static::buildChecksum($entity->getPrimaryKey());
469 15
470 1
        if ($update || !isset($this->map[$class][$key])) {
471 1
            $this->map[$class][$key] = $entity;
472
        }
473
474
        return $this->map[$class][$key];
475 14
    }
476
477 14
    /**
478 13
     * Fetch one or more entities
479
     *
480
     * With $primaryKey it tries to find this primary key in the entity map (carefully: mostly the database returns a
481 1
     * string and we do not convert them). If there is no entity in the entity map it tries to fetch the entity from
482 1
     * the database. The return value is then null (not found) or the entity.
483 1
     *
484
     * Without $primaryKey it creates an entityFetcher and returns this.
485
     *
486 1
     * @param string $class The entity class you want to fetch
487
     * @param mixed $primaryKey The primary key of the entity you want to fetch
488
     * @return Entity|EntityFetcher
489
     * @throws IncompletePrimaryKey
490
     * @throws NoEntity
491
     */
492
    public function fetch($class, $primaryKey = null)
493
    {
494
        $reflection = new \ReflectionClass($class);
495
        if (!$reflection->isSubclassOf(Entity::class)) {
496
            throw new NoEntity($class . ' is not a subclass of Entity');
497
        }
498
        /** @var string|Entity $class */
499
500
        if ($primaryKey === null) {
501
            return new EntityFetcher($this, $class);
502
        }
503
504
        $primaryKey = $this::buildPrimaryKey($class, (array)$primaryKey);
505
        $checksum = $this::buildChecksum($primaryKey);
506
507
        if (isset($this->map[$class][$checksum])) {
508
            return $this->map[$class][$checksum];
509
        }
510
511
        $fetcher = new EntityFetcher($this, $class);
512
        foreach ($primaryKey as $attribute => $value) {
513
            $fetcher->where($attribute, $value);
514
        }
515
516
        return $fetcher->one();
517
    }
518
519 2
    /**
520
     * Returns $value formatted to use in a sql statement.
521 2
     *
522 2
     * @param  mixed $value The variable that should be returned in SQL syntax
523
     * @return string
524 2
     * @codeCoverageIgnore This is just a proxy
525
     */
526
    public function escapeValue($value)
527
    {
528
        return $this->getDbal()->escapeValue($value);
529
    }
530
531
    /**
532
     * Returns $identifier quoted for use in a sql statement
533
     *
534
     * @param string $identifier Identifier to quote
535
     * @return string
536
     * @codeCoverageIgnore This is just a proxy
537
     */
538
    public function escapeIdentifier($identifier)
539
    {
540
        return $this->getDbal()->escapeIdentifier($identifier);
541
    }
542
543
    /**
544
     * Returns an array of columns from $table.
545
     *
546
     * @param string $table
547
     * @return Column[]|Table
548
     */
549
    public function describe($table)
550
    {
551
        if (!isset($this->descriptions[$table])) {
552
            $this->descriptions[$table] = $this->getDbal()->describe($table);
553
        }
554
        return $this->descriptions[$table];
555
    }
556
557
    /**
558
     * Build a checksum from $primaryKey
559
     *
560
     * @param array $primaryKey
561
     * @return string
562
     */
563
    protected static function buildChecksum(array $primaryKey)
564
    {
565
        return md5(serialize($primaryKey));
566
    }
567
568
    /**
569
     * Builds the primary key with column names as keys
570
     *
571
     * @param string|Entity $class
572
     * @param array $primaryKey
573
     * @return array
574
     * @throws IncompletePrimaryKey
575
     */
576
    protected static function buildPrimaryKey($class, array $primaryKey)
577
    {
578
        $primaryKeyVars = $class::getPrimaryKeyVars();
579
        if (count($primaryKeyVars) !== count($primaryKey)) {
580
            throw new IncompletePrimaryKey(
581
                'Primary key consist of [' . implode(',', $primaryKeyVars) . '] only ' . count($primaryKey) . ' given'
582
            );
583
        }
584
585
        return array_combine($primaryKeyVars, $primaryKey);
1 ignored issue
show
Bug Best Practice introduced by
The expression return array_combine($primaryKeyVars, $primaryKey) could also return false which is incompatible with the documented return type array. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
586
    }
587
}
588