Completed
Push — master ( 256198...3ea60c )
by Jared
01:48
created

DatabaseDriver::prefixWhere()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 19
rs 9.2
c 0
b 0
f 0
cc 4
eloc 11
nc 4
nop 2
1
<?php
2
3
/**
4
 * @author Jared King <[email protected]>
5
 *
6
 * @see http://jaredtking.com
7
 *
8
 * @copyright 2015 Jared King
9
 * @license MIT
10
 */
11
12
namespace Pulsar\Driver;
13
14
use JAQB\QueryBuilder;
15
use PDOException;
16
use PDOStatement;
17
use Pimple\Container;
18
use Pulsar\Exception\DriverException;
19
use Pulsar\Model;
20
use Pulsar\Query;
21
22
/**
23
 * Driver for storing models in a database using PDO.
24
 */
25
class DatabaseDriver implements DriverInterface
26
{
27
    /**
28
     * @var QueryBuilder
29
     */
30
    private $connection;
31
32
    /**
33
     * @var Container
34
     */
35
    private $container;
36
37
    /**
38
     * Sets the database connection.
39
     *
40
     * @param QueryBuilder $db
41
     *
42
     * @return self
43
     */
44
    public function setConnection(QueryBuilder $db)
45
    {
46
        $this->connection = $db;
47
48
        return $this;
49
    }
50
51
    /**
52
     * Gets the database connection.
53
     *
54
     * @throws DriverException when the connection has not been set yet
55
     *
56
     * @return QueryBuilder
57
     */
58
    public function getConnection()
59
    {
60
        if (!$this->connection && $this->container) {
61
            $this->connection = $this->container['db'];
62
        }
63
64
        if (!$this->connection) {
65
            throw new DriverException('The database driver has not been given a connection!');
66
        }
67
68
        return $this->connection;
69
    }
70
71
    /**
72
     * @deprecated
73
     *
74
     * Sets the DI container
75
     *
76
     * @param Container $container
77
     *
78
     * @return $this
79
     */
80
    public function setContainer(Container $container)
81
    {
82
        $this->container = $container;
83
84
        return $this;
85
    }
86
87
    /**
88
     * @deprecated
89
     *
90
     * Gets the DI container
91
     *
92
     * @return Container
93
     */
94
    public function getContainer()
95
    {
96
        return $this->container;
97
    }
98
99 View Code Duplication
    public function createModel(Model $model, array $parameters)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
100
    {
101
        $values = $this->serialize($parameters);
102
        $tablename = $model->getTablename();
103
        $db = $this->getConnection();
104
105
        try {
106
            return $db->insert($values)
107
                      ->into($tablename)
108
                      ->execute() instanceof PDOStatement;
109
        } catch (PDOException $original) {
110
            $e = new DriverException('An error occurred in the database driver when creating the '.$model::modelName().': '.$original->getMessage());
111
            $e->setException($original);
112
            throw $e;
113
        }
114
    }
115
116
    public function getCreatedID(Model $model, $propertyName)
117
    {
118
        try {
119
            $id = $this->getConnection()->lastInsertId();
120
        } catch (PDOException $original) {
121
            $e = new DriverException('An error occurred in the database driver when getting the ID of the new '.$model::modelName().': '.$original->getMessage());
122
            $e->setException($original);
123
            throw $e;
124
        }
125
126
        return Model::cast($model::getProperty($propertyName), $id);
0 ignored issues
show
Bug introduced by
It seems like $model::getProperty($propertyName) targeting Pulsar\Model::getProperty() can also be of type null; however, Pulsar\Model::cast() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
127
    }
128
129 View Code Duplication
    public function loadModel(Model $model)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
130
    {
131
        $tablename = $model->getTablename();
132
        $db = $this->getConnection();
133
134
        try {
135
            $row = $db->select('*')
136
                      ->from($tablename)
137
                      ->where($model->ids())
138
                      ->one();
139
        } catch (PDOException $original) {
140
            $e = new DriverException('An error occurred in the database driver when loading an instance of '.$model::modelName().': '.$original->getMessage());
141
            $e->setException($original);
142
            throw $e;
143
        }
144
145
        if (!is_array($row)) {
146
            return false;
147
        }
148
149
        return $this->unserialize($row, $model::getProperties());
150
    }
151
152 View Code Duplication
    public function updateModel(Model $model, array $parameters)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
153
    {
154
        if (count($parameters) == 0) {
155
            return true;
156
        }
157
158
        $values = $this->serialize($parameters);
159
        $tablename = $model->getTablename();
160
        $db = $this->getConnection();
161
162
        try {
163
            return $db->update($tablename)
164
                      ->values($values)
165
                      ->where($model->ids())
166
                      ->execute() instanceof PDOStatement;
167
        } catch (PDOException $original) {
168
            $e = new DriverException('An error occurred in the database driver when updating the '.$model::modelName().': '.$original->getMessage());
169
            $e->setException($original);
170
            throw $e;
171
        }
172
    }
173
174 View Code Duplication
    public function deleteModel(Model $model)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
175
    {
176
        $tablename = $model->getTablename();
177
        $db = $this->getConnection();
178
179
        try {
180
            return $db->delete($tablename)
181
                      ->where($model->ids())
182
                      ->execute() instanceof PDOStatement;
183
        } catch (PDOException $original) {
184
            $e = new DriverException('An error occurred in the database driver while deleting the '.$model::modelName().': '.$original->getMessage());
185
            $e->setException($original);
186
            throw $e;
187
        }
188
    }
189
190
    public function queryModels(Query $query)
191
    {
192
        $modelClass = $query->getModel();
193
        $model = new $modelClass();
194
        $tablename = $model->getTablename();
195
196
        // build a DB query from the model query
197
        $dbQuery = $this->getConnection()
198
            ->select($this->prefixSelect('*', $tablename))
199
            ->from($tablename)
200
            ->where($this->prefixWhere($query->getWhere(), $tablename))
201
            ->limit($query->getLimit(), $query->getStart())
202
            ->orderBy($this->prefixSort($query->getSort(), $tablename));
203
204
        // join conditions
205
        foreach ($query->getJoins() as $join) {
206
            list($foreignModelClass, $column, $foreignKey) = $join;
207
208
            $foreignModel = new $foreignModelClass();
209
            $foreignTablename = $foreignModel->getTablename();
210
            $condition = $this->prefixColumn($column, $tablename).'='.$this->prefixColumn($foreignKey, $foreignTablename);
211
212
            $dbQuery->join($foreignTablename, $condition);
213
        }
214
215
        try {
216
            $data = $dbQuery->all();
217
        } catch (PDOException $original) {
218
            $e = new DriverException('An error occurred in the database driver while performing the '.$model::modelName().' query: '.$original->getMessage());
219
            $e->setException($original);
220
            throw $e;
221
        }
222
223
        $properties = $model::getProperties();
224
        foreach ($data as &$row) {
225
            $row = $this->unserialize($row, $properties);
226
        }
227
228
        return $data;
229
    }
230
231
    public function totalRecords(Query $query)
232
    {
233
        $modelClass = $query->getModel();
234
        $model = new $modelClass();
235
        $tablename = $model->getTablename();
236
        $db = $this->getConnection();
237
238
        try {
239
            return (int) $db->select('count(*)')
240
                            ->from($tablename)
241
                            ->where($query->getWhere())
242
                            ->scalar();
243
        } catch (PDOException $original) {
244
            $e = new DriverException('An error occurred in the database driver while getting the number of '.$model::modelName().' objects: '.$original->getMessage());
245
            $e->setException($original);
246
            throw $e;
247
        }
248
    }
249
250 View Code Duplication
    public function startTransaction(Model $model)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
251
    {
252
        try {
253
            return $this->getConnection()->beginTransaction();
254
        } catch (PDOException $original) {
255
            $e = new DriverException('An error occurred in the database driver while starting a transaction: '.$original->getMessage());
256
            $e->setException($original);
257
            throw $e;
258
        }
259
    }
260
261 View Code Duplication
    public function commitTransaction(Model $model)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
262
    {
263
        try {
264
            return $this->getConnection()->commit();
265
        } catch (PDOException $original) {
266
            $e = new DriverException('An error occurred in the database driver while committing a transaction: '.$original->getMessage());
267
            $e->setException($original);
268
            throw $e;
269
        }
270
    }
271
272 View Code Duplication
    public function rollBackTransaction(Model $model)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
273
    {
274
        try {
275
            return $this->getConnection()->rollBack();
276
        } catch (PDOException $original) {
277
            $e = new DriverException('An error occurred in the database driver while rolling back a transaction: '.$original->getMessage());
278
            $e->setException($original);
279
            throw $e;
280
        }
281
    }
282
283
    /**
284
     * Marshals a value to storage.
285
     *
286
     * @param mixed $value
287
     *
288
     * @return mixed serialized value
289
     */
290
    public function serializeValue($value)
291
    {
292
        // encode arrays/objects as JSON
293
        if (is_array($value) || is_object($value)) {
294
            return json_encode($value);
295
        }
296
297
        return $value;
298
    }
299
300
    /**
301
     * Serializes an array of values.
302
     *
303
     * @param array $values
304
     *
305
     * @return array
306
     */
307
    private function serialize(array $values)
308
    {
309
        foreach ($values as &$value) {
310
            $value = $this->serializeValue($value);
311
        }
312
313
        return $values;
314
    }
315
316
    /**
317
     * Unserializes an array of values.
318
     *
319
     * @param array $values
320
     * @param array $properties model properties
321
     *
322
     * @return array
323
     */
324
    private function unserialize(array $values, array $properties)
325
    {
326
        foreach ($values as $k => &$value) {
327
            if (isset($properties[$k])) {
328
                $value = Model::cast($properties[$k], $value);
329
            }
330
        }
331
332
        return $values;
333
    }
334
335
    /**
336
     * Returns a prefixed select statement.
337
     *
338
     * @param string $columns
339
     * @param string $tablename
340
     *
341
     * @return string
342
     */
343
    private function prefixSelect($columns, $tablename)
344
    {
345
        $prefixed = [];
346
        foreach (explode(',', $columns) as $column) {
347
            $prefixed[] = $this->prefixColumn($column, $tablename);
348
        }
349
350
        return implode(',', $prefixed);
351
    }
352
353
    /**
354
     * Returns a prefixed where statement.
355
     *
356
     * @param string $columns
0 ignored issues
show
Bug introduced by
There is no parameter named $columns. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
357
     * @param string $tablename
358
     *
359
     * @return array
360
     */
361
    private function prefixWhere(array $where, $tablename)
362
    {
363
        $return = [];
364
        foreach ($where as $key => $condition) {
365
            // handles $where[property] = value
366
            if (!is_numeric($key)) {
367
                $return[$this->prefixColumn($key, $tablename)] = $condition;
368
            // handles $where[] = [property, value, '=']
0 ignored issues
show
Unused Code Comprehensibility introduced by
45% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
369
            } elseif (is_array($condition)) {
370
                $condition[0] = $this->prefixColumn($condition[0], $tablename);
371
                $return[] = $condition;
372
            // handles raw SQL - do nothing
373
            } else {
374
                $return[] = $condition;
375
            }
376
        }
377
378
        return $return;
379
    }
380
381
    /**
382
     * Returns a prefixed sort statement.
383
     *
384
     * @param string $columns
0 ignored issues
show
Bug introduced by
There is no parameter named $columns. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
385
     * @param string $tablename
386
     *
387
     * @return array
388
     */
389
    private function prefixSort(array $sort, $tablename)
390
    {
391
        foreach ($sort as &$condition) {
392
            $condition[0] = $this->prefixColumn($condition[0], $tablename);
393
        }
394
395
        return $sort;
396
    }
397
398
    /**
399
     * Prefix columns with tablename that contains only
400
     * alphanumeric/underscores/*.
401
     *
402
     * @param string $column
403
     * @param string $tablename
404
     *
405
     * @return string prefixed column
406
     */
407
    private function prefixColumn($column, $tablename)
408
    {
409
        if ($column === '*' || preg_match('/^[a-z0-9_]+$/i', $column)) {
410
            return "$tablename.$column";
411
        }
412
413
        return $column;
414
    }
415
}
416