Passed
Pull Request — master (#49)
by Thomas
02:17
created

Dbal::escapeDouble()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

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