Passed
Pull Request — master (#49)
by Thomas
03:25 queued 01:18
created

Dbal::describe()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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