Test Failed
Pull Request — master (#36)
by Thomas
02:48
created

EntityManager::getDbal()   C

Complexity

Conditions 7
Paths 33

Size

Total Lines 28
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 7

Importance

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