EntityManager   F
last analyzed

Complexity

Total Complexity 72

Size/Duplication

Total Lines 568
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 10

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 72
lcom 2
cbo 10
dl 0
loc 568
rs 2.64
c 0
b 0
f 0
ccs 141
cts 141
cp 1

25 Methods

Rating   Name   Duplication   Size   Complexity  
A useBulkInserts() 0 7 2
A update() 0 4 1
A map() 0 11 4
A __construct() 0 8 2
B getInstance() 0 20 6
A getInstanceByNameSpace() 0 10 3
A getInstanceByParent() 0 16 4
A defineForNamespace() 0 5 1
A defineForParent() 0 5 1
B setOption() 0 23 8
A getOption() 0 4 2
A setConnection() 0 17 5
A getConnection() 0 28 5
A getDbal() 0 17 4
A getNamer() 0 8 2
A sync() 0 20 4
A insert() 0 11 4
A finishBulkInserts() 0 6 1
A delete() 0 6 1
A fetch() 0 26 5
A escapeValue() 0 4 1
A escapeIdentifier() 0 4 1
A describe() 0 7 2
A buildChecksum() 0 4 1
A buildPrimaryKey() 0 11 2

How to fix   Complexity   

Complex Class

Complex classes like EntityManager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use EntityManager, and based on these observations, apply Extract Interface, too.

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
use PDO;
14
use ReflectionClass;
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_NAMING_SCHEME_ATTRIBUTE = 'namingSchemeAttribute';
30
    const OPT_QUOTING_CHARACTER = 'quotingChar';
31
    const OPT_IDENTIFIER_DIVIDER = 'identifierDivider';
32
    const OPT_BOOLEAN_TRUE = 'true';
33
    const OPT_BOOLEAN_FALSE = 'false';
34
    const OPT_DBAL_CLASS = 'dbalClass';
35
36
    /** @deprecated */
37
    const OPT_MYSQL_BOOLEAN_TRUE = 'mysqlTrue';
38
    /** @deprecated */
39
    const OPT_MYSQL_BOOLEAN_FALSE = 'mysqlFalse';
40
    /** @deprecated */
41
    const OPT_SQLITE_BOOLEAN_TRUE = 'sqliteTrue';
42
    /** @deprecated */
43
    const OPT_SQLITE_BOOLEAN_FALSE = 'sqliteFalse';
44
    /** @deprecated */
45
    const OPT_PGSQL_BOOLEAN_TRUE = 'pgsqlTrue';
46
    /** @deprecated */
47
    const OPT_PGSQL_BOOLEAN_FALSE = 'pgsqlFalse';
48
49
    /** Connection to database
50
     * @var PDO|callable|DbConfig */
51
    protected $connection;
52
53
    /** The Database Abstraction Layer
54
     * @var Dbal */
55
    protected $dbal;
56
57
    /** The Namer instance
58
     * @var Namer */
59
    protected $namer;
60
61
    /** The Entity map
62
     * @var Entity[][] */
63
    protected $map = [];
64
65
    /** The options set for this instance
66
     * @var array */
67
    protected $options = [];
68
69
    /** Already fetched column descriptions
70
     * @var Table[]|Column[][] */
71
    protected $descriptions = [];
72
73
    /** Classes forcing bulk insert
74
     * @var BulkInsert[] */
75
    protected $bulkInserts = [];
76
77
    /** Mapping for EntityManager instances
78
     * @var EntityManager[string]|EntityManager[string][string] */
79
    protected static $emMapping = [
80
        'byClass'     => [],
81
        'byNameSpace' => [],
82
        'byParent'    => [],
83
        'last'        => null,
84
    ];
85 755
86
    /**
87 755
     * Constructor
88 11
     *
89
     * @param array $options Options for the new EntityManager
90
     */
91 755
    public function __construct($options = [])
92 755
    {
93
        foreach ($options as $option => $value) {
94
            $this->setOption($option, $value);
95
        }
96
97
        self::$emMapping['last'] = $this;
98
    }
99
100
    /**
101
     * Get an instance of the EntityManager.
102
     *
103
     * If no class is given it gets $class from backtrace.
104
     *
105 312
     * It first gets tries the EntityManager for the Namespace of $class, then for the parents of $class. If no
106
     * EntityManager is found it returns the last created EntityManager (null if no EntityManager got created).
107 312
     *
108 42
     * @param string $class
109 42
     * @return EntityManager
110 1
     */
111
    public static function getInstance($class = null)
112 41
    {
113
        if (empty($class)) {
114
            $trace = debug_backtrace();
115 311
            if (empty($trace[1]['class'])) {
116 311
                return self::$emMapping['last'];
117 309
            }
118
            $class = $trace[1]['class'];
119
        }
120 2
121
        if (!isset(self::$emMapping['byClass'][$class])) {
122
            if (!($em = self::getInstanceByParent($class)) && !($em = self::getInstanceByNameSpace($class))) {
123 2
                return self::$emMapping['last'];
124
            }
125
126
            self::$emMapping['byClass'][$class] = $em;
127
        }
128
129
        return self::$emMapping['byClass'][$class];
130
    }
131
132 310
    /**
133
     * Get the instance by NameSpace mapping
134 310
     *
135 2
     * @param $class
136 2
     * @return EntityManager
137
     */
138
    private static function getInstanceByNameSpace($class)
139
    {
140 309
        foreach (self::$emMapping['byNameSpace'] as $nameSpace => $em) {
141
            if (strpos($class, $nameSpace) === 0) {
142
                return $em;
143
            }
144
        }
145
146
        return null;
147
    }
148
149 311
    /**
150
     * Get the instance by Parent class mapping
151
     *
152 311
     * @param $class
153 309
     * @return EntityManager
154
     */
155
    private static function getInstanceByParent($class)
156 2
    {
157 2
        // we don't need a reflection when we don't have mapping byParent
158 2
        if (empty(self::$emMapping['byParent'])) {
159 2
            return null;
160
        }
161
162
        $reflection = new ReflectionClass($class);
163 1
        foreach (self::$emMapping['byParent'] as $parentClass => $em) {
164
            if ($reflection->isSubclassOf($parentClass)) {
165
                return $em;
166
            }
167
        }
168
169
        return null;
170
    }
171
172 2
    /**
173
     * Define $this EntityManager as the default EntityManager for $nameSpace
174 2
     *
175 2
     * @param $nameSpace
176
     * @return static
177
     */
178
    public function defineForNamespace($nameSpace)
179
    {
180
        self::$emMapping['byNameSpace'][$nameSpace] = $this;
181
        return $this;
182
    }
183
184 2
    /**
185
     * Define $this EntityManager as the default EntityManager for subClasses of $class
186 2
     *
187 2
     * @param $class
188
     * @return static
189
     */
190
    public function defineForParent($class)
191
    {
192
        self::$emMapping['byParent'][$class] = $this;
193
        return $this;
194
    }
195
196
    /**
197 15
     * Set $option to $value
198
     *
199
     * @param string $option One of OPT_* constants
200 15
     * @param mixed $value
201 1
     * @return static
202 1
     */
203
    public function setOption($option, $value)
204 14
    {
205 13
        switch ($option) {
206 12
            case self::OPT_CONNECTION:
207 3
                $this->setConnection($value);
208 3
                break;
209
210 11
            case self::OPT_SQLITE_BOOLEAN_TRUE:
211 10
            case self::OPT_MYSQL_BOOLEAN_TRUE:
212 9
            case self::OPT_PGSQL_BOOLEAN_TRUE:
213 3
                $option = self::OPT_BOOLEAN_TRUE;
214 3
                break;
215
216
            case self::OPT_SQLITE_BOOLEAN_FALSE:
217 15
            case self::OPT_MYSQL_BOOLEAN_FALSE:
218 15
            case self::OPT_PGSQL_BOOLEAN_FALSE:
219
                $option = self::OPT_BOOLEAN_FALSE;
220
                break;
221
        }
222
223
        $this->options[$option] = $value;
224
        return $this;
225
    }
226
227 10
    /**
228
     * Get $option
229 10
     *
230
     * @param $option
231
     * @return mixed
232
     */
233
    public function getOption($option)
234
    {
235
        return isset($this->options[$option]) ? $this->options[$option] : null;
236
    }
237
238
    /**
239
     * Add connection after instantiation
240
     *
241
     * The connection can be an array of parameters for DbConfig::__construct(), a callable function that returns a PDO
242
     * instance, an instance of DbConfig or a PDO instance itself.
243 36
     *
244
     * When it is not a PDO instance the connection get established on first use.
245 36
     *
246 6
     * @param mixed $connection A configuration for (or a) PDO instance
247
     * @throws InvalidConfiguration
248 30
     */
249 27
    public function setConnection($connection)
250 27
    {
251 3
        if (is_callable($connection) || $connection instanceof DbConfig) {
252 2
            $this->connection = $connection;
253 2
        } else {
254
            if ($connection instanceof PDO) {
255 1
                $connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
256 1
                $this->connection = $connection;
257
            } elseif (is_array($connection)) {
258
                $this->connection = new DbConfig(...$connection);
0 ignored issues
show
Bug introduced by
The call to DbConfig::__construct() misses a required argument $name.

This check looks for function calls that miss required arguments.

Loading history...
Documentation introduced by
$connection is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

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