Completed
Pull Request — master (#53)
by Thomas
01:48
created

Dbal::escapeIdentifier()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 5
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
    use Escaping;
20
21
    /** @var array */
22
    protected static $typeMapping = [];
23
24
    protected static $compositeWhereInTemplate = '(%s) IN (VALUES %s)';
25
26
    /** @var EntityManager */
27
    protected $entityManager;
28
29
    /**
30
     * Dbal constructor.
31
     *
32
     * @param EntityManager $entityManager
33
     * @param array         $options
34
     */
35
    public function __construct(EntityManager $entityManager, array $options = [])
36
    {
37
        $this->entityManager = $entityManager;
38 731
39
        foreach ($options as $option => $value) {
40 731
            $this->setOption($option, $value);
41
        }
42 731
    }
43 4
44
    /**
45 731
     * Set $option to $value
46
     *
47
     * @param string $option
48
     * @param mixed  $value
49
     * @return static
50
     */
51
    public function setOption($option, $value)
52
    {
53
        switch ($option) {
54 22
            case EntityManager::OPT_IDENTIFIER_DIVIDER:
55
                $this->identifierDivider = $value;
56
                break;
57 22
58 1
            case EntityManager::OPT_QUOTING_CHARACTER:
59 1
                $this->quotingCharacter = $value;
60
                break;
61 21
62 1
            case EntityManager::OPT_BOOLEAN_TRUE:
63 1
                $this->booleanTrue = $value;
64
                break;
65 20
66 19
            case EntityManager::OPT_BOOLEAN_FALSE:
67 19
                $this->booleanFalse = $value;
68
                break;
69 19
        }
70 19
        return $this;
71 19
    }
72
73 22
    /**
74
     * Returns $identifier quoted for use in a sql statement
75
     *
76
     * @param string $identifier Identifier to quote
77
     * @return string
78
     */
79
    public function escapeIdentifier($identifier)
80
    {
81
        $quote = $this->quotingCharacter;
82 191
        $divider = $this->identifierDivider;
83
        return $quote . str_replace($divider, $quote . $divider . $quote, $identifier) . $quote;
84 191
    }
85 191
86 191
    /**
87
     * Returns $value formatted to use in a sql statement.
88
     *
89
     * @param  mixed $value The variable that should be returned in SQL syntax
90
     * @return string
91
     * @throws NotScalar
92
     */
93
    public function escapeValue($value)
94
    {
95
        $type   = is_object($value) ? get_class($value) : gettype($value);
96 184
        $method = [ $this, 'escape' . ucfirst($type) ];
97
98 184
        if (is_callable($method)) {
99 184
            return call_user_func($method, $value);
100
        } else {
101 184
            throw new NotScalar('$value has to be scalar data type. ' . gettype($value) . ' given');
102 183
        }
103
    }
104 1
105
    /**
106
     * Describe a table
107
     *
108
     * @param string $table
109
     * @return Table|Column[]
110
     * @throws UnsupportedDriver
111
     * @throws Exception
112
     */
113
    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

113
    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...
114
    {
115 1
        throw new UnsupportedDriver('Not supported for this driver');
116
    }
117 1
118
    /**
119
     * @param Entity[] $entities
120
     * @return bool
121
     * @throws Exception\InvalidArgument
122
     */
123
    protected static function assertSameType(array $entities)
124
    {
125
        if (count($entities) < 2) {
126
            return true;
127
        }
128 2
129
        $type = get_class(reset($entities));
130 2
        foreach ($entities as $i => $entity) {
131
            if (get_class($entity) !== $type) {
132 2
                throw new Exception\InvalidArgument(sprintf('$entities[%d] is not from the same type', $i));
133 1
            }
134
        }
135
136 1
        return true;
137 1
    }
138
139
    /**
140
     * Insert $entities into database
141
     *
142
     * The entities have to be from same type otherwise a InvalidArgument will be thrown.
143
     *
144
     * @param Entity ...$entities
145
     * @return bool
146
     * @throws Exception\InvalidArgument
147 6
     */
148
    public function insert(Entity ...$entities)
149 6
    {
150 6
        if (count($entities) === 0) {
151
            return false;
152 6
        }
153 6
        static::assertSameType($entities);
154 6
        $insert = $this->buildInsertStatement(...$entities);
155 6
        $this->entityManager->getConnection()->query($insert);
156 6
        return true;
157 6
    }
158
159
    /**
160
     * Insert $entities and update with default values from database
161 6
     *
162 6
     * The entities have to be from same type otherwise a InvalidArgument will be thrown.
163 6
     *
164
     * @param Entity ...$entities
165
     * @return bool
166 6
     * @throws Exception\InvalidArgument
167 6
     */
168 6
    public function insertAndSync(Entity ...$entities)
169 6
    {
170
        if (count($entities) === 0) {
171 3
            return false;
172
        }
173
        self::assertSameType($entities);
174
        $this->insert(...$entities);
175
        $this->syncInserted(...$entities);
176
        return true;
177
    }
178
179
    /**
180
     * Insert $entities and sync with auto increment primary key
181
     *
182 6
     * The entities have to be from same type otherwise a InvalidArgument will be thrown.
183
     *
184 6
     * @param Entity ...$entities
185 6
     * @return int|bool
186 6
     * @throws UnsupportedDriver
187 6
     * @throws Exception\InvalidArgument
188 6
     */
189
    public function insertAndSyncWithAutoInc(Entity ...$entities)
190
    {
191 6
        if (count($entities) === 0) {
192 6
            return false;
193 6
        }
194
        self::assertSameType($entities);
195 4
        throw new UnsupportedDriver('Auto incremented column for this driver is not supported');
196
    }
197
198
    /**
199
     * Update $entity in database and returns success
200
     *
201
     * @param Entity $entity
202
     * @return bool
203
     * @internal
204 12
     */
205
    public function update(Entity $entity)
206 12
    {
207
        $data       = $entity->getData();
208 12
        $primaryKey = $entity->getPrimaryKey();
209 12
210 11
        $where = [];
211 12
        foreach ($primaryKey as $attribute => $value) {
212
            $col     = $entity::getColumnName($attribute);
213
            $where[] = $this->escapeIdentifier($col) . ' = ' . $this->escapeValue($value);
214
            if (isset($data[$col])) {
215 12
                unset($data[$col]);
216 12
            }
217 11
        }
218 12
219
        $set = [];
220
        foreach ($data as $col => $value) {
221
            $set[] = $this->escapeIdentifier($col) . ' = ' . $this->escapeValue($value);
222 12
        }
223 12
224
        $statement = 'UPDATE ' . $this->escapeIdentifier($entity::getTableName()) . ' ' .
225 12
                     'SET ' . implode(',', $set) . ' ' .
226
                     'WHERE ' . implode(' AND ', $where);
227
        $this->entityManager->getConnection()->query($statement);
228
229
        return $this->entityManager->sync($entity, true);
230
    }
231
232
    /**
233
     * Delete $entity from database
234 3
     *
235
     * This method does not delete from the map - you can still receive the entity via fetch.
236 3
     *
237 3
     * @param Entity $entity
238
     * @return bool
239 3
     */
240 3
    public function delete(Entity $entity)
241 3
    {
242
        $primaryKey = $entity->getPrimaryKey();
243
        $where      = [];
244
        foreach ($primaryKey as $attribute => $value) {
245
            $col     = $entity::getColumnName($attribute);
246
            $where[] = $this->escapeIdentifier($col) . ' = ' . $this->escapeValue($value);
247
        }
248
249
        $statement = 'DELETE FROM ' . $this->escapeIdentifier($entity::getTableName()) . ' ' .
250
                     'WHERE ' . implode(' AND ', $where);
251 79
        $this->entityManager->getConnection()->query($statement);
252
253 79
        return true;
254
    }
255 79
256 33
    /**
257
     * Build the insert statement for $entity
258
     *
259 79
     * @param Entity $entity
260
     * @param Entity[] $entities
261
     * @return string
262
     */
263
    protected function buildInsertStatement(Entity $entity, Entity ...$entities)
264
    {
265
        array_unshift($entities, $entity);
266
        $cols = [];
267
        $rows = [];
268 24
        foreach ($entities as $entity) {
269
            $data = $entity->getData();
270 24
            $cols = array_unique(array_merge($cols, array_keys($data)));
271 16
            $rows[] = $data;
272
        }
273
274 8
        $cols = array_combine($cols, array_map([$this, 'escapeIdentifier'], $cols));
275
276
        $statement = 'INSERT INTO ' . $this->escapeIdentifier($entity::getTableName()) . ' ' .
277
                     '(' . implode(',', $cols) . ') VALUES ';
278
279
        $statement .= implode(',', array_map(function ($values) use ($cols) {
280
            $result = [];
281
            foreach ($cols as $key => $col) {
282
                $result[] = isset($values[$key]) ? $this->escapeValue($values[$key]) : $this->escapeNULL();
283 121
            }
284
            return '(' . implode(',', $result) . ')';
285 121
        }, $rows));
286
287
        return $statement;
288
    }
289
290
    /**
291
     * Update the autoincrement value
292
     *
293
     * @param Entity     $entity
294 51
     * @param int|string $value
295
     */
296 51
    protected function updateAutoincrement(Entity $entity, $value)
297
    {
298
        $var    = $entity::getPrimaryKeyVars()[0];
299
        $column = $entity::getColumnName($var);
300
301
        $entity->setOriginalData(array_merge($entity->getData(), [ $column => $value ]));
302
        $entity->__set($var, $value);
303
    }
304
305 4
    /**
306
     * Sync the $entities after insert
307 4
     *
308
     * @param Entity ...$entities
309
     */
310
    protected function syncInserted(Entity ...$entities)
311
    {
312
        $entity = reset($entities);
313
        $vars = $entity::getPrimaryKeyVars();
314
        $cols = array_map([$entity, 'getColumnName'], $vars);
315 1
        $primary = array_combine($vars, $cols);
316
317 1
        $query = "SELECT * FROM " . $this->escapeIdentifier($entity::getTableName()) . " WHERE ";
318
        $query .= count($cols) > 1 ? $this->buildCompositeWhereInStatement($cols, $entities) :
319
            $this->escapeIdentifier($cols[0]) . ' IN (' . implode(',', array_map(function (Entity $entity) {
320
                return $this->escapeValue(array_values($entity->getPrimaryKey())[0]);
321
            }, $entities)) . ')';
322
323
        $statement = $this->entityManager->getConnection()->query($query);
324
        $left = $entities;
325
        while ($row = $statement->fetch(\PDO::FETCH_ASSOC)) {
326 20
            foreach ($left as $k => $entity) {
327
                foreach ($primary as $var => $col) {
328 20
                    if ($entity->$var != $row[$col]) {
329
                        continue 2;
330
                    }
331
                }
332
333
                $this->entityManager->map($entity, true);
334
                $entity->setOriginalData($row);
335
                $entity->reset();
336
                unset($left[$k]);
337 1
                break;
338
            }
339 1
        }
340 1
    }
341
342
    /**
343
     * Build a where in statement for composite primary keys
344
     *
345
     * @param array $cols
346
     * @param array $entities
347
     * @return string
348
     */
349
    protected function buildCompositeWhereInStatement(array $cols, array $entities)
350
    {
351
        $primaryKeys = [];
352
        foreach ($entities as $entity) {
353
            $pKey = array_map([$this, 'escapeValue'], $entity->getPrimaryKey());
354
            $primaryKeys[] = count($cols) > 1 ? '(' . implode(',', $pKey) . ')' : reset($pKey);
355
        }
356
357
        return sprintf(
358
            static::$compositeWhereInTemplate,
359
            implode(',', array_map([$this, 'escapeIdentifier'], $cols)),
360
            implode(',', $primaryKeys) . ')'
361
        );
362
    }
363
364
    /**
365
     * Normalize $type
366
     *
367
     * The type returned by mysql is for example VARCHAR(20) - this function converts it to varchar
368
     *
369
     * @param string $type
370
     * @return string
371
     */
372
    protected function normalizeType($type)
373
    {
374
        $type = strtolower($type);
375
376
        if (($pos = strpos($type, '(')) !== false && $pos > 0) {
377
            $type = substr($type, 0, $pos);
378
        }
379
380
        return trim($type);
381
    }
382
}
383