Dbal   B
last analyzed

Complexity

Total Complexity 46

Size/Duplication

Total Lines 368
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 46
lcom 1
cbo 6
dl 0
loc 368
rs 8.72
c 0
b 0
f 0
ccs 105
cts 105
cp 1

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 2
A setOption() 0 21 5
A escapeIdentifier() 0 6 1
A escapeValue() 0 14 4
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 31 6
A buildCompositeWhereInStatement() 0 13 3
A normalizeType() 0 10 3

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