Passed
Pull Request — master (#49)
by Thomas
02:17
created

EntityManager::update()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
ccs 2
cts 2
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] */
0 ignored issues
show
Documentation Bug introduced by
The doc comment EntityManager[string]|En...Manager[string][string] at position 1 could not be parsed: Expected ']' at position 1, but found '['.
Loading history...
76
    protected static $emMapping = [
77
        'byClass'     => [],
78
        'byNameSpace' => [],
79
        'byParent'    => [],
80
        'last'        => null,
81
    ];
82
83
    /**
84
     * Constructor
85 755
     *
86
     * @param array $options Options for the new EntityManager
87 755
     */
88 11
    public function __construct($options = [])
89
    {
90
        foreach ($options as $option => $value) {
91 755
            $this->setOption($option, $value);
92 755
        }
93
94
        self::$emMapping['last'] = $this;
95
    }
96
97
    /**
98
     * Get an instance of the EntityManager.
99
     *
100
     * If no class is given it gets $class from backtrace.
101
     *
102
     * It first gets tries the EntityManager for the Namespace of $class, then for the parents of $class. If no
103
     * EntityManager is found it returns the last created EntityManager (null if no EntityManager got created).
104
     *
105 312
     * @param string $class
106
     * @return EntityManager
107 312
     */
108 42
    public static function getInstance($class = null)
109 42
    {
110 1
        if (empty($class)) {
111
            $trace = debug_backtrace();
112 41
            if (empty($trace[1]['class'])) {
113
                return self::$emMapping['last'];
114
            }
115 311
            $class = $trace[1]['class'];
116 311
        }
117 309
118
        if (!isset(self::$emMapping['byClass'][$class])) {
119
            if (!($em = self::getInstanceByParent($class)) && !($em = self::getInstanceByNameSpace($class))) {
120 2
                return self::$emMapping['last'];
121
            }
122
123 2
            self::$emMapping['byClass'][$class] = $em;
124
        }
125
126
        return self::$emMapping['byClass'][$class];
127
    }
128
129
    /**
130
     * Get the instance by NameSpace mapping
131
     *
132 310
     * @param $class
133
     * @return EntityManager
134 310
     */
135 2
    private static function getInstanceByNameSpace($class)
136 2
    {
137
        foreach (self::$emMapping['byNameSpace'] as $nameSpace => $em) {
138
            if (strpos($class, $nameSpace) === 0) {
139
                return $em;
140 309
            }
141
        }
142
143
        return null;
144
    }
145
146
    /**
147
     * Get the instance by Parent class mapping
148
     *
149 311
     * @param $class
150
     * @return EntityManager
151
     */
152 311
    private static function getInstanceByParent($class)
153 309
    {
154
        // we don't need a reflection when we don't have mapping byParent
155
        if (empty(self::$emMapping['byParent'])) {
156 2
            return null;
157 2
        }
158 2
159 2
        /** @noinspection PhpUnhandledExceptionInspection */
160
        $reflection = new \ReflectionClass($class);
161
        foreach (self::$emMapping['byParent'] as $parentClass => $em) {
162
            if ($reflection->isSubclassOf($parentClass)) {
163 1
                return $em;
164
            }
165
        }
166
167
        return null;
168
    }
169
170
    /**
171
     * Define $this EntityManager as the default EntityManager for $nameSpace
172 2
     *
173
     * @param $nameSpace
174 2
     * @return self
175 2
     */
176
    public function defineForNamespace($nameSpace)
177
    {
178
        self::$emMapping['byNameSpace'][$nameSpace] = $this;
179
        return $this;
180
    }
181
182
    /**
183
     * Define $this EntityManager as the default EntityManager for subClasses of $class
184 2
     *
185
     * @param $class
186 2
     * @return self
187 2
     */
188
    public function defineForParent($class)
189
    {
190
        self::$emMapping['byParent'][$class] = $this;
191
        return $this;
192
    }
193
194
    /**
195
     * Set $option to $value
196
     *
197 15
     * @param string $option One of OPT_* constants
198
     * @param mixed $value
199
     * @return self
200 15
     */
201 1
    public function setOption($option, $value)
202 1
    {
203
        switch ($option) {
204 14
            case self::OPT_CONNECTION:
205 13
                $this->setConnection($value);
206 12
                break;
207 3
208 3
            case self::OPT_SQLITE_BOOLEAN_TRUE:
0 ignored issues
show
introduced by
The constant ORM\EntityManager::OPT_SQLITE_BOOLEAN_TRUE has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

208
            case /** @scrutinizer ignore-deprecated */ self::OPT_SQLITE_BOOLEAN_TRUE:
Loading history...
209
            case self::OPT_MYSQL_BOOLEAN_TRUE:
0 ignored issues
show
introduced by
The constant ORM\EntityManager::OPT_MYSQL_BOOLEAN_TRUE has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

209
            case /** @scrutinizer ignore-deprecated */ self::OPT_MYSQL_BOOLEAN_TRUE:
Loading history...
210 11
            case self::OPT_PGSQL_BOOLEAN_TRUE:
0 ignored issues
show
introduced by
The constant ORM\EntityManager::OPT_PGSQL_BOOLEAN_TRUE has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

210
            case /** @scrutinizer ignore-deprecated */ self::OPT_PGSQL_BOOLEAN_TRUE:
Loading history...
211 10
                $option = self::OPT_BOOLEAN_TRUE;
212 9
                break;
213 3
214 3
            case self::OPT_SQLITE_BOOLEAN_FALSE:
0 ignored issues
show
introduced by
The constant ORM\EntityManager::OPT_SQLITE_BOOLEAN_FALSE has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

214
            case /** @scrutinizer ignore-deprecated */ self::OPT_SQLITE_BOOLEAN_FALSE:
Loading history...
215
            case self::OPT_MYSQL_BOOLEAN_FALSE:
0 ignored issues
show
introduced by
The constant ORM\EntityManager::OPT_MYSQL_BOOLEAN_FALSE has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

215
            case /** @scrutinizer ignore-deprecated */ self::OPT_MYSQL_BOOLEAN_FALSE:
Loading history...
216
            case self::OPT_PGSQL_BOOLEAN_FALSE:
0 ignored issues
show
introduced by
The constant ORM\EntityManager::OPT_PGSQL_BOOLEAN_FALSE has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

216
            case /** @scrutinizer ignore-deprecated */ self::OPT_PGSQL_BOOLEAN_FALSE:
Loading history...
217 15
                $option = self::OPT_BOOLEAN_FALSE;
218 15
                break;
219
        }
220
221
        $this->options[$option] = $value;
222
        return $this;
223
    }
224
225
    /**
226
     * Get $option
227 10
     *
228
     * @param $option
229 10
     * @return mixed
230
     */
231
    public function getOption($option)
232
    {
233
        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 36
     *
244
     * @param mixed $connection A configuration for (or a) PDO instance
245 36
     * @throws InvalidConfiguration
246 6
     */
247
    public function setConnection($connection)
248 30
    {
249 27
        if (is_callable($connection) || $connection instanceof DbConfig) {
250 27
            $this->connection = $connection;
251 3
        } else {
252 2
            if ($connection instanceof \PDO) {
253 2
                $connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
254
                $this->connection = $connection;
255 1
            } elseif (is_array($connection)) {
256 1
                $this->connection = new DbConfig(...$connection);
257
            } else {
258
                throw new InvalidConfiguration(
259
                    'Connection must be callable, DbConfig, PDO or an array of parameters for DbConfig::__constructor'
260 35
                );
261
            }
262
        }
263
    }
264
265
    /**
266
     * Get the pdo connection.
267
     *
268 20
     * @return \PDO
269
     * @throws NoConnection
270 20
     * @throws NoConnection
271 1
     */
272
    public function getConnection()
273
    {
274 19
        if (!$this->connection) {
275 7
            throw new NoConnection('No database connection');
276
        }
277 4
278 4
        if (!$this->connection instanceof \PDO) {
279 4
            if ($this->connection instanceof DbConfig) {
280 4
                /** @var DbConfig $dbConfig */
281 4
                $dbConfig         = $this->connection;
282 4
                $this->connection = new \PDO(
283
                    $dbConfig->getDsn(),
284
                    $dbConfig->user,
285 3
                    $dbConfig->pass,
286 3
                    $dbConfig->attributes
287 1
                );
288
            } else {
289 2
                $pdo = call_user_func($this->connection);
290
                if (!$pdo instanceof \PDO) {
291 6
                    throw new NoConnection('Getter does not return PDO instance');
292
                }
293
                $this->connection = $pdo;
294 18
            }
295
            $this->connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
296
        }
297
298
        return $this->connection;
299
    }
300
301
    /**
302 18
     * Get the Datbase Abstraction Layer
303
     *
304 18
     * @return Dbal
305 18
     */
306 18
    public function getDbal()
307 18
    {
308 18
        if (!$this->dbal) {
309
            $connectionType = $this->getConnection()->getAttribute(\PDO::ATTR_DRIVER_NAME);
310 18
            $options        = &$this->options;
311 2
            $dbalClass      = isset($options[self::OPT_DBAL_CLASS]) ?
312
                $options[self::OPT_DBAL_CLASS] : __NAMESPACE__ . '\\Dbal\\' . ucfirst($connectionType);
313
314 18
            if (!class_exists($dbalClass)) {
315
                $dbalClass = Other::class;
316
            }
317 18
318
            $this->dbal = new $dbalClass($this, $options);
319
        }
320
321
        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
     */
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
        if (isset($this->bulkInserts[get_class($entity)])) {
382
            $this->bulkInserts[get_class($entity)]->add($entity);
383
            return true;
384
        }
385
386
        return $useAutoIncrement && $entity::isAutoIncremented() ?
387
            $this->getDbal()->insertAndSyncWithAutoInc($entity) :
388
            $this->getDbal()->insertAndSync($entity);
389
    }
390
391 6
    /**
392
     * Force $class to use bulk insert.
393 6
     *
394
     * At the end you should call finish bulk insert otherwise you may loose data.
395
     *
396
     * @param string $class
397
     * @param int $limit Maximum number of rows per insert
398
     * @return BulkInsert
399
     */
400
    public function useBulkInserts($class, $limit = 20)
401
    {
402
        if (!isset($this->bulkInserts[$class])) {
403
            $this->bulkInserts[$class] = new BulkInsert($this->getDbal(), $class, $limit);
404 6
        }
405
        return $this->bulkInserts[$class];
406 6
    }
407 4
408 4
    /**
409
     * Finish the bulk insert for $class.
410
     *
411
     * Returns an array of entities added.
412
     *
413
     * @param $class
414
     * @return Entity[]
415
     */
416
    public function finishBulkInserts($class)
417
    {
418
        $bulkInsert = $this->bulkInserts[$class];
419
        unset($this->bulkInserts[$class]);
420
        return $bulkInsert->finish();
421
    }
422
423
    /**
424
     * Update $entity in database
425
     *
426 41
     * @param Entity $entity
427
     * @return bool
428 41
     * @internal
429 41
     */
430
    public function update(Entity $entity)
431 35
    {
432 35
        return $this->getDbal()->update($entity);
433
    }
434
435 35
    /**
436
     * Delete $entity from database
437
     *
438
     * This method does not delete from the map - you can still receive the entity via fetch.
439
     *
440
     * @param Entity $entity
441
     * @return bool
442
     */
443
    public function delete(Entity $entity)
444
    {
445
        $this->getDbal()->delete($entity);
446
        $entity->setOriginalData([]);
447
        return true;
448
    }
449
450
    /**
451
     * Map $entity in the entity map
452
     *
453 67
     * Returns the given entity or an entity that previously got mapped. This is useful to work in every function with
454
     * the same object.
455 67
     *
456 67
     * ```php?start_inline=true
457 1
     * $user = $enitityManager->map(new User(['id' => 42]));
458
     * ```
459
     *
460 66
     * @param Entity $entity
461 52
     * @param bool $update Update the entity map
462
     * @param string $class Overwrite the class
463
     * @return Entity
464 15
     */
465 13
    public function map(Entity $entity, $update = false, $class = null)
466
    {
467
        $class = $class ?: get_class($entity);
468 15
        $key   = md5(serialize($entity->getPrimaryKey()));
469 15
470 1
        if ($update || !isset($this->map[$class][$key])) {
471 1
            $this->map[$class][$key] = $entity;
472
        }
473
474
        return $this->map[$class][$key];
475 14
    }
476
477 14
    /**
478 13
     * Fetch one or more entities
479
     *
480
     * With $primaryKey it tries to find this primary key in the entity map (carefully: mostly the database returns a
481 1
     * string and we do not convert them). If there is no entity in the entity map it tries to fetch the entity from
482 1
     * the database. The return value is then null (not found) or the entity.
483 1
     *
484
     * Without $primaryKey it creates an entityFetcher and returns this.
485
     *
486 1
     * @param string $class The entity class you want to fetch
487
     * @param mixed $primaryKey The primary key of the entity you want to fetch
488
     * @return Entity|EntityFetcher
489
     * @throws IncompletePrimaryKey
490
     * @throws NoEntity
491
     */
492
    public function fetch($class, $primaryKey = null)
493
    {
494
        /** @noinspection PhpUnhandledExceptionInspection */
495
        $reflection = new \ReflectionClass($class);
496
        if (!$reflection->isSubclassOf(Entity::class)) {
497
            throw new NoEntity($class . ' is not a subclass of Entity');
498
        }
499
500
        if ($primaryKey === null) {
501
            return new EntityFetcher($this, $class);
502
        }
503
504
        if (!is_array($primaryKey)) {
505
            $primaryKey = [ $primaryKey ];
506
        }
507
508
        /** @noinspection PhpUndefinedMethodInspection */
509
        $primaryKeyVars = $class::getPrimaryKeyVars();
510
        if (count($primaryKeyVars) !== count($primaryKey)) {
511
            throw new IncompletePrimaryKey(
512
                'Primary key consist of [' . implode(',', $primaryKeyVars) . '] only ' . count($primaryKey) . ' given'
513
            );
514
        }
515
516
        $primaryKey = array_combine($primaryKeyVars, $primaryKey);
517
518
        if (isset($this->map[$class][md5(serialize($primaryKey))])) {
519 2
            return $this->map[$class][md5(serialize($primaryKey))];
520
        }
521 2
522 2
        $fetcher = new EntityFetcher($this, $class);
523
        foreach ($primaryKey as $attribute => $value) {
524 2
            $fetcher->where($attribute, $value);
525
        }
526
527
        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
    public function describe($table)
561
    {
562
        if (!isset($this->descriptions[$table])) {
563
            $this->descriptions[$table] = $this->getDbal()->describe($table);
564
        }
565
        return $this->descriptions[$table];
566
    }
567
}
568