Completed
Pull Request — master (#49)
by Thomas
16:34
created

Dbal   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 406
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 74.83%

Importance

Changes 0
Metric Value
wmc 52
lcom 1
cbo 4
dl 0
loc 406
ccs 110
cts 147
cp 0.7483
rs 7.44
c 0
b 0
f 0

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 2
A setOption() 0 21 5
A escapeIdentifier() 0 6 1
A escapeValue() 0 11 3
A update() 0 26 4
A delete() 0 15 2
A buildInsertStatement() 0 26 4
A updateAutoincrement() 0 8 1
A describe() 0 4 1
A insert() 0 11 3
A bulkInsert() 0 19 6
B syncInserted() 0 35 8
A normalizeType() 0 10 3
A extractParenthesis() 0 8 2
A escapeString() 0 4 1
A escapeInteger() 0 4 1
A escapeDouble() 0 4 1
A escapeNULL() 0 4 1
A escapeBoolean() 0 4 2
A escapeDateTime() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like Dbal often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Dbal, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace ORM\Dbal;
4
5
use ORM\Entity;
6
use ORM\EntityManager;
7
use ORM\Exception;
8
use ORM\Exception\NotScalar;
9
use ORM\Exception\UnsupportedDriver;
10
11
/**
12
 * Base class for database abstraction
13
 *
14
 * @package ORM
15
 * @author  Thomas Flori <[email protected]>
16
 */
17
abstract class Dbal
18
{
19
    /** @var array */
20
    protected static $typeMapping = [];
21
22
    /** @var EntityManager */
23
    protected $entityManager;
24
    /** @var string */
25
    protected $quotingCharacter = '"';
26
    /** @var string */
27
    protected $identifierDivider = '.';
28
    /** @var string */
29
    protected $booleanTrue = '1';
30
    /** @var string */
31
    protected $booleanFalse = '0';
32
33
    /**
34
     * Dbal constructor.
35
     *
36
     * @param EntityManager $entityManager
37
     * @param array         $options
38
     */
39 746
    public function __construct(EntityManager $entityManager, array $options = [])
40
    {
41 746
        $this->entityManager = $entityManager;
42
43 746
        foreach ($options as $option => $value) {
44 4
            $this->setOption($option, $value);
45
        }
46 746
    }
47
48
    /**
49
     * Set $option to $value
50
     *
51
     * @param string $option
52
     * @param mixed  $value
53
     * @return self
54
     */
55 22
    public function setOption($option, $value)
56
    {
57
        switch ($option) {
58 22
            case EntityManager::OPT_IDENTIFIER_DIVIDER:
59 1
                $this->identifierDivider = $value;
60 1
                break;
61
62 21
            case EntityManager::OPT_QUOTING_CHARACTER:
63 1
                $this->quotingCharacter = $value;
64 1
                break;
65
66 20
            case EntityManager::OPT_BOOLEAN_TRUE:
67 19
                $this->booleanTrue = $value;
68 19
                break;
69
70 19
            case EntityManager::OPT_BOOLEAN_FALSE:
71 19
                $this->booleanFalse = $value;
72 19
                break;
73
        }
74 22
        return $this;
75
    }
76
77
    /**
78
     * Returns $identifier quoted for use in a sql statement
79
     *
80
     * @param string $identifier Identifier to quote
81
     * @return string
82
     */
83 191
    public function escapeIdentifier($identifier)
84
    {
85 191
        $quote = $this->quotingCharacter;
86 191
        $divider = $this->identifierDivider;
87 191
        return $quote . str_replace($divider, $quote . $divider . $quote, $identifier) . $quote;
88
    }
89
90
    /**
91
     * Returns $value formatted to use in a sql statement.
92
     *
93
     * @param  mixed $value The variable that should be returned in SQL syntax
94
     * @return string
95
     * @throws NotScalar
96
     */
97 184
    public function escapeValue($value)
98
    {
99 184
        $type   = is_object($value) ? get_class($value) : gettype($value);
100 184
        $method = [ $this, 'escape' . ucfirst($type) ];
101
102 184
        if (is_callable($method)) {
103 183
            return call_user_func($method, $value);
104
        } else {
105 1
            throw new NotScalar('$value has to be scalar data type. ' . gettype($value) . ' given');
106
        }
107
    }
108
109
    /**
110
     * Describe a table
111
     *
112
     * @param string $table
113
     * @return Table|Column[]
114
     * @throws UnsupportedDriver
115
     * @throws Exception
116
     */
117 1
    public function describe($table)
118
    {
119 1
        throw new UnsupportedDriver('Not supported for this driver');
120
    }
121
122
    /**
123
     * Inserts $entity in database and synchronizes the entity
124
     *
125
     * Returns whether the insert was successful or not.
126
     *
127
     * @param Entity $entity
128
     * @param bool $useAutoIncrement
129
     * @return bool
130
     * @throws UnsupportedDriver
131
     */
132 2
    public function insert(Entity $entity, $useAutoIncrement = true)
133
    {
134 2
        $statement = $this->buildInsertStatement($entity);
135
136 2
        if ($useAutoIncrement && $entity::isAutoIncremented()) {
137 1
            throw new UnsupportedDriver('Auto incremented column for this driver is not supported');
138
        }
139
140 1
        $this->entityManager->getConnection()->query($statement);
141 1
        return $this->entityManager->sync($entity, true);
142
    }
143
144
    /**
145
     * Inserts $entities in one query
146
     *
147
     * If update is false the entities will not be synchronized after insert.
148
     *
149
     * @param Entity[] $entities
150
     * @param bool $update
151
     * @param bool $useAutoIncrement
152
     * @return bool
153
     * @throws UnsupportedDriver
154
     */
155
    public function bulkInsert(array $entities, $update = true, $useAutoIncrement = true)
156
    {
157
        if (count($entities) === 0) {
158
            return false;
159
        }
160
161
        $statement = $this->buildInsertStatement(...$entities);
162
163
        $entity = reset($entities);
164
        if ($update && $useAutoIncrement && $entity::isAutoIncremented()) {
165
            throw new UnsupportedDriver('Auto incremented column for this driver is not supported');
166
        }
167
168
        $this->entityManager->getConnection()->query($statement);
169
        if ($update) {
170
            $this->syncInserted(...$entities);
171
        }
172
        return true;
173
    }
174
175
    /**
176
     * Update $entity in database and returns success
177
     *
178
     * @param Entity $entity
179
     * @return bool
180
     * @internal
181
     */
182 6
    public function update(Entity $entity)
183
    {
184 6
        $data       = $entity->getData();
185 6
        $primaryKey = $entity->getPrimaryKey();
186
187 6
        $where = [];
188 6
        foreach ($primaryKey as $attribute => $value) {
189 6
            $col     = $entity::getColumnName($attribute);
190 6
            $where[] = $this->escapeIdentifier($col) . ' = ' . $this->escapeValue($value);
191 6
            if (isset($data[$col])) {
192 6
                unset($data[$col]);
193
            }
194
        }
195
196 6
        $set = [];
197 6
        foreach ($data as $col => $value) {
198 6
            $set[] = $this->escapeIdentifier($col) . ' = ' . $this->escapeValue($value);
199
        }
200
201 6
        $statement = 'UPDATE ' . $this->escapeIdentifier($entity::getTableName()) . ' ' .
202 6
                     'SET ' . implode(',', $set) . ' ' .
203 6
                     'WHERE ' . implode(' AND ', $where);
204 6
        $this->entityManager->getConnection()->query($statement);
205
206 3
        return $this->entityManager->sync($entity, true);
207
    }
208
209
    /**
210
     * Delete $entity from database
211
     *
212
     * This method does not delete from the map - you can still receive the entity via fetch.
213
     *
214
     * @param Entity $entity
215
     * @return bool
216
     */
217 6
    public function delete(Entity $entity)
218
    {
219 6
        $primaryKey = $entity->getPrimaryKey();
220 6
        $where      = [];
221 6
        foreach ($primaryKey as $attribute => $value) {
222 6
            $col     = $entity::getColumnName($attribute);
223 6
            $where[] = $this->escapeIdentifier($col) . ' = ' . $this->escapeValue($value);
224
        }
225
226 6
        $statement = 'DELETE FROM ' . $this->escapeIdentifier($entity::getTableName()) . ' ' .
227 6
                     'WHERE ' . implode(' AND ', $where);
228 6
        $this->entityManager->getConnection()->query($statement);
229
230 4
        return true;
231
    }
232
233
    /**
234
     * Build the insert statement for $entity
235
     *
236
     * @param Entity $entity
237
     * @param Entity[] $entities
238
     * @return string
239
     */
240 12
    protected function buildInsertStatement(Entity $entity, Entity ...$entities)
241
    {
242 12
        array_unshift($entities, $entity);
243 12
        $cols = [];
244 12
        $rows = [];
245 12
        foreach ($entities as $entity) {
246 12
            $data = $entity->getData();
247 12
            $cols = array_unique(array_merge($cols, array_keys($data)));
248 12
            $rows[] = $data;
249
        }
250
251 12
        $cols = array_combine($cols, array_map([$this, 'escapeIdentifier'], $cols));
252
253 12
        $statement = 'INSERT INTO ' . $this->escapeIdentifier($entity::getTableName()) . ' ' .
254 12
                     '(' . implode(',', $cols) . ') VALUES ';
255
256 12
        $statement .= implode(',', array_map(function ($values) use ($cols) {
257 12
            $result = [];
258 12
            foreach ($cols as $key => $col) {
259 11
                $result[] = isset($values[$key]) ? $this->escapeValue($values[$key]) : $this->escapeNULL();
260
            }
261 12
            return '(' . implode(',', $result) . ')';
262 12
        }, $rows));
263
264 12
        return $statement;
265
    }
266
267
    /**
268
     * Update the autoincrement value
269
     *
270
     * @param Entity     $entity
271
     * @param int|string $value
272
     */
273 3
    protected function updateAutoincrement(Entity $entity, $value)
274
    {
275 3
        $var    = $entity::getPrimaryKeyVars()[0];
276 3
        $column = $entity::getColumnName($var);
277
278 3
        $entity->setOriginalData(array_merge($entity->getData(), [ $column => $value ]));
279 3
        $entity->__set($var, $value);
280 3
    }
281
282
    /**
283
     * Sync the $entities after insert
284
     *
285
     * @param Entity ...$entities
286
     */
287
    protected function syncInserted(Entity ...$entities)
288
    {
289
        $entity = reset($entities);
290
        $vars = $entity::getPrimaryKeyVars();
291
        $cols = array_map([$entity, 'getColumnName'], $vars);
292
        $primary = array_combine($vars, $cols);
293
294
        $query = "SELECT * FROM " . $this->escapeIdentifier($entity::getTableName()) . " WHERE ";
295
        $query .= count($cols) > 1 ? '(' . implode(',', array_map([$this, 'escapeIdentifier'], $cols)) . ')' : $cols[0];
296
        $query .= ' IN (';
297
        $pKeys = [];
298
        foreach ($entities as $entity) {
299
            $pKey = array_map([$this, 'escapeValue'], $entity->getPrimaryKey());
300
            $pKeys[] = count($cols) > 1 ? '(' . implode(',', $pKey) . ')' : reset($pKey);
301
        }
302
        $query .= implode(',', $pKeys) . ')';
303
304
        $statement = $this->entityManager->getConnection()->query($query);
305
        $left = $entities;
306
        while ($row = $statement->fetch(\PDO::FETCH_ASSOC)) {
307
            foreach ($left as $k => $entity) {
308
                foreach ($primary as $var => $col) {
309
                    if ($entity->$var != $row[$col]) {
310
                        continue 2;
311
                    }
312
                }
313
314
                $this->entityManager->map($entity, true);
315
                $entity->setOriginalData($row);
316
                $entity->reset();
317
                unset($left[$k]);
318
                break;
319
            }
320
        }
321
    }
322
323
    /**
324
     * Normalize $type
325
     *
326
     * The type returned by mysql is for example VARCHAR(20) - this function converts it to varchar
327
     *
328
     * @param string $type
329
     * @return string
330
     */
331 79
    protected function normalizeType($type)
332
    {
333 79
        $type = strtolower($type);
334
335 79
        if (($pos = strpos($type, '(')) !== false && $pos > 0) {
336 33
            $type = substr($type, 0, $pos);
337
        }
338
339 79
        return trim($type);
340
    }
341
342
    /**
343
     * Extract content from parenthesis in $type
344
     *
345
     * @param string $type
346
     * @return string
347
     */
348 24
    protected function extractParenthesis($type)
349
    {
350 24
        if (preg_match('/\((.+)\)/', $type, $match)) {
351 16
            return $match[1];
352
        }
353
354 8
        return null;
355
    }
356
357
    /**
358
     * Escape a string for query
359
     *
360
     * @param string $value
361
     * @return string
362
     */
363 121
    protected function escapeString($value)
364
    {
365 121
        return $this->entityManager->getConnection()->quote($value);
366
    }
367
368
    /**
369
     * Escape an integer for query
370
     *
371
     * @param int $value
372
     * @return string
373
     */
374 51
    protected function escapeInteger($value)
375
    {
376 51
        return (string) $value;
377
    }
378
379
    /**
380
     * Escape a double for Query
381
     *
382
     * @param double $value
383
     * @return string
384
     */
385 4
    protected function escapeDouble($value)
386
    {
387 4
        return (string) $value;
388
    }
389
390
    /**
391
     * Escape NULL for query
392
     *
393
     * @return string
394
     */
395 1
    protected function escapeNULL()
396
    {
397 1
        return 'NULL';
398
    }
399
400
    /**
401
     * Escape a boolean for query
402
     *
403
     * @param bool $value
404
     * @return string
405
     */
406 20
    protected function escapeBoolean($value)
407
    {
408 20
        return ($value) ? $this->booleanTrue : $this->booleanFalse;
409
    }
410
411
    /**
412
     * Escape a date time object for query
413
     *
414
     * @param \DateTime $value
415
     * @return mixed
416
     */
417 1
    protected function escapeDateTime(\DateTime $value)
418
    {
419 1
        $value->setTimezone(new \DateTimeZone('UTC'));
420 1
        return $this->escapeString($value->format('Y-m-d\TH:i:s.u\Z'));
421
    }
422
}
423