Completed
Pull Request — master (#49)
by Thomas
03:23
created

Dbal   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 412
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 94.34%

Importance

Changes 0
Metric Value
wmc 53
lcom 1
cbo 5
dl 0
loc 412
ccs 150
cts 159
cp 0.9434
rs 6.96
c 0
b 0
f 0

22 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 describe() 0 4 1
A assertSameType() 0 15 4
A insert() 0 10 2
A insertAndSync() 0 10 2
A insertAndSyncWithAutoInc() 0 8 2
A update() 0 26 4
A delete() 0 15 2
A buildInsertStatement() 0 26 4
A updateAutoincrement() 0 8 1
B syncInserted() 0 37 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 190
    public function escapeIdentifier($identifier)
84
    {
85 190
        $quote = $this->quotingCharacter;
86 190
        $divider = $this->identifierDivider;
87 190
        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 183
    public function escapeValue($value)
98
    {
99 183
        $type   = is_object($value) ? get_class($value) : gettype($value);
100 183
        $method = [ $this, 'escape' . ucfirst($type) ];
101
102 183
        if (is_callable($method)) {
103 182
            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
     * @param Entity[] $entities
124
     * @return bool
125
     * @throws Exception\InvalidArgument
126
     */
127 12
    protected static function assertSameType(array $entities)
128
    {
129 12
        if (count($entities) < 2) {
130 12
            return true;
131
        }
132
133
        $type = get_class(reset($entities));
134
        foreach ($entities as $i => $entity) {
135
            if (get_class($entity) !== $type) {
136
                throw new Exception\InvalidArgument(sprintf('$entities[%d] is not from the same type'));
137
            }
138
        }
139
140
        return true;
141
    }
142
143 5
    public function insert(Entity ...$entities)
144
    {
145 5
        if (count($entities) === 0) {
146
            return false;
147
        }
148 5
        static::assertSameType($entities);
149 5
        $insert = $this->buildInsertStatement(...$entities);
150 5
        $this->entityManager->getConnection()->query($insert);
151 5
        return true;
152
    }
153
154 5
    public function insertAndSync(Entity ...$entities)
155
    {
156 5
        if (count($entities) === 0) {
157
            return false;
158
        }
159 5
        self::assertSameType($entities);
160 5
        $this->insert(...$entities);
161 5
        $this->syncInserted(...$entities);
162 5
        return true;
163
    }
164
165
    /**
166
     * @param Entity ...$entities
167
     * @return int|bool
168
     * @throws UnsupportedDriver
169
     */
170 1
    public function insertAndSyncWithAutoInc(Entity ...$entities)
171
    {
172 1
        if (count($entities) === 0) {
173
            return false;
174
        }
175 1
        self::assertSameType($entities);
176 1
        throw new UnsupportedDriver('Auto incremented column for this driver is not supported');
177
    }
178
179
    /**
180
     * Update $entity in database and returns success
181
     *
182
     * @param Entity $entity
183
     * @return bool
184
     * @internal
185
     */
186 6
    public function update(Entity $entity)
187
    {
188 6
        $data       = $entity->getData();
189 6
        $primaryKey = $entity->getPrimaryKey();
190
191 6
        $where = [];
192 6
        foreach ($primaryKey as $attribute => $value) {
193 6
            $col     = $entity::getColumnName($attribute);
194 6
            $where[] = $this->escapeIdentifier($col) . ' = ' . $this->escapeValue($value);
195 6
            if (isset($data[$col])) {
196 6
                unset($data[$col]);
197
            }
198
        }
199
200 6
        $set = [];
201 6
        foreach ($data as $col => $value) {
202 6
            $set[] = $this->escapeIdentifier($col) . ' = ' . $this->escapeValue($value);
203
        }
204
205 6
        $statement = 'UPDATE ' . $this->escapeIdentifier($entity::getTableName()) . ' ' .
206 6
                     'SET ' . implode(',', $set) . ' ' .
207 6
                     'WHERE ' . implode(' AND ', $where);
208 6
        $this->entityManager->getConnection()->query($statement);
209
210 3
        return $this->entityManager->sync($entity, true);
211
    }
212
213
    /**
214
     * Delete $entity from database
215
     *
216
     * This method does not delete from the map - you can still receive the entity via fetch.
217
     *
218
     * @param Entity $entity
219
     * @return bool
220
     */
221 6
    public function delete(Entity $entity)
222
    {
223 6
        $primaryKey = $entity->getPrimaryKey();
224 6
        $where      = [];
225 6
        foreach ($primaryKey as $attribute => $value) {
226 6
            $col     = $entity::getColumnName($attribute);
227 6
            $where[] = $this->escapeIdentifier($col) . ' = ' . $this->escapeValue($value);
228
        }
229
230 6
        $statement = 'DELETE FROM ' . $this->escapeIdentifier($entity::getTableName()) . ' ' .
231 6
                     'WHERE ' . implode(' AND ', $where);
232 6
        $this->entityManager->getConnection()->query($statement);
233
234 4
        return true;
235
    }
236
237
    /**
238
     * Build the insert statement for $entity
239
     *
240
     * @param Entity $entity
241
     * @param Entity[] $entities
242
     * @return string
243
     */
244 10
    protected function buildInsertStatement(Entity $entity, Entity ...$entities)
245
    {
246 10
        array_unshift($entities, $entity);
247 10
        $cols = [];
248 10
        $rows = [];
249 10
        foreach ($entities as $entity) {
250 10
            $data = $entity->getData();
251 10
            $cols = array_unique(array_merge($cols, array_keys($data)));
252 10
            $rows[] = $data;
253
        }
254
255 10
        $cols = array_combine($cols, array_map([$this, 'escapeIdentifier'], $cols));
256
257 10
        $statement = 'INSERT INTO ' . $this->escapeIdentifier($entity::getTableName()) . ' ' .
258 10
                     '(' . implode(',', $cols) . ') VALUES ';
259
260 10
        $statement .= implode(',', array_map(function ($values) use ($cols) {
261 10
            $result = [];
262 10
            foreach ($cols as $key => $col) {
263 10
                $result[] = isset($values[$key]) ? $this->escapeValue($values[$key]) : $this->escapeNULL();
264
            }
265 10
            return '(' . implode(',', $result) . ')';
266 10
        }, $rows));
267
268 10
        return $statement;
269
    }
270
271
    /**
272
     * Update the autoincrement value
273
     *
274
     * @param Entity     $entity
275
     * @param int|string $value
276
     */
277 1
    protected function updateAutoincrement(Entity $entity, $value)
278
    {
279 1
        $var    = $entity::getPrimaryKeyVars()[0];
280 1
        $column = $entity::getColumnName($var);
281
282 1
        $entity->setOriginalData(array_merge($entity->getData(), [ $column => $value ]));
283 1
        $entity->__set($var, $value);
284 1
    }
285
286
    /**
287
     * Sync the $entities after insert
288
     *
289
     * @param Entity ...$entities
290
     */
291 6
    protected function syncInserted(Entity ...$entities)
292
    {
293 6
        $entity = reset($entities);
294 6
        $vars = $entity::getPrimaryKeyVars();
295 6
        $cols = array_map([$entity, 'getColumnName'], $vars);
296 6
        $primary = array_combine($vars, $cols);
297
298 6
        $query = "SELECT * FROM " . $this->escapeIdentifier($entity::getTableName()) . " WHERE ";
299 6
        $query .= count($cols) > 1 ?
300
            '(' . implode(',', array_map([$this, 'escapeIdentifier'], $cols)) . ')' :
301 6
            $this->escapeIdentifier($cols[0]);
302 6
        $query .= ' IN (';
303 6
        $pKeys = [];
304 6
        foreach ($entities as $entity) {
305 6
            $pKey = array_map([$this, 'escapeValue'], $entity->getPrimaryKey());
306 6
            $pKeys[] = count($cols) > 1 ? '(' . implode(',', $pKey) . ')' : reset($pKey);
307
        }
308 6
        $query .= implode(',', $pKeys) . ')';
309
310 6
        $statement = $this->entityManager->getConnection()->query($query);
311 6
        $left = $entities;
312 6
        while ($row = $statement->fetch(\PDO::FETCH_ASSOC)) {
313 6
            foreach ($left as $k => $entity) {
314 6
                foreach ($primary as $var => $col) {
315 6
                    if ($entity->$var != $row[$col]) {
316 6
                        continue 2;
317
                    }
318
                }
319
320 6
                $this->entityManager->map($entity, true);
321 6
                $entity->setOriginalData($row);
322 6
                $entity->reset();
323 6
                unset($left[$k]);
324 6
                break;
325
            }
326
        }
327 6
    }
328
329
    /**
330
     * Normalize $type
331
     *
332
     * The type returned by mysql is for example VARCHAR(20) - this function converts it to varchar
333
     *
334
     * @param string $type
335
     * @return string
336
     */
337 79
    protected function normalizeType($type)
338
    {
339 79
        $type = strtolower($type);
340
341 79
        if (($pos = strpos($type, '(')) !== false && $pos > 0) {
342 33
            $type = substr($type, 0, $pos);
343
        }
344
345 79
        return trim($type);
346
    }
347
348
    /**
349
     * Extract content from parenthesis in $type
350
     *
351
     * @param string $type
352
     * @return string
353
     */
354 24
    protected function extractParenthesis($type)
355
    {
356 24
        if (preg_match('/\((.+)\)/', $type, $match)) {
357 16
            return $match[1];
358
        }
359
360 8
        return null;
361
    }
362
363
    /**
364
     * Escape a string for query
365
     *
366
     * @param string $value
367
     * @return string
368
     */
369 120
    protected function escapeString($value)
370
    {
371 120
        return $this->entityManager->getConnection()->quote($value);
372
    }
373
374
    /**
375
     * Escape an integer for query
376
     *
377
     * @param int $value
378
     * @return string
379
     */
380 52
    protected function escapeInteger($value)
381
    {
382 52
        return (string) $value;
383
    }
384
385
    /**
386
     * Escape a double for Query
387
     *
388
     * @param double $value
389
     * @return string
390
     */
391 4
    protected function escapeDouble($value)
392
    {
393 4
        return (string) $value;
394
    }
395
396
    /**
397
     * Escape NULL for query
398
     *
399
     * @return string
400
     */
401 1
    protected function escapeNULL()
402
    {
403 1
        return 'NULL';
404
    }
405
406
    /**
407
     * Escape a boolean for query
408
     *
409
     * @param bool $value
410
     * @return string
411
     */
412 20
    protected function escapeBoolean($value)
413
    {
414 20
        return ($value) ? $this->booleanTrue : $this->booleanFalse;
415
    }
416
417
    /**
418
     * Escape a date time object for query
419
     *
420
     * @param \DateTime $value
421
     * @return mixed
422
     */
423 1
    protected function escapeDateTime(\DateTime $value)
424
    {
425 1
        $value->setTimezone(new \DateTimeZone('UTC'));
426 1
        return $this->escapeString($value->format('Y-m-d\TH:i:s.u\Z'));
427
    }
428
}
429