Completed
Push — master ( fdbdf8...009d3f )
by Jared
01:41
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\ConnectionManager;
15
use JAQB\Exception\JAQBException;
16
use JAQB\QueryBuilder;
17
use PDOException;
18
use PDOStatement;
19
use Pulsar\Exception\DriverException;
20
use Pulsar\Model;
21
use Pulsar\Query;
22
23
/**
24
 * Driver for storing models in a database using PDO.
25
 */
26
class DatabaseDriver implements DriverInterface
27
{
28
    /**
29
     * @var ConnectionManager
30
     */
31
    private $connections;
32
33
    /**
34
     * @var QueryBuilder
35
     */
36
    private $connection;
37
38
    /**
39
     * Sets the connection manager.
40
     *
41
     * @param ConnectionManager $manager
42
     *
43
     * @return self
44
     */
45
    public function setConnectionManager(ConnectionManager $manager)
46
    {
47
        $this->connections = $manager;
48
49
        return $this;
50
    }
51
52
    /**
53
     * Gets the connection manager.
54
     *
55
     * @return ConnectionManager
56
     */
57
    public function getConnectionManager()
58
    {
59
        return $this->connections;
60
    }
61
62
    /**
63
     * Sets the default database connection.
64
     *
65
     * @param QueryBuilder $db
66
     *
67
     * @return self
68
     */
69
    public function setConnection(QueryBuilder $db)
70
    {
71
        $this->connection = $db;
72
73
        return $this;
74
    }
75
76
    /**
77
     * Gets the database connection.
78
     *
79
     * @param string|false $id connection ID
80
     *
81
     * @throws DriverException when the connection has not been set yet
82
     *
83
     * @return QueryBuilder
84
     */
85
    public function getConnection($id)
86
    {
87
        if ($this->connections) {
88
            try {
89
                if ($id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
90
                    return $this->connections->get($id);
91
                } else {
92
                    return $this->connections->getDefault();
93
                }
94
            } catch (JAQBException $e) {
95
                throw new DriverException($e->getMessage());
96
            }
97
        }
98
99
        if (!$this->connection) {
100
            throw new DriverException('The database driver has not been given a connection!');
101
        }
102
103
        return $this->connection;
104
    }
105
106 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...
107
    {
108
        $values = $this->serialize($parameters);
109
        $tablename = $model->getTablename();
110
        $db = $this->getConnection($model->getConnection());
111
112
        try {
113
            return $db->insert($values)
114
                      ->into($tablename)
115
                      ->execute() instanceof PDOStatement;
116
        } catch (PDOException $original) {
117
            $e = new DriverException('An error occurred in the database driver when creating the '.$model::modelName().': '.$original->getMessage());
118
            $e->setException($original);
119
            throw $e;
120
        }
121
    }
122
123
    public function getCreatedID(Model $model, $propertyName)
124
    {
125
        try {
126
            $id = $this->getConnection($model->getConnection())->lastInsertId();
127
        } catch (PDOException $original) {
128
            $e = new DriverException('An error occurred in the database driver when getting the ID of the new '.$model::modelName().': '.$original->getMessage());
129
            $e->setException($original);
130
            throw $e;
131
        }
132
133
        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...
134
    }
135
136
    public function loadModel(Model $model)
137
    {
138
        $tablename = $model->getTablename();
139
        $db = $this->getConnection($model->getConnection());
140
141
        try {
142
            $row = $db->select('*')
143
                      ->from($tablename)
144
                      ->where($model->ids())
145
                      ->one();
146
        } catch (PDOException $original) {
147
            $e = new DriverException('An error occurred in the database driver when loading an instance of '.$model::modelName().': '.$original->getMessage());
148
            $e->setException($original);
149
            throw $e;
150
        }
151
152
        if (!is_array($row)) {
153
            return false;
154
        }
155
156
        return $row;
157
    }
158
159
    public function updateModel(Model $model, array $parameters)
160
    {
161
        if (count($parameters) == 0) {
162
            return true;
163
        }
164
165
        $values = $this->serialize($parameters);
166
        $tablename = $model->getTablename();
167
        $db = $this->getConnection($model->getConnection());
168
169
        try {
170
            return $db->update($tablename)
171
                      ->values($values)
172
                      ->where($model->ids())
173
                      ->execute() instanceof PDOStatement;
174
        } catch (PDOException $original) {
175
            $e = new DriverException('An error occurred in the database driver when updating the '.$model::modelName().': '.$original->getMessage());
176
            $e->setException($original);
177
            throw $e;
178
        }
179
    }
180
181 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...
182
    {
183
        $tablename = $model->getTablename();
184
        $db = $this->getConnection($model->getConnection());
185
186
        try {
187
            return $db->delete($tablename)
188
                      ->where($model->ids())
189
                      ->execute() instanceof PDOStatement;
190
        } catch (PDOException $original) {
191
            $e = new DriverException('An error occurred in the database driver while deleting the '.$model::modelName().': '.$original->getMessage());
192
            $e->setException($original);
193
            throw $e;
194
        }
195
    }
196
197
    public function queryModels(Query $query)
198
    {
199
        $modelClass = $query->getModel();
200
        $model = new $modelClass();
201
        $tablename = $model->getTablename();
202
203
        // build a DB query from the model query
204
        $dbQuery = $this->getConnection($model->getConnection())
205
            ->select($this->prefixSelect('*', $tablename))
206
            ->from($tablename)
207
            ->where($this->prefixWhere($query->getWhere(), $tablename))
208
            ->limit($query->getLimit(), $query->getStart())
209
            ->orderBy($this->prefixSort($query->getSort(), $tablename));
210
211
        // join conditions
212
        foreach ($query->getJoins() as $join) {
213
            list($foreignModelClass, $column, $foreignKey) = $join;
214
215
            $foreignModel = new $foreignModelClass();
216
            $foreignTablename = $foreignModel->getTablename();
217
            $condition = $this->prefixColumn($column, $tablename).'='.$this->prefixColumn($foreignKey, $foreignTablename);
218
219
            $dbQuery->join($foreignTablename, $condition);
220
        }
221
222
        try {
223
            return $dbQuery->all();
224
        } catch (PDOException $original) {
225
            $e = new DriverException('An error occurred in the database driver while performing the '.$model::modelName().' query: '.$original->getMessage());
226
            $e->setException($original);
227
            throw $e;
228
        }
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($model->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
     * Returns a prefixed select statement.
285
     *
286
     * @param string $columns
287
     * @param string $tablename
288
     *
289
     * @return string
290
     */
291
    private function prefixSelect($columns, $tablename)
292
    {
293
        $prefixed = [];
294
        foreach (explode(',', $columns) as $column) {
295
            $prefixed[] = $this->prefixColumn($column, $tablename);
296
        }
297
298
        return implode(',', $prefixed);
299
    }
300
301
    /**
302
     * Returns a prefixed where statement.
303
     *
304
     * @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...
305
     * @param string $tablename
306
     *
307
     * @return array
308
     */
309
    private function prefixWhere(array $where, $tablename)
310
    {
311
        $return = [];
312
        foreach ($where as $key => $condition) {
313
            // handles $where[property] = value
314
            if (!is_numeric($key)) {
315
                $return[$this->prefixColumn($key, $tablename)] = $condition;
316
            // 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...
317
            } elseif (is_array($condition)) {
318
                $condition[0] = $this->prefixColumn($condition[0], $tablename);
319
                $return[] = $condition;
320
            // handles raw SQL - do nothing
321
            } else {
322
                $return[] = $condition;
323
            }
324
        }
325
326
        return $return;
327
    }
328
329
    /**
330
     * Returns a prefixed sort statement.
331
     *
332
     * @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...
333
     * @param string $tablename
334
     *
335
     * @return array
336
     */
337
    private function prefixSort(array $sort, $tablename)
338
    {
339
        foreach ($sort as &$condition) {
340
            $condition[0] = $this->prefixColumn($condition[0], $tablename);
341
        }
342
343
        return $sort;
344
    }
345
346
    /**
347
     * Prefix columns with tablename that contains only
348
     * alphanumeric/underscores/*.
349
     *
350
     * @param string $column
351
     * @param string $tablename
352
     *
353
     * @return string prefixed column
354
     */
355
    private function prefixColumn($column, $tablename)
356
    {
357
        if ($column === '*' || preg_match('/^[a-z0-9_]+$/i', $column)) {
358
            return "$tablename.$column";
359
        }
360
361
        return $column;
362
    }
363
}
364