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

Dbal::describe()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
ccs 1
cts 1
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
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 731
     */
39
    public function __construct(EntityManager $entityManager, array $options = [])
40 731
    {
41
        $this->entityManager = $entityManager;
42 731
43 4
        foreach ($options as $option => $value) {
44
            $this->setOption($option, $value);
45 731
        }
46
    }
47
48
    /**
49
     * Set $option to $value
50
     *
51
     * @param string $option
52
     * @param mixed  $value
53
     * @return self
54 22
     */
55
    public function setOption($option, $value)
56
    {
57 22
        switch ($option) {
58 1
            case EntityManager::OPT_IDENTIFIER_DIVIDER:
59 1
                $this->identifierDivider = $value;
60
                break;
61 21
62 1
            case EntityManager::OPT_QUOTING_CHARACTER:
63 1
                $this->quotingCharacter = $value;
64
                break;
65 20
66 19
            case EntityManager::OPT_BOOLEAN_TRUE:
67 19
                $this->booleanTrue = $value;
68
                break;
69 19
70 19
            case EntityManager::OPT_BOOLEAN_FALSE:
71 19
                $this->booleanFalse = $value;
72
                break;
73 22
        }
74
        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 191
     */
83
    public function escapeIdentifier($identifier)
84 191
    {
85 191
        $quote = $this->quotingCharacter;
86 191
        $divider = $this->identifierDivider;
87
        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 184
     */
97
    public function escapeValue($value)
98 184
    {
99 184
        $type   = is_object($value) ? get_class($value) : gettype($value);
100
        $method = [ $this, 'escape' . ucfirst($type) ];
101 184
102 183
        if (is_callable($method)) {
103
            return call_user_func($method, $value);
104 1
        } else {
105
            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 1
     * @throws Exception
116
     */
117 1
    public function describe($table)
0 ignored issues
show
Unused Code introduced by
The parameter $table is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

117
    public function describe(/** @scrutinizer ignore-unused */ $table)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

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