Completed
Push — master ( d1e5f9...153f8a )
by Jared
01:42
created

DatabaseDriver::commitTransaction()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 7

Duplication

Lines 10
Ratio 100 %

Importance

Changes 0
Metric Value
dl 10
loc 10
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 7
nc 2
nop 1
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
    /**
251
     * Marshals a value to storage.
252
     *
253
     * @param mixed $value
254
     *
255
     * @return mixed serialized value
256
     */
257
    public function serializeValue($value)
258
    {
259
        // encode arrays/objects as JSON
260
        if (is_array($value) || is_object($value)) {
261
            return json_encode($value);
262
        }
263
264
        return $value;
265
    }
266
267
    /**
268
     * Serializes an array of values.
269
     *
270
     * @param array $values
271
     *
272
     * @return array
273
     */
274
    private function serialize(array $values)
275
    {
276
        foreach ($values as &$value) {
277
            $value = $this->serializeValue($value);
278
        }
279
280
        return $values;
281
    }
282
283
    /**
284
     * Unserializes an array of values.
285
     *
286
     * @param array $values
287
     * @param array $properties model properties
288
     *
289
     * @return array
290
     */
291
    private function unserialize(array $values, array $properties)
292
    {
293
        foreach ($values as $k => &$value) {
294
            if (isset($properties[$k])) {
295
                $value = Model::cast($properties[$k], $value);
296
            }
297
        }
298
299
        return $values;
300
    }
301
302
    /**
303
     * Returns a prefixed select statement.
304
     *
305
     * @param string $columns
306
     * @param string $tablename
307
     *
308
     * @return string
309
     */
310
    private function prefixSelect($columns, $tablename)
311
    {
312
        $prefixed = [];
313
        foreach (explode(',', $columns) as $column) {
314
            $prefixed[] = $this->prefixColumn($column, $tablename);
315
        }
316
317
        return implode(',', $prefixed);
318
    }
319
320
    /**
321
     * Returns a prefixed where statement.
322
     *
323
     * @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...
324
     * @param string $tablename
325
     *
326
     * @return array
327
     */
328
    private function prefixWhere(array $where, $tablename)
329
    {
330
        $return = [];
331
        foreach ($where as $key => $condition) {
332
            // handles $where[property] = value
333
            if (!is_numeric($key)) {
334
                $return[$this->prefixColumn($key, $tablename)] = $condition;
335
            // 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...
336
            } elseif (is_array($condition)) {
337
                $condition[0] = $this->prefixColumn($condition[0], $tablename);
338
                $return[] = $condition;
339
            // handles raw SQL - do nothing
340
            } else {
341
                $return[] = $condition;
342
            }
343
        }
344
345
        return $return;
346
    }
347
348
    /**
349
     * Returns a prefixed sort statement.
350
     *
351
     * @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...
352
     * @param string $tablename
353
     *
354
     * @return array
355
     */
356
    private function prefixSort(array $sort, $tablename)
357
    {
358
        foreach ($sort as &$condition) {
359
            $condition[0] = $this->prefixColumn($condition[0], $tablename);
360
        }
361
362
        return $sort;
363
    }
364
365
    /**
366
     * Prefix columns with tablename that contains only
367
     * alphanumeric/underscores/*.
368
     *
369
     * @param string $column
370
     * @param string $tablename
371
     *
372
     * @return string prefixed column
373
     */
374
    private function prefixColumn($column, $tablename)
375
    {
376
        if ($column === '*' || preg_match('/^[a-z0-9_]+$/i', $column)) {
377
            return "$tablename.$column";
378
        }
379
380
        return $column;
381
    }
382
}
383