Passed
Branch feature-dbal (53df75)
by Thomas
02:50
created

EntityManager::setOption()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 27
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 27
ccs 18
cts 18
cp 1
rs 8.439
c 0
b 0
f 0
cc 6
eloc 19
nc 6
nop 2
crap 6
1
<?php
2
3
namespace ORM;
4
5
use ORM\Dbal\Column;
6
use ORM\Dbal\Other;
7
use ORM\Exceptions\IncompletePrimaryKey;
8
use ORM\Exceptions\InvalidConfiguration;
9
use ORM\Exceptions\NoConnection;
10
use ORM\Exceptions\NoEntity;
11
use ORM\Exceptions\NotScalar;
12
use ORM\Exceptions\UnsupportedDriver;
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
28
    /** @deprecated */
29
    const OPT_MYSQL_BOOLEAN_TRUE     = 'mysqlTrue';
30
    /** @deprecated */
31
    const OPT_MYSQL_BOOLEAN_FALSE    = 'mysqlFalse';
32
    /** @deprecated */
33
    const OPT_SQLITE_BOOLEAN_TRUE    = 'sqliteTrue';
34
    /** @deprecated */
35
    const OPT_SQLITE_BOOLEAN_FASLE   = 'sqliteFalse';
36
    /** @deprecated */
37
    const OPT_PGSQL_BOOLEAN_TRUE     = 'pgsqlTrue';
38
    /** @deprecated */
39
    const OPT_PGSQL_BOOLEAN_FALSE    = 'pgsqlFalse';
40
    /** @deprecated */
41
    const OPT_QUOTING_CHARACTER      = 'quotingChar';
42
    /** @deprecated */
43
    const OPT_IDENTIFIER_DIVIDER     = 'identifierDivider';
44
45
    /** Connection to database
46
     * @var \PDO|callable|DbConfig */
47
    protected $connection;
48
49
    /** The Database Abstraction Layer
50
     * @var Dbal */
51
    private $dbal;
52
53
    /** The Entity map
54
     * @var Entity[][] */
55
    protected $map = [];
56
57
    /** The options set for this instance
58
     * @var array */
59
    protected $options = [];
60
61
    /** Already fetched column descriptions
62
     * @var Column[][] */
63
    protected $descriptions = [];
64
65
    /**
66
     * Constructor
67
     *
68
     * @param array $options Options for the new EntityManager
69
     * @throws InvalidConfiguration
70
     */
71 20
    public function __construct($options = [])
72
    {
73 20
        foreach ($options as $option => $value) {
74 9
            $this->setOption($option, $value);
75
        }
76 20
    }
77
78
    /**
79
     * Set $option to $value
80
     *
81
     * @param string $option One of OPT_* constants
82
     * @param mixed  $value
83
     * @return self
84
     */
85 13
    public function setOption($option, $value)
86
    {
87
        switch ($option) {
88 13
            case self::OPT_CONNECTION:
89 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...
90 1
                break;
91
92 12
            case self::OPT_TABLE_NAME_TEMPLATE:
93 1
                Entity::setTableNameTemplate($value);
94 1
                break;
95
96 11
            case self::OPT_NAMING_SCHEME_TABLE:
97 1
                Entity::setNamingSchemeTable($value);
98 1
                break;
99
100 10
            case self::OPT_NAMING_SCHEME_COLUMN:
101 1
                Entity::setNamingSchemeColumn($value);
102 1
                break;
103
104 9
            case self::OPT_NAMING_SCHEME_METHODS:
105 1
                Entity::setNamingSchemeMethods($value);
106 1
                break;
107
        }
108
109 13
        $this->options[$option] = $value;
110 13
        return $this;
111
    }
112
113
    /**
114
     * Get $option
115
     *
116
     * @param $option
117
     * @return mixed
118
     */
119 4
    public function getOption($option)
120
    {
121 4
        return isset($this->options[$option]) ? $this->options[$option] : null;
122
    }
123
124
    /**
125
     * Add connection after instantiation
126
     *
127
     * The connection can be an array of parameters for DbConfig::__construct(), a callable function that returns a PDO
128
     * instance, an instance of DbConfig or a PDO instance itself.
129
     *
130
     * When it is not a PDO instance the connection get established on first use.
131
     *
132
     * @param \PDO|callable|DbConfig|array $connection A configuration for (or a) PDO instance
133
     * @throws InvalidConfiguration
134
     */
135 10
    public function setConnection($connection)
136
    {
137 10
        if (is_callable($connection) || $connection instanceof DbConfig) {
138 6
            $this->connection = $connection;
139
        } else {
140 4
            if ($connection instanceof \PDO) {
141 1
                $connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
142 1
                $this->connection = $connection;
143 3
            } elseif (is_array($connection)) {
144 2
                $dbConfigReflection = new \ReflectionClass(DbConfig::class);
145 2
                $this->connection   = $dbConfigReflection->newInstanceArgs($connection);
146
            } else {
147 1
                throw new InvalidConfiguration(
148 1
                    'Connection must be callable, DbConfig, PDO or an array of parameters for DbConfig::__constructor'
149
                );
150
            }
151
        }
152 9
    }
153
154
    /**
155
     * Get the pdo connection.
156
     *
157
     * @return \PDO
158
     * @throws NoConnection
159
     */
160 9
    public function getConnection()
161
    {
162 9
        if (!$this->connection) {
163 1
            throw new NoConnection('No database connection');
164
        }
165
166 8
        if (!$this->connection instanceof \PDO) {
167 7
            if ($this->connection instanceof DbConfig) {
168
                /** @var DbConfig $dbConfig */
169 4
                $dbConfig = $this->connection;
170 4
                $this->connection = new \PDO(
171 4
                    $dbConfig->getDsn(),
172 4
                    $dbConfig->user,
173 4
                    $dbConfig->pass,
174 4
                    $dbConfig->attributes
175
                );
176
            } else {
177 3
                $pdo = call_user_func($this->connection);
178 3
                if (!$pdo instanceof \PDO) {
179 1
                    throw new NoConnection('Getter does not return PDO instance');
180
                }
181 2
                $this->connection = $pdo;
182
            }
183 6
            $this->connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
184
        }
185
186 7
        return $this->connection;
187
    }
188
189
    /**
190
     * Synchronizing $entity with database
191
     *
192
     * If $reset is true it also calls reset() on $entity.
193
     *
194
     * @param Entity $entity
195
     * @param bool   $reset Reset entities current data
196
     * @return bool
197
     * @throws IncompletePrimaryKey
198
     * @throws InvalidConfiguration
199
     * @throws NoConnection
200
     * @throws NoEntity
201
     */
202 13
    public function sync(Entity $entity, $reset = false)
203
    {
204 13
        $this->map($entity, true);
205
206
        /** @var EntityFetcher $fetcher */
207 10
        $fetcher = $this->fetch(get_class($entity));
208 10
        foreach ($entity->getPrimaryKey() as $var => $value) {
209 10
            $fetcher->where($var, $value);
210
        }
211
212 10
        $result = $this->getConnection()->query($fetcher->getQuery());
213 7
        if ($originalData = $result->fetch(\PDO::FETCH_ASSOC)) {
214 5
            $entity->setOriginalData($originalData);
215 5
            if ($reset) {
216 2
                $entity->reset();
217
            }
218 5
            return true;
219
        }
220 2
        return false;
221
    }
222
223
    /**
224
     * Insert $entity in database
225
     *
226
     * Returns boolean if it is not auto incremented or the value of auto incremented column otherwise.
227
     *
228
     * @param Entity $entity
229
     * @param bool   $useAutoIncrement
230
     * @return mixed
231
     * @internal
232
     */
233 11
    public function insert(Entity $entity, $useAutoIncrement = true)
234
    {
235 11
        return $this->getDbal()->insert($entity, $useAutoIncrement);
236
    }
237
238
    /**
239
     * Update $entity in database
240
     *
241
     * @param Entity $entity
242
     * @return bool
243
     * @internal
244
     */
245 6
    public function update(Entity $entity)
246
    {
247 6
        $this->getDbal()->update($entity);
248 3
        $this->sync($entity, true);
249 3
        return true;
250
    }
251
252
    /**
253
     * Delete $entity from database
254
     *
255
     * This method does not delete from the map - you can still receive the entity via fetch.
256
     *
257
     * @param Entity $entity
258
     * @return bool
259
     */
260 6
    public function delete(Entity $entity)
261
    {
262 6
        $this->getDbal()->delete($entity);
263 4
        $entity->setOriginalData([]);
264 4
        return true;
265
    }
266
267
    /**
268
     * Map $entity in the entity map
269
     *
270
     * Returns the given entity or an entity that previously got mapped. This is useful to work in every function with
271
     * the same object.
272
     *
273
     * ```php?start_inline=true
274
     * $user = $enitityManager->map(new User(['id' => 42]));
275
     * ```
276
     *
277
     * @param Entity $entity
278
     * @param bool   $update Update the entity map
279
     * @return Entity
280
     * @throws IncompletePrimaryKey
281
     */
282 26
    public function map(Entity $entity, $update = false)
283
    {
284 26
        $class = get_class($entity);
285 26
        $key = md5(serialize($entity->getPrimaryKey()));
286
287 22
        if ($update || !isset($this->map[$class][$key])) {
288 22
            $this->map[$class][$key] = $entity;
289
        }
290
291 22
        return $this->map[$class][$key];
292
    }
293
294
    /**
295
     * Fetch one or more entities
296
     *
297
     * With $primaryKey it tries to find this primary key in the entity map (carefully: mostly the database returns a
298
     * string and we do not convert them). If there is no entity in the entity map it tries to fetch the entity from
299
     * the database. The return value is then null (not found) or the entity.
300
     *
301
     * Without $primaryKey it creates an entityFetcher and returns this.
302
     *
303
     * @param string|Entity $class      The entity class you want to fetch
304
     * @param mixed         $primaryKey The primary key of the entity you want to fetch
305
     * @return Entity|EntityFetcher
306
     * @throws IncompletePrimaryKey
307
     * @throws InvalidConfiguration
308
     * @throws NoConnection
309
     * @throws NoEntity
310
     */
311 60
    public function fetch($class, $primaryKey = null)
312
    {
313 60
        $reflection = new \ReflectionClass($class);
314 60
        if (!$reflection->isSubclassOf(Entity::class)) {
315 1
            throw new NoEntity($class . ' is not a subclass of Entity');
316
        }
317
318 59
        if ($primaryKey === null) {
319 51
            return new EntityFetcher($this, $class);
320
        }
321
322 9
        if (!is_array($primaryKey)) {
323 7
            $primaryKey = [$primaryKey];
324
        }
325
326 9
        $primaryKeyVars = $class::getPrimaryKeyVars();
327 9
        if (count($primaryKeyVars) !== count($primaryKey)) {
328 1
            throw new IncompletePrimaryKey(
329 1
                'Primary key consist of [' . implode(',', $primaryKeyVars) . '] only ' . count($primaryKey) . ' given'
330
            );
331
        }
332
333 8
        $primaryKey = array_combine($primaryKeyVars, $primaryKey);
334
335 8
        if (isset($this->map[$class][md5(serialize($primaryKey))])) {
336 7
            return $this->map[$class][md5(serialize($primaryKey))];
337
        }
338
339 1
        $fetcher = new EntityFetcher($this, $class);
340 1
        foreach ($primaryKey as $var => $value) {
341 1
            $fetcher->where($var, $value);
342
        }
343
344 1
        return $fetcher->one();
345
    }
346
347 11
    public function getDbal()
348
    {
349 11
        if (!$this->dbal) {
350 11
            $connectionType = $this->getConnection()->getAttribute(\PDO::ATTR_DRIVER_NAME);
351 11
            $dbalClass = __NAMESPACE__ . '\\Dbal\\' . ucfirst($connectionType);
352 11
            if (!class_exists($dbalClass)) {
353 2
                $this->dbal = new Other($this);
354
            } else {
355 9
                $this->dbal = new $dbalClass($this);
356
            }
357
358
            // backward compatibility - deprecated
359 11
            if (isset($this->options[$connectionType . 'True'])) {
360 1
                $this->dbal->setBooleanTrue($this->options[$connectionType . 'True']);
361
            }
362 11
            if (isset($this->options[$connectionType . 'False'])) {
363 1
                $this->dbal->setBooleanFalse($this->options[$connectionType . 'False']);
364
            }
365 11
            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...
366 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...
367
            }
368 11
            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...
369 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...
370
            }
371
        }
372
373 11
        return $this->dbal;
374
    }
375
376
    /**
377
     * Returns $value formatted to use in a sql statement.
378
     *
379
     * @param  mixed  $value      The variable that should be returned in SQL syntax
380
     * @return string
381
     * @codeCoverageIgnore This is just a proxy
382
     */
383
    public function escapeValue($value)
384
    {
385
        return $this->getDbal()->escapeValue($value);
386
    }
387
388
    /**
389
     * Returns $identifier quoted for use in a sql statement
390
     *
391
     * @param string $identifier Identifier to quote
392
     * @return string
393
     * @codeCoverageIgnore This is just a proxy
394
     */
395
    public function escapeIdentifier($identifier)
396
    {
397
        return $this->getDbal()->escapeIdentifier($identifier);
398
    }
399
400
    /**
401
     * Returns an array of columns from $table.
402
     *
403
     * @param string $table
404
     * @return Dbal\Column[]
405
     */
406 2
    public function describe($table)
407
    {
408 2
        if (!isset($this->descriptions[$table])) {
409 2
            $this->descriptions[$table] = $this->getDbal()->describe($table);
410
        }
411 2
        return $this->descriptions[$table];
412
    }
413
}
414