Passed
Pull Request — master (#41)
by Thomas
03:10
created

EntityManager::getNamer()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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