Passed
Branch feature-validator (25cef9)
by Thomas
03:18
created

EntityManager::setOption()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

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