|
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) |
|
|
|
|
|
|
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); |
|
|
|
|
|
|
127
|
|
|
} |
|
128
|
|
|
|
|
129
|
|
View Code Duplication |
public function loadModel(Model $model) |
|
|
|
|
|
|
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) |
|
|
|
|
|
|
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) |
|
|
|
|
|
|
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 |
|
|
|
|
|
|
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, '='] |
|
|
|
|
|
|
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 |
|
|
|
|
|
|
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
|
|
|
|
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.