Passed
Branch feature-dbal (014e98)
by Thomas
02:38
created

EntityManager::update()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

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 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 1
crap 1
1
<?php
2
3
namespace ORM;
4
5
use ORM\Dbal\Other;
6
use ORM\Exceptions\IncompletePrimaryKey;
7
use ORM\Exceptions\InvalidConfiguration;
8
use ORM\Exceptions\NoConnection;
9
use ORM\Exceptions\NoEntity;
10
use ORM\Exceptions\NotScalar;
11
use ORM\Exceptions\UnsupportedDriver;
12
13
/**
14
 * The EntityManager that manages the instances of Entities.
15
 *
16
 * @package ORM
17
 * @author Thomas Flori <[email protected]>
18
 */
19
class EntityManager
20
{
21
    const OPT_CONNECTION             = 'connection';
22
    const OPT_TABLE_NAME_TEMPLATE    = 'tableNameTemplate';
23
    const OPT_NAMING_SCHEME_TABLE    = 'namingSchemeTable';
24
    const OPT_NAMING_SCHEME_COLUMN   = 'namingSchemeColumn';
25
    const OPT_NAMING_SCHEME_METHODS  = 'namingSchemeMethods';
26
27
    /** @deprecated */
28
    const OPT_MYSQL_BOOLEAN_TRUE     = 'mysqlTrue';
29
    /** @deprecated */
30
    const OPT_MYSQL_BOOLEAN_FALSE    = 'mysqlFalse';
31
    /** @deprecated */
32
    const OPT_SQLITE_BOOLEAN_TRUE    = 'sqliteTrue';
33
    /** @deprecated */
34
    const OPT_SQLITE_BOOLEAN_FASLE   = 'sqliteFalse';
35
    /** @deprecated */
36
    const OPT_PGSQL_BOOLEAN_TRUE     = 'pgsqlTrue';
37
    /** @deprecated */
38
    const OPT_PGSQL_BOOLEAN_FALSE    = 'pgsqlFalse';
39
    /** @deprecated */
40
    const OPT_QUOTING_CHARACTER      = 'quotingChar';
41
    /** @deprecated */
42
    const OPT_IDENTIFIER_DIVIDER     = 'identifierDivider';
43
44
    /** Connection to database
45
     * @var \PDO|callable|DbConfig */
46
    protected $connection;
47
48
    /** The Database Abstraction Layer
49
     * @var Dbal */
50
    private $dbal;
51
52
    /** The Entity map
53
     * @var Entity[][] */
54
    protected $map = [];
55
56
    /** The options set for this instance
57
     * @var array */
58
    protected $options = [];
59
60
    /**
61
     * Constructor
62
     *
63
     * @param array $options Options for the new EntityManager
64
     * @throws InvalidConfiguration
65
     */
66 20
    public function __construct($options = [])
67
    {
68 20
        foreach ($options as $option => $value) {
69 9
            $this->setOption($option, $value);
70
        }
71 20
    }
72
73
    /**
74
     * Set $option to $value
75
     *
76
     * @param string $option One of OPT_* constants
77
     * @param mixed  $value
78
     * @return self
79
     */
80 13
    public function setOption($option, $value)
81
    {
82
        switch ($option) {
83 13
            case self::OPT_CONNECTION:
84 1
                $this->setConnection($value);
0 ignored issues
show
Documentation introduced by
$value is of type *, but the function expects a callable.

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...
85 1
                break;
86
87 12
            case self::OPT_TABLE_NAME_TEMPLATE:
88 1
                Entity::setTableNameTemplate($value);
89 1
                break;
90
91 11
            case self::OPT_NAMING_SCHEME_TABLE:
92 1
                Entity::setNamingSchemeTable($value);
93 1
                break;
94
95 10
            case self::OPT_NAMING_SCHEME_COLUMN:
96 1
                Entity::setNamingSchemeColumn($value);
97 1
                break;
98
99 9
            case self::OPT_NAMING_SCHEME_METHODS:
100 1
                Entity::setNamingSchemeMethods($value);
101 1
                break;
102
        }
103
104 13
        $this->options[$option] = $value;
105 13
        return $this;
106
    }
107
108
    /**
109
     * Get $option
110
     *
111
     * @param $option
112
     * @return mixed
113
     */
114 4
    public function getOption($option)
115
    {
116 4
        return isset($this->options[$option]) ? $this->options[$option] : null;
117
    }
118
119
    /**
120
     * Add connection after instantiation
121
     *
122
     * The connection can be an array of parameters for DbConfig::__construct(), a callable function that returns a PDO
123
     * instance, an instance of DbConfig or a PDO instance itself.
124
     *
125
     * When it is not a PDO instance the connection get established on first use.
126
     *
127
     * @param \PDO|callable|DbConfig|array $connection A configuration for (or a) PDO instance
128
     * @throws InvalidConfiguration
129
     */
130 10
    public function setConnection($connection)
131
    {
132 10
        if (is_callable($connection) || $connection instanceof DbConfig) {
133 6
            $this->connection = $connection;
134
        } else {
135 4
            if ($connection instanceof \PDO) {
136 1
                $connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
137 1
                $this->connection = $connection;
138 3
            } elseif (is_array($connection)) {
139 2
                $dbConfigReflection = new \ReflectionClass(DbConfig::class);
140 2
                $this->connection   = $dbConfigReflection->newInstanceArgs($connection);
141
            } else {
142 1
                throw new InvalidConfiguration(
143 1
                    'Connection must be callable, DbConfig, PDO or an array of parameters for DbConfig::__constructor'
144
                );
145
            }
146
        }
147 9
    }
148
149
    /**
150
     * Get the pdo connection for $name.
151
     *
152
     * @return \PDO
153
     * @throws NoConnection
154
     */
155 9
    public function getConnection()
156
    {
157 9
        if (!$this->connection) {
158 1
            throw new NoConnection('No database connection');
159
        }
160
161 8
        if (!$this->connection instanceof \PDO) {
162 7
            if ($this->connection instanceof DbConfig) {
163
                /** @var DbConfig $dbConfig */
164 4
                $dbConfig = $this->connection;
165 4
                $this->connection = new \PDO(
166 4
                    $dbConfig->getDsn(),
167 4
                    $dbConfig->user,
168 4
                    $dbConfig->pass,
169 4
                    $dbConfig->attributes
170
                );
171
            } else {
172 3
                $pdo = call_user_func($this->connection);
173 3
                if (!$pdo instanceof \PDO) {
174 1
                    throw new NoConnection('Getter does not return PDO instance');
175
                }
176 2
                $this->connection = $pdo;
177
            }
178 6
            $this->connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
179
        }
180
181 7
        return $this->connection;
182
    }
183
184
    /**
185
     * Synchronizing $entity with database
186
     *
187
     * If $reset is true it also calls reset() on $entity.
188
     *
189
     * @param Entity $entity
190
     * @param bool   $reset Reset entities current data
191
     * @return bool
192
     * @throws IncompletePrimaryKey
193
     * @throws InvalidConfiguration
194
     * @throws NoConnection
195
     * @throws NoEntity
196
     */
197 13
    public function sync(Entity $entity, $reset = false)
198
    {
199 13
        $this->map($entity, true);
200
201
        /** @var EntityFetcher $fetcher */
202 10
        $fetcher = $this->fetch(get_class($entity));
203 10
        foreach ($entity->getPrimaryKey() as $var => $value) {
204 10
            $fetcher->where($var, $value);
205
        }
206
207 10
        $result = $this->getConnection()->query($fetcher->getQuery());
208 7
        if ($originalData = $result->fetch(\PDO::FETCH_ASSOC)) {
209 5
            $entity->setOriginalData($originalData);
210 5
            if ($reset) {
211 2
                $entity->reset();
212
            }
213 5
            return true;
214
        }
215 2
        return false;
216
    }
217
218
    /**
219
     * Insert $entity in database
220
     *
221
     * Returns boolean if it is not auto incremented or the value of auto incremented column otherwise.
222
     *
223
     * @param Entity $entity
224
     * @param bool   $useAutoIncrement
225
     * @return mixed
226
     * @internal
227
     */
228 11
    public function insert(Entity $entity, $useAutoIncrement = true)
229
    {
230 11
        return $this->getDbal()->insert($entity, $useAutoIncrement);
231
    }
232
233
    /**
234
     * Update $entity in database
235
     *
236
     * @param Entity $entity
237
     * @return bool
238
     * @internal
239
     */
240 6
    public function update(Entity $entity)
241
    {
242 6
        $this->getDbal()->update($entity);
243 3
        $this->sync($entity, true);
244 3
        return true;
245
    }
246
247
    /**
248
     * Delete $entity from database
249
     *
250
     * This method does not delete from the map - you can still receive the entity via fetch.
251
     *
252
     * @param Entity $entity
253
     * @return bool
254
     */
255 6
    public function delete(Entity $entity)
256
    {
257 6
        $this->getDbal()->delete($entity);
258 4
        $entity->setOriginalData([]);
259 4
        return true;
260
    }
261
262
    /**
263
     * Map $entity in the entity map
264
     *
265
     * Returns the given entity or an entity that previously got mapped. This is useful to work in every function with
266
     * the same object.
267
     *
268
     * ```php?start_inline=true
269
     * $user = $enitityManager->map(new User(['id' => 42]));
270
     * ```
271
     *
272
     * @param Entity $entity
273
     * @param bool   $update Update the entity map
274
     * @return Entity
275
     * @throws IncompletePrimaryKey
276
     */
277 26
    public function map(Entity $entity, $update = false)
278
    {
279 26
        $class = get_class($entity);
280 26
        $key = md5(serialize($entity->getPrimaryKey()));
281
282 22
        if ($update || !isset($this->map[$class][$key])) {
283 22
            $this->map[$class][$key] = $entity;
284
        }
285
286 22
        return $this->map[$class][$key];
287
    }
288
289
    /**
290
     * Fetch one or more entities
291
     *
292
     * With $primaryKey it tries to find this primary key in the entity map (carefully: mostly the database returns a
293
     * string and we do not convert them). If there is no entity in the entity map it tries to fetch the entity from
294
     * the database. The return value is then null (not found) or the entity.
295
     *
296
     * Without $primaryKey it creates an entityFetcher and returns this.
297
     *
298
     * @param string|Entity $class      The entity class you want to fetch
299
     * @param mixed         $primaryKey The primary key of the entity you want to fetch
300
     * @return Entity|EntityFetcher
301
     * @throws IncompletePrimaryKey
302
     * @throws InvalidConfiguration
303
     * @throws NoConnection
304
     * @throws NoEntity
305
     */
306 60
    public function fetch($class, $primaryKey = null)
307
    {
308 60
        $reflection = new \ReflectionClass($class);
309 60
        if (!$reflection->isSubclassOf(Entity::class)) {
310 1
            throw new NoEntity($class . ' is not a subclass of Entity');
311
        }
312
313 59
        if ($primaryKey === null) {
314 51
            return new EntityFetcher($this, $class);
315
        }
316
317 9
        if (!is_array($primaryKey)) {
318 7
            $primaryKey = [$primaryKey];
319
        }
320
321 9
        $primaryKeyVars = $class::getPrimaryKeyVars();
322 9
        if (count($primaryKeyVars) !== count($primaryKey)) {
323 1
            throw new IncompletePrimaryKey(
324 1
                'Primary key consist of [' . implode(',', $primaryKeyVars) . '] only ' . count($primaryKey) . ' given'
325
            );
326
        }
327
328 8
        $primaryKey = array_combine($primaryKeyVars, $primaryKey);
329
330 8
        if (isset($this->map[$class][md5(serialize($primaryKey))])) {
331 7
            return $this->map[$class][md5(serialize($primaryKey))];
332
        }
333
334 1
        $fetcher = new EntityFetcher($this, $class);
335 1
        foreach ($primaryKey as $var => $value) {
336 1
            $fetcher->where($var, $value);
337
        }
338
339 1
        return $fetcher->one();
340
    }
341
342 175
    public function getDbal()
343
    {
344 175
        if (!$this->dbal) {
345 175
            $connectionType = $this->getConnection()->getAttribute(\PDO::ATTR_DRIVER_NAME);
346 175
            $dbalClass = __NAMESPACE__ . '\\Dbal\\' . ucfirst($connectionType);
347 175
            if (!class_exists($dbalClass)) {
348 163
                $this->dbal = new Other($this);
349
            } else {
350 12
                $this->dbal = new $dbalClass($this);
351
            }
352
353
            // backward compatibility - deprecated
354 175
            if (isset($this->options[$connectionType . 'True'])) {
355 1
                $this->dbal->setBooleanTrue($this->options[$connectionType . 'True']);
356
            }
357 175
            if (isset($this->options[$connectionType . 'False'])) {
358 1
                $this->dbal->setBooleanFalse($this->options[$connectionType . 'False']);
359
            }
360 175
            if (isset($this->options[self::OPT_QUOTING_CHARACTER])) {
1 ignored issue
show
Deprecated Code introduced by
The constant ORM\EntityManager::OPT_QUOTING_CHARACTER has been deprecated.

This class constant has been deprecated.

Loading history...
361 1
                $this->dbal->setQuotingCharacter($this->options[self::OPT_QUOTING_CHARACTER]);
1 ignored issue
show
Deprecated Code introduced by
The constant ORM\EntityManager::OPT_QUOTING_CHARACTER has been deprecated.

This class constant has been deprecated.

Loading history...
362
            }
363 175
            if (isset($this->options[self::OPT_IDENTIFIER_DIVIDER])) {
1 ignored issue
show
Deprecated Code introduced by
The constant ORM\EntityManager::OPT_IDENTIFIER_DIVIDER has been deprecated.

This class constant has been deprecated.

Loading history...
364 1
                $this->dbal->setIdentifierDivider($this->options[self::OPT_IDENTIFIER_DIVIDER]);
1 ignored issue
show
Deprecated Code introduced by
The constant ORM\EntityManager::OPT_IDENTIFIER_DIVIDER has been deprecated.

This class constant has been deprecated.

Loading history...
365
            }
366
        }
367
368 175
        return $this->dbal;
369
    }
370
371
    /**
372
     * Returns $value formatted to use in a sql statement.
373
     *
374
     * @param  mixed  $value      The variable that should be returned in SQL syntax
375
     * @return string
376
     */
377 103
    public function escapeValue($value)
378
    {
379 103
        return $this->getDbal()->escapeValue($value);
380
    }
381
382
    /**
383
     * Returns $identifier quoted for use in a sql statement
384
     *
385
     * @param string $identifier Identifier to quote
386
     * @return string
387
     */
388 74
    public function escapeIdentifier($identifier)
389
    {
390 74
        return $this->getDbal()->escapeIdentifier($identifier);
391
    }
392
}
393