Completed
Push — master ( a90e9d...bdf610 )
by Jared
06:48
created

DatabaseDriver::unserializeValue()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 20
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 10
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 ICanBoogie\Inflector;
15
use PDOException;
16
use PDOStatement;
17
use Pimple\Container;
18
use Pulsar\Exception\DriverException;
19
use Pulsar\Model;
20
use Pulsar\Property;
21
use Pulsar\Query;
22
23
/**
24
 * Class DatabaseDriver.
25
 */
26
class DatabaseDriver implements DriverInterface
27
{
28
    /**
29
     * @var Container
30
     */
31
    private $app;
32
33
    /**
34
     * @param Container $app
35
     */
36
    public function __construct(Container $app = null)
37
    {
38
        $this->app = $app;
39
    }
40
41 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...
42
    {
43
        $values = $this->serialize($parameters);
44
        $tablename = $this->getTablename($model);
45
        $db = $this->app['db'];
46
47
        try {
48
            return $db->insert($values)
49
                      ->into($tablename)
50
                      ->execute() instanceof PDOStatement;
51
        } catch (PDOException $original) {
52
            $e = new DriverException('An error occurred in the database driver when creating the '.$model::modelName().': '.$original->getMessage());
53
            $e->setException($original);
54
            throw $e;
55
        }
56
    }
57
58
    public function getCreatedID(Model $model, $propertyName)
59
    {
60
        try {
61
            $id = $this->app['db']->getPDO()->lastInsertId();
62
        } catch (PDOException $original) {
63
            $e = new DriverException('An error occurred in the database driver when getting the ID of the new '.$model::modelName().': '.$original->getMessage());
64
            $e->setException($original);
65
            throw $e;
66
        }
67
68
        return $this->unserializeValue($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\Driver\DatabaseDriver::unserializeValue() 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...
69
    }
70
71 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...
72
    {
73
        $tablename = $this->getTablename($model);
74
        $db = $this->app['db'];
75
76
        try {
77
            $row = $db->select('*')
78
                      ->from($tablename)
79
                      ->where($model->ids())
80
                      ->one();
81
        } catch (PDOException $original) {
82
            $e = new DriverException('An error occurred in the database driver when loading an instance of '.$model::modelName().': '.$original->getMessage());
83
            $e->setException($original);
84
            throw $e;
85
        }
86
87
        if (!is_array($row)) {
88
            return false;
89
        }
90
91
        return $this->unserialize($row, $model::getProperties());
92
    }
93
94 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...
95
    {
96
        if (count($parameters) == 0) {
97
            return true;
98
        }
99
100
        $values = $this->serialize($parameters);
101
        $tablename = $this->getTablename($model);
102
        $db = $this->app['db'];
103
104
        try {
105
            return $db->update($tablename)
106
                      ->values($values)
107
                      ->where($model->ids())
108
                      ->execute() instanceof PDOStatement;
109
        } catch (PDOException $original) {
110
            $e = new DriverException('An error occurred in the database driver when updating the '.$model::modelName().': '.$original->getMessage());
111
            $e->setException($original);
112
            throw $e;
113
        }
114
    }
115
116 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...
117
    {
118
        $tablename = $this->getTablename($model);
119
        $db = $this->app['db'];
120
121
        try {
122
            return $db->delete($tablename)
123
                      ->where($model->ids())
124
                      ->execute() instanceof PDOStatement;
125
        } catch (PDOException $original) {
126
            $e = new DriverException('An error occurred in the database driver while deleting the '.$model::modelName().': '.$original->getMessage());
127
            $e->setException($original);
128
            throw $e;
129
        }
130
    }
131
132
    public function queryModels(Query $query)
133
    {
134
        $model = $query->getModel();
135
        $tablename = $this->getTablename($model);
136
137
        // build a DB query from the model query
138
        $dbQuery = $this->app['db']
139
            ->select($this->prefixSelect('*', $tablename))
140
            ->from($tablename)
141
            ->where($this->prefixWhere($query->getWhere(), $tablename))
142
            ->limit($query->getLimit(), $query->getStart())
143
            ->orderBy($this->prefixSort($query->getSort(), $tablename));
144
145
        // join conditions
146
        foreach ($query->getJoins() as $join) {
147
            list($foreignModel, $column, $foreignKey) = $join;
148
149
            $foreignTablename = $this->getTablename($foreignModel);
150
            $condition = $this->prefixColumn($column, $tablename).'='.$this->prefixColumn($foreignKey, $foreignTablename);
151
152
            $dbQuery->join($foreignTablename, $condition);
153
        }
154
155
        try {
156
            $data = $dbQuery->all();
157
        } catch (PDOException $original) {
158
            $e = new DriverException('An error occurred in the database driver while performing the '.$model::modelName().' query: '.$original->getMessage());
159
            $e->setException($original);
160
            throw $e;
161
        }
162
163
        $properties = $model::getProperties();
164
        foreach ($data as &$row) {
165
            $row = $this->unserialize($row, $properties);
166
        }
167
168
        return $data;
169
    }
170
171 View Code Duplication
    public function totalRecords(Query $query)
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...
172
    {
173
        $model = $query->getModel();
174
        $tablename = $this->getTablename($model);
175
        $db = $this->app['db'];
176
177
        try {
178
            return (int) $db->select('count(*)')
179
                            ->from($tablename)
180
                            ->where($query->getWhere())
181
                            ->scalar();
182
        } catch (PDOException $original) {
183
            $e = new DriverException('An error occurred in the database driver while getting the number of '.$model::modelName().' objects: '.$original->getMessage());
184
            $e->setException($original);
185
            throw $e;
186
        }
187
    }
188
189
    /**
190
     * Generates the tablename for the model.
191
     *
192
     * @param string|Model $model
193
     *
194
     * @return string
195
     */
196
    public function getTablename($model)
197
    {
198
        $inflector = Inflector::get();
199
200
        return $inflector->camelize($inflector->pluralize($model::modelName()));
201
    }
202
203
    /**
204
     * Marshals a value to storage.
205
     *
206
     * @param mixed $value
207
     *
208
     * @return mixed serialized value
209
     */
210
    public function serializeValue($value)
211
    {
212
        // encode arrays/objects as JSON
213
        if (is_array($value) || is_object($value)) {
214
            return json_encode($value);
215
        }
216
217
        return $value;
218
    }
219
220
    /**
221
     * Marshals a value for a given property from storage.
222
     *
223
     * @param array $property
224
     * @param mixed $value
225
     *
226
     * @return mixed unserialized value
227
     */
228
    public function unserializeValue(array $property, $value)
229
    {
230
        if ($value === null) {
231
            return;
232
        }
233
234
        // handle empty strings as null
235
        if ($property['null'] && $value == '') {
236
            return;
237
        }
238
239
        $type = array_value($property, 'type');
240
        $m = 'to_'.$type;
241
242
        if (!method_exists(Property::class, $m)) {
243
            return $value;
244
        }
245
246
        return Property::$m($value);
247
    }
248
249
    /**
250
     * Serializes an array of values.
251
     *
252
     * @param array $values
253
     *
254
     * @return array
255
     */
256
    private function serialize(array $values)
257
    {
258
        foreach ($values as &$value) {
259
            $value = $this->serializeValue($value);
260
        }
261
262
        return $values;
263
    }
264
265
    /**
266
     * Unserializes an array of values.
267
     *
268
     * @param array $values
269
     * @param array $properties model properties
270
     *
271
     * @return array
272
     */
273
    private function unserialize(array $values, array $properties)
274
    {
275
        foreach ($values as $k => &$value) {
276
            if (isset($properties[$k])) {
277
                $value = $this->unserializeValue($properties[$k], $value);
278
            }
279
        }
280
281
        return $values;
282
    }
283
284
    /**
285
     * Returns a prefixed select statement.
286
     *
287
     * @param string $columns
288
     * @param string $tablename
289
     *
290
     * @return string
291
     */
292
    private function prefixSelect($columns, $tablename)
293
    {
294
        $prefixed = [];
295
        foreach (explode(',', $columns) as $column) {
296
            $prefixed[] = $this->prefixColumn($column, $tablename);
297
        }
298
299
        return implode(',', $prefixed);
300
    }
301
302
    /**
303
     * Returns a prefixed where statement.
304
     *
305
     * @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...
306
     * @param string $tablename
307
     *
308
     * @return array
309
     */
310
    private function prefixWhere(array $where, $tablename)
311
    {
312
        $return = [];
313
        foreach ($where as $key => $condition) {
314
            // handles $where[property] = value
315
            if (!is_numeric($key)) {
316
                $return[$this->prefixColumn($key, $tablename)] = $condition;
317
            // 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...
318
            } elseif (is_array($condition)) {
319
                $condition[0] = $this->prefixColumn($condition[0], $tablename);
320
                $return[] = $condition;
321
            // handles raw SQL - do nothing
322
            } else {
323
                $return[] = $condition;
324
            }
325
        }
326
327
        return $return;
328
    }
329
330
    /**
331
     * Returns a prefixed sort statement.
332
     *
333
     * @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...
334
     * @param string $tablename
335
     *
336
     * @return array
337
     */
338
    private function prefixSort(array $sort, $tablename)
339
    {
340
        foreach ($sort as &$condition) {
341
            $condition[0] = $this->prefixColumn($condition[0], $tablename);
342
        }
343
344
        return $sort;
345
    }
346
347
    /**
348
     * Prefix columns with tablename that contains only
349
     * alphanumeric/underscores/*.
350
     *
351
     * @param string $column
352
     * @param string $tablename
353
     *
354
     * @return string prefixed column
355
     */
356
    private function prefixColumn($column, $tablename)
357
    {
358
        if ($column === '*' || preg_match('/^[a-z0-9_]+$/i', $column)) {
359
            return "$tablename.$column";
360
        }
361
362
        return $column;
363
    }
364
}
365