Passed
Push — master ( 050c29...4fa43a )
by Thomas
58s
created

EntityManager::getInstance()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 20
ccs 11
cts 11
cp 1
rs 8.8571
c 0
b 0
f 0
cc 6
eloc 11
nc 7
nop 1
crap 6
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
    /** Mapping for EntityManager instances
71
     * @var EntityManager[string]|EntityManager[string][string] */
72
    protected static $emMapping = [
73
        'byClass'     => [],
74
        'byNameSpace' => [],
75
        'byParent'    => [],
76
        'last'        => null,
77
    ];
78
79
    /**
80
     * Constructor
81
     *
82
     * @param array $options Options for the new EntityManager
83
     * @throws InvalidConfiguration
84
     */
85 766
    public function __construct($options = [])
86
    {
87 766
        foreach ($options as $option => $value) {
88 11
            $this->setOption($option, $value);
89
        }
90
91 766
        self::$emMapping['last'] = $this;
92 766
    }
93
94
    /**
95
     * Get an instance of the EntityManager.
96
     *
97
     * If no class is given it gets $class from backtrace.
98
     *
99
     * It first gets tries the EntityManager for the Namespace of $class, then for the parents of $class. If no
100
     * EntityManager is found it returns the last created EntityManager (null if no EntityManager got created).
101
     *
102
     * @param string $class
103
     * @return EntityManager
104
     */
105 308
    public static function getInstance($class = null)
106
    {
107 308
        if (empty($class)) {
108 42
            $trace = debug_backtrace();
109 42
            if (empty($trace[1]['class'])) {
110 1
                return self::$emMapping['last'];
111
            }
112 41
            $class = $trace[1]['class'];
113
        }
114
115 307
        if (!isset(self::$emMapping['byClass'][$class])) {
116 307
            if (!($em = self::getInstanceByParent($class)) && !($em = self::getInstanceByNameSpace($class))) {
117 305
                return self::$emMapping['last'];
118
            }
119
120 2
            self::$emMapping['byClass'][$class] = $em;
121
        }
122
123 2
        return self::$emMapping['byClass'][$class];
124
    }
125
126
    /**
127
     * Get the instance by NameSpace mapping
128
     *
129
     * @param $class
130
     * @return EntityManager
131
     */
132 306
    private static function getInstanceByNameSpace($class)
133
    {
134 306
        foreach (self::$emMapping['byNameSpace'] as $nameSpace => $em) {
135 2
            if (strpos($class, $nameSpace) === 0) {
136 2
                return $em;
137
            }
138
        }
139
140 305
        return null;
141
    }
142
143
    /**
144
     * Get the instance by Parent class mapping
145
     *
146
     * @param $class
147
     * @return EntityManager
148
     */
149 307
    private static function getInstanceByParent($class)
150
    {
151
        // we don't need a reflection when we don't have mapping byParent
152 307
        if (empty(self::$emMapping['byParent'])) {
153 305
            return null;
154
        }
155
156 2
        $reflection = new \ReflectionClass($class);
157 2
        foreach (self::$emMapping['byParent'] as $parentClass => $em) {
158 2
            if ($reflection->isSubclassOf($parentClass)) {
159 2
                return $em;
160
            }
161
        }
162
163 1
        return null;
164
    }
165
166
    /**
167
     * Define $this EntityManager as the default EntityManager for $nameSpace
168
     *
169
     * @param $nameSpace
170
     * @return self
171
     */
172 2
    public function defineForNamespace($nameSpace)
173
    {
174 2
        self::$emMapping['byNameSpace'][$nameSpace] = $this;
175 2
        return $this;
176
    }
177
178
    /**
179
     * Define $this EntityManager as the default EntityManager for subClasses of $class
180
     *
181
     * @param $class
182
     * @return self
183
     */
184 2
    public function defineForParent($class)
185
    {
186 2
        self::$emMapping['byParent'][$class] = $this;
187 2
        return $this;
188
    }
189
190
    /**
191
     * Set $option to $value
192
     *
193
     * @param string $option One of OPT_* constants
194
     * @param mixed  $value
195
     * @return self
196
     */
197 15
    public function setOption($option, $value)
198
    {
199
        switch ($option) {
200 15
            case self::OPT_CONNECTION:
201 1
                $this->setConnection($value);
202 1
                break;
203
204 14
            case self::OPT_SQLITE_BOOLEAN_TRUE:
205 13
            case self::OPT_MYSQL_BOOLEAN_TRUE:
206 12
            case self::OPT_PGSQL_BOOLEAN_TRUE:
207 3
                $option = self::OPT_BOOLEAN_TRUE;
208 3
                break;
209
210 11
            case self::OPT_SQLITE_BOOLEAN_FALSE:
211 10
            case self::OPT_MYSQL_BOOLEAN_FALSE:
212 9
            case self::OPT_PGSQL_BOOLEAN_FALSE:
213 3
                $option = self::OPT_BOOLEAN_FALSE;
214 3
                break;
215
        }
216
217 15
        $this->options[$option] = $value;
218 15
        return $this;
219
    }
220
221
    /**
222
     * Get $option
223
     *
224
     * @param $option
225
     * @return mixed
226
     */
227 10
    public function getOption($option)
228
    {
229 10
        return isset($this->options[$option]) ? $this->options[$option] : null;
230
    }
231
232
    /**
233
     * Add connection after instantiation
234
     *
235
     * The connection can be an array of parameters for DbConfig::__construct(), a callable function that returns a PDO
236
     * instance, an instance of DbConfig or a PDO instance itself.
237
     *
238
     * When it is not a PDO instance the connection get established on first use.
239
     *
240
     * @param mixed $connection A configuration for (or a) PDO instance
241
     * @throws InvalidConfiguration
242
     */
243 36
    public function setConnection($connection)
244
    {
245 36
        if (is_callable($connection) || $connection instanceof DbConfig) {
246 6
            $this->connection = $connection;
247
        } else {
248 30
            if ($connection instanceof \PDO) {
249 27
                $connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
250 27
                $this->connection = $connection;
251 3
            } elseif (is_array($connection)) {
252 2
                $dbConfigReflection = new \ReflectionClass(DbConfig::class);
253 2
                $this->connection   = $dbConfigReflection->newInstanceArgs($connection);
254
            } else {
255 1
                throw new InvalidConfiguration(
256 1
                    'Connection must be callable, DbConfig, PDO or an array of parameters for DbConfig::__constructor'
257
                );
258
            }
259
        }
260 35
    }
261
262
    /**
263
     * Get the pdo connection.
264
     *
265
     * @return \PDO
266
     * @throws NoConnection
267
     */
268 20
    public function getConnection()
269
    {
270 20
        if (!$this->connection) {
271 1
            throw new NoConnection('No database connection');
272
        }
273
274 19
        if (!$this->connection instanceof \PDO) {
275 7
            if ($this->connection instanceof DbConfig) {
276
                /** @var DbConfig $dbConfig */
277 4
                $dbConfig         = $this->connection;
278 4
                $this->connection = new \PDO(
279 4
                    $dbConfig->getDsn(),
280 4
                    $dbConfig->user,
281 4
                    $dbConfig->pass,
282 4
                    $dbConfig->attributes
283
                );
284
            } else {
285 3
                $pdo = call_user_func($this->connection);
286 3
                if (!$pdo instanceof \PDO) {
287 1
                    throw new NoConnection('Getter does not return PDO instance');
288
                }
289 2
                $this->connection = $pdo;
290
            }
291 6
            $this->connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
292
        }
293
294 18
        return $this->connection;
295
    }
296
297
    /**
298
     * Get the Datbase Abstraction Layer
299
     *
300
     * @return Dbal
301
     */
302 18
    public function getDbal()
303
    {
304 18
        if (!$this->dbal) {
305 18
            $connectionType = $this->getConnection()->getAttribute(\PDO::ATTR_DRIVER_NAME);
306 18
            $options        = &$this->options;
307 18
            $dbalClass      = isset($options[self::OPT_DBAL_CLASS]) ?
308 18
                $options[self::OPT_DBAL_CLASS] : __NAMESPACE__ . '\\Dbal\\' . ucfirst($connectionType);
309
310 18
            if (!class_exists($dbalClass)) {
311 2
                $dbalClass = Other::class;
312
            }
313
314 18
            $this->dbal = new $dbalClass($this, $options);
315
        }
316
317 18
        return $this->dbal;
318
    }
319
320
    /**
321
     * Get the Namer instance
322
     *
323
     * @return Namer
324
     * @codeCoverageIgnore trivial code...
325
     */
326
    public function getNamer()
327
    {
328
        if (!$this->namer) {
329
            $this->namer = new Namer($this->options);
330
        }
331
332
        return $this->namer;
333
    }
334
335
    /**
336
     * Synchronizing $entity with database
337
     *
338
     * If $reset is true it also calls reset() on $entity.
339
     *
340
     * @param Entity $entity
341
     * @param bool   $reset Reset entities current data
342
     * @return bool
343
     * @throws IncompletePrimaryKey
344
     * @throws InvalidConfiguration
345
     * @throws NoConnection
346
     * @throws NoEntity
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
        return $this->getDbal()->insert($entity, $useAutoIncrement);
382
    }
383
384
    /**
385
     * Update $entity in database
386
     *
387
     * @param Entity $entity
388
     * @return bool
389
     * @internal
390
     */
391 6
    public function update(Entity $entity)
392
    {
393 6
        return $this->getDbal()->update($entity);
394
    }
395
396
    /**
397
     * Delete $entity from database
398
     *
399
     * This method does not delete from the map - you can still receive the entity via fetch.
400
     *
401
     * @param Entity $entity
402
     * @return bool
403
     */
404 6
    public function delete(Entity $entity)
405
    {
406 6
        $this->getDbal()->delete($entity);
407 4
        $entity->setOriginalData([]);
408 4
        return true;
409
    }
410
411
    /**
412
     * Map $entity in the entity map
413
     *
414
     * Returns the given entity or an entity that previously got mapped. This is useful to work in every function with
415
     * the same object.
416
     *
417
     * ```php?start_inline=true
418
     * $user = $enitityManager->map(new User(['id' => 42]));
419
     * ```
420
     *
421
     * @param Entity $entity
422
     * @param bool   $update Update the entity map
423
     * @param string $class  Overwrite the class
424
     * @return Entity
425
     */
426 41
    public function map(Entity $entity, $update = false, $class = null)
427
    {
428 41
        $class = $class ?: get_class($entity);
429 41
        $key   = md5(serialize($entity->getPrimaryKey()));
430
431 35
        if ($update || !isset($this->map[$class][$key])) {
432 35
            $this->map[$class][$key] = $entity;
433
        }
434
435 35
        return $this->map[$class][$key];
436
    }
437
438
    /**
439
     * Fetch one or more entities
440
     *
441
     * With $primaryKey it tries to find this primary key in the entity map (carefully: mostly the database returns a
442
     * string and we do not convert them). If there is no entity in the entity map it tries to fetch the entity from
443
     * the database. The return value is then null (not found) or the entity.
444
     *
445
     * Without $primaryKey it creates an entityFetcher and returns this.
446
     *
447
     * @param string $class      The entity class you want to fetch
448
     * @param mixed  $primaryKey The primary key of the entity you want to fetch
449
     * @return Entity|EntityFetcher
450
     * @throws IncompletePrimaryKey
451
     * @throws NoEntity
452
     */
453 67
    public function fetch($class, $primaryKey = null)
454
    {
455 67
        $reflection = new \ReflectionClass($class);
456 67
        if (!$reflection->isSubclassOf(Entity::class)) {
457 1
            throw new NoEntity($class . ' is not a subclass of Entity');
458
        }
459
460 66
        if ($primaryKey === null) {
461 52
            return new EntityFetcher($this, $class);
462
        }
463
464 15
        if (!is_array($primaryKey)) {
465 13
            $primaryKey = [ $primaryKey ];
466
        }
467
468 15
        $primaryKeyVars = $class::getPrimaryKeyVars();
469 15
        if (count($primaryKeyVars) !== count($primaryKey)) {
470 1
            throw new IncompletePrimaryKey(
471 1
                'Primary key consist of [' . implode(',', $primaryKeyVars) . '] only ' . count($primaryKey) . ' given'
472
            );
473
        }
474
475 14
        $primaryKey = array_combine($primaryKeyVars, $primaryKey);
476
477 14
        if (isset($this->map[$class][md5(serialize($primaryKey))])) {
478 13
            return $this->map[$class][md5(serialize($primaryKey))];
479
        }
480
481 1
        $fetcher = new EntityFetcher($this, $class);
482 1
        foreach ($primaryKey as $attribute => $value) {
483 1
            $fetcher->where($attribute, $value);
484
        }
485
486 1
        return $fetcher->one();
487
    }
488
489
    /**
490
     * Returns $value formatted to use in a sql statement.
491
     *
492
     * @param  mixed $value The variable that should be returned in SQL syntax
493
     * @return string
494
     * @codeCoverageIgnore This is just a proxy
495
     */
496
    public function escapeValue($value)
497
    {
498
        return $this->getDbal()->escapeValue($value);
499
    }
500
501
    /**
502
     * Returns $identifier quoted for use in a sql statement
503
     *
504
     * @param string $identifier Identifier to quote
505
     * @return string
506
     * @codeCoverageIgnore This is just a proxy
507
     */
508
    public function escapeIdentifier($identifier)
509
    {
510
        return $this->getDbal()->escapeIdentifier($identifier);
511
    }
512
513
    /**
514
     * Returns an array of columns from $table.
515
     *
516
     * @param string $table
517
     * @return Column[]|Table
518
     */
519 3
    public function describe($table)
520
    {
521 3
        if (!isset($this->descriptions[$table])) {
522 3
            $this->descriptions[$table] = $this->getDbal()->describe($table);
523
        }
524 2
        return $this->descriptions[$table];
525
    }
526
}
527