Completed
Pull Request — master (#49)
by Thomas
44:38 queued 08:11
created

EntityManager::finishBulkInserts()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 4
cts 4
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
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] */
76
    protected static $emMapping = [
77
        'byClass'     => [],
78
        'byNameSpace' => [],
79
        'byParent'    => [],
80
        'last'        => null,
81
    ];
82
83
    /**
84
     * Constructor
85
     *
86
     * @param array $options Options for the new EntityManager
87
     * @throws InvalidConfiguration
88
     */
89 770
    public function __construct($options = [])
90
    {
91 770
        foreach ($options as $option => $value) {
92 11
            $this->setOption($option, $value);
93
        }
94
95 770
        self::$emMapping['last'] = $this;
96 770
    }
97
98
    /**
99
     * Get an instance of the EntityManager.
100
     *
101
     * If no class is given it gets $class from backtrace.
102
     *
103
     * It first gets tries the EntityManager for the Namespace of $class, then for the parents of $class. If no
104
     * EntityManager is found it returns the last created EntityManager (null if no EntityManager got created).
105
     *
106
     * @param string $class
107
     * @return EntityManager
108
     */
109 322
    public static function getInstance($class = null)
110
    {
111 322
        if (empty($class)) {
112 42
            $trace = debug_backtrace();
113 42
            if (empty($trace[1]['class'])) {
114 1
                return self::$emMapping['last'];
115
            }
116 41
            $class = $trace[1]['class'];
117
        }
118
119 321
        if (!isset(self::$emMapping['byClass'][$class])) {
120 321
            if (!($em = self::getInstanceByParent($class)) && !($em = self::getInstanceByNameSpace($class))) {
121 319
                return self::$emMapping['last'];
122
            }
123
124 2
            self::$emMapping['byClass'][$class] = $em;
125
        }
126
127 2
        return self::$emMapping['byClass'][$class];
128
    }
129
130
    /**
131
     * Get the instance by NameSpace mapping
132
     *
133
     * @param $class
134
     * @return EntityManager
135
     */
136 320
    private static function getInstanceByNameSpace($class)
137
    {
138 320
        foreach (self::$emMapping['byNameSpace'] as $nameSpace => $em) {
139 2
            if (strpos($class, $nameSpace) === 0) {
140 2
                return $em;
141
            }
142
        }
143
144 319
        return null;
145
    }
146
147
    /**
148
     * Get the instance by Parent class mapping
149
     *
150
     * @param $class
151
     * @return EntityManager
152
     */
153 321
    private static function getInstanceByParent($class)
154
    {
155
        // we don't need a reflection when we don't have mapping byParent
156 321
        if (empty(self::$emMapping['byParent'])) {
157 319
            return null;
158
        }
159
160 2
        $reflection = new \ReflectionClass($class);
161 2
        foreach (self::$emMapping['byParent'] as $parentClass => $em) {
162 2
            if ($reflection->isSubclassOf($parentClass)) {
163 2
                return $em;
164
            }
165
        }
166
167 1
        return null;
168
    }
169
170
    /**
171
     * Define $this EntityManager as the default EntityManager for $nameSpace
172
     *
173
     * @param $nameSpace
174
     * @return self
175
     */
176 2
    public function defineForNamespace($nameSpace)
177
    {
178 2
        self::$emMapping['byNameSpace'][$nameSpace] = $this;
179 2
        return $this;
180
    }
181
182
    /**
183
     * Define $this EntityManager as the default EntityManager for subClasses of $class
184
     *
185
     * @param $class
186
     * @return self
187
     */
188 2
    public function defineForParent($class)
189
    {
190 2
        self::$emMapping['byParent'][$class] = $this;
191 2
        return $this;
192
    }
193
194
    /**
195
     * Set $option to $value
196
     *
197
     * @param string $option One of OPT_* constants
198
     * @param mixed  $value
199
     * @return self
200
     */
201 15
    public function setOption($option, $value)
202
    {
203
        switch ($option) {
204 15
            case self::OPT_CONNECTION:
205 1
                $this->setConnection($value);
206 1
                break;
207
208 14
            case self::OPT_SQLITE_BOOLEAN_TRUE:
209 13
            case self::OPT_MYSQL_BOOLEAN_TRUE:
210 12
            case self::OPT_PGSQL_BOOLEAN_TRUE:
211 3
                $option = self::OPT_BOOLEAN_TRUE;
212 3
                break;
213
214 11
            case self::OPT_SQLITE_BOOLEAN_FALSE:
215 10
            case self::OPT_MYSQL_BOOLEAN_FALSE:
216 9
            case self::OPT_PGSQL_BOOLEAN_FALSE:
217 3
                $option = self::OPT_BOOLEAN_FALSE;
218 3
                break;
219
        }
220
221 15
        $this->options[$option] = $value;
222 15
        return $this;
223
    }
224
225
    /**
226
     * Get $option
227
     *
228
     * @param $option
229
     * @return mixed
230
     */
231 10
    public function getOption($option)
232
    {
233 10
        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
     *
244
     * @param mixed $connection A configuration for (or a) PDO instance
245
     * @throws InvalidConfiguration
246
     */
247 36
    public function setConnection($connection)
248
    {
249 36
        if (is_callable($connection) || $connection instanceof DbConfig) {
250 6
            $this->connection = $connection;
251
        } else {
252 30
            if ($connection instanceof \PDO) {
253 27
                $connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
254 27
                $this->connection = $connection;
255 3
            } elseif (is_array($connection)) {
256 2
                $dbConfigReflection = new \ReflectionClass(DbConfig::class);
257 2
                $this->connection   = $dbConfigReflection->newInstanceArgs($connection);
258
            } else {
259 1
                throw new InvalidConfiguration(
260 1
                    'Connection must be callable, DbConfig, PDO or an array of parameters for DbConfig::__constructor'
261
                );
262
            }
263
        }
264 35
    }
265
266
    /**
267
     * Get the pdo connection.
268
     *
269
     * @return \PDO
270
     * @throws NoConnection
271
     */
272 20
    public function getConnection()
273
    {
274 20
        if (!$this->connection) {
275 1
            throw new NoConnection('No database connection');
276
        }
277
278 19
        if (!$this->connection instanceof \PDO) {
279 7
            if ($this->connection instanceof DbConfig) {
280
                /** @var DbConfig $dbConfig */
281 4
                $dbConfig         = $this->connection;
282 4
                $this->connection = new \PDO(
283 4
                    $dbConfig->getDsn(),
284 4
                    $dbConfig->user,
285 4
                    $dbConfig->pass,
286 4
                    $dbConfig->attributes
287
                );
288
            } else {
289 3
                $pdo = call_user_func($this->connection);
290 3
                if (!$pdo instanceof \PDO) {
291 1
                    throw new NoConnection('Getter does not return PDO instance');
292
                }
293 2
                $this->connection = $pdo;
294
            }
295 6
            $this->connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
296
        }
297
298 18
        return $this->connection;
299
    }
300
301
    /**
302
     * Get the Datbase Abstraction Layer
303
     *
304
     * @return Dbal
305
     */
306 18
    public function getDbal()
307
    {
308 18
        if (!$this->dbal) {
309 18
            $connectionType = $this->getConnection()->getAttribute(\PDO::ATTR_DRIVER_NAME);
310 18
            $options        = &$this->options;
311 18
            $dbalClass      = isset($options[self::OPT_DBAL_CLASS]) ?
312 18
                $options[self::OPT_DBAL_CLASS] : __NAMESPACE__ . '\\Dbal\\' . ucfirst($connectionType);
313
314 18
            if (!class_exists($dbalClass)) {
315 2
                $dbalClass = Other::class;
316
            }
317
318 18
            $this->dbal = new $dbalClass($this, $options);
319
        }
320
321 18
        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
     * @throws IncompletePrimaryKey
348
     * @throws InvalidConfiguration
349
     * @throws NoConnection
350
     * @throws NoEntity
351
     */
352 14
    public function sync(Entity $entity, $reset = false)
353
    {
354 14
        $this->map($entity, true);
355
356
        /** @var EntityFetcher $fetcher */
357 10
        $fetcher = $this->fetch(get_class($entity));
358 10
        foreach ($entity->getPrimaryKey() as $attribute => $value) {
359 10
            $fetcher->where($attribute, $value);
360
        }
361
362 10
        $result = $this->getConnection()->query($fetcher->getQuery());
363 7
        if ($originalData = $result->fetch(\PDO::FETCH_ASSOC)) {
364 5
            $entity->setOriginalData($originalData);
365 5
            if ($reset) {
366 2
                $entity->reset();
367
            }
368 5
            return true;
369
        }
370 2
        return false;
371
    }
372
373
    /**
374
     * Insert $entity in database
375
     *
376
     * Returns boolean if it is not auto incremented or the value of auto incremented column otherwise.
377
     *
378
     * @param Entity $entity
379
     * @param bool   $useAutoIncrement
380
     * @return bool
381
     * @internal
382
     */
383 15
    public function insert(Entity $entity, $useAutoIncrement = true)
384
    {
385 15
        if (isset($this->bulkInserts[get_class($entity)])) {
386 1
            $this->bulkInserts[get_class($entity)]->add($entity);
387 1
            return true;
388
        }
389
390 14
        return $this->getDbal()->insert($entity, $useAutoIncrement);
391
    }
392
393
    /**
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 2
    public function useBulkInserts($class, $limit = 20)
403
    {
404 2
        if (!isset($this->bulkInserts[$class])) {
405 1
            $this->bulkInserts[$class] = new BulkInsert($this->getDbal(), $class, $limit);
406
        }
407 2
        return $this->bulkInserts[$class];
408
    }
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 2
    public function finishBulkInserts($class)
419
    {
420 2
        $bulkInsert = $this->bulkInserts[$class];
421 2
        unset($this->bulkInserts[$class]);
422 2
        return $bulkInsert->finish();
423
    }
424
425
    /**
426
     * Update $entity in database
427
     *
428
     * @param Entity $entity
429
     * @return bool
430
     * @internal
431
     */
432 6
    public function update(Entity $entity)
433
    {
434 6
        return $this->getDbal()->update($entity);
435
    }
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 6
    public function delete(Entity $entity)
446
    {
447 6
        $this->getDbal()->delete($entity);
448 4
        $entity->setOriginalData([]);
449 4
        return true;
450
    }
451
452
    /**
453
     * Map $entity in the entity map
454
     *
455
     * Returns the given entity or an entity that previously got mapped. This is useful to work in every function with
456
     * the same object.
457
     *
458
     * ```php?start_inline=true
459
     * $user = $enitityManager->map(new User(['id' => 42]));
460
     * ```
461
     *
462
     * @param Entity $entity
463
     * @param bool   $update Update the entity map
464
     * @param string $class  Overwrite the class
465
     * @return Entity
466
     */
467 41
    public function map(Entity $entity, $update = false, $class = null)
468
    {
469 41
        $class = $class ?: get_class($entity);
470 41
        $key   = md5(serialize($entity->getPrimaryKey()));
471
472 35
        if ($update || !isset($this->map[$class][$key])) {
473 35
            $this->map[$class][$key] = $entity;
474
        }
475
476 35
        return $this->map[$class][$key];
477
    }
478
479
    /**
480
     * Fetch one or more entities
481
     *
482
     * With $primaryKey it tries to find this primary key in the entity map (carefully: mostly the database returns a
483
     * 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
     * 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 67
    public function fetch($class, $primaryKey = null)
495
    {
496 67
        $reflection = new \ReflectionClass($class);
497 67
        if (!$reflection->isSubclassOf(Entity::class)) {
498 1
            throw new NoEntity($class . ' is not a subclass of Entity');
499
        }
500
501 66
        if ($primaryKey === null) {
502 52
            return new EntityFetcher($this, $class);
503
        }
504
505 15
        if (!is_array($primaryKey)) {
506 13
            $primaryKey = [ $primaryKey ];
507
        }
508
509 15
        $primaryKeyVars = $class::getPrimaryKeyVars();
510 15
        if (count($primaryKeyVars) !== count($primaryKey)) {
511 1
            throw new IncompletePrimaryKey(
512 1
                'Primary key consist of [' . implode(',', $primaryKeyVars) . '] only ' . count($primaryKey) . ' given'
513
            );
514
        }
515
516 14
        $primaryKey = array_combine($primaryKeyVars, $primaryKey);
517
518 14
        if (isset($this->map[$class][md5(serialize($primaryKey))])) {
519 13
            return $this->map[$class][md5(serialize($primaryKey))];
520
        }
521
522 1
        $fetcher = new EntityFetcher($this, $class);
523 1
        foreach ($primaryKey as $attribute => $value) {
524 1
            $fetcher->where($attribute, $value);
525
        }
526
527 1
        return $fetcher->one();
528
    }
529
530
    /**
531
     * Returns $value formatted to use in a sql statement.
532
     *
533
     * @param  mixed $value The variable that should be returned in SQL syntax
534
     * @return string
535
     * @codeCoverageIgnore This is just a proxy
536
     */
537
    public function escapeValue($value)
538
    {
539
        return $this->getDbal()->escapeValue($value);
540
    }
541
542
    /**
543
     * Returns $identifier quoted for use in a sql statement
544
     *
545
     * @param string $identifier Identifier to quote
546
     * @return string
547
     * @codeCoverageIgnore This is just a proxy
548
     */
549
    public function escapeIdentifier($identifier)
550
    {
551
        return $this->getDbal()->escapeIdentifier($identifier);
552
    }
553
554
    /**
555
     * Returns an array of columns from $table.
556
     *
557
     * @param string $table
558
     * @return Column[]|Table
559
     */
560 2
    public function describe($table)
561
    {
562 2
        if (!isset($this->descriptions[$table])) {
563 2
            $this->descriptions[$table] = $this->getDbal()->describe($table);
564
        }
565 2
        return $this->descriptions[$table];
566
    }
567
}
568