|
1
|
|
|
<?php |
|
2
|
|
|
namespace Darya\Database\Query; |
|
3
|
|
|
|
|
4
|
|
|
use Exception; |
|
5
|
|
|
use Darya\Database; |
|
6
|
|
|
use Darya\Database\Query\Translator; |
|
7
|
|
|
use Darya\Storage; |
|
8
|
|
|
|
|
9
|
|
|
/** |
|
10
|
|
|
* An abstract query translator that prepares SQL common across more than one |
|
11
|
|
|
* RDBMS. |
|
12
|
|
|
* |
|
13
|
|
|
* @author Chris Andrew <[email protected]> |
|
14
|
|
|
*/ |
|
15
|
|
|
abstract class AbstractSqlTranslator implements Translator { |
|
16
|
|
|
|
|
17
|
|
|
/** |
|
18
|
|
|
* @var array Filter comparison operators |
|
19
|
|
|
*/ |
|
20
|
|
|
protected $operators = array('>=', '<=', '>', '<', '=', '!=', '<>', 'in', 'not in', 'is', 'is not', 'like', 'not like'); |
|
21
|
|
|
|
|
22
|
|
|
/** |
|
23
|
|
|
* Concatenates the given set of strings that aren't empty. |
|
24
|
|
|
* |
|
25
|
|
|
* Runs implode() after filtering out empty elements. |
|
26
|
|
|
* |
|
27
|
|
|
* Delimiter defaults to a single whitespace character. |
|
28
|
|
|
* |
|
29
|
|
|
* @param array $strings |
|
30
|
|
|
* @param string $delimiter [optional] |
|
31
|
|
|
* @return string |
|
32
|
|
|
*/ |
|
33
|
|
|
protected static function concatenate($strings, $delimiter = ' ') { |
|
34
|
|
|
$strings = array_filter($strings, function($value) { |
|
35
|
|
|
return !empty($value); |
|
36
|
|
|
}); |
|
37
|
|
|
|
|
38
|
|
|
return implode($delimiter, $strings); |
|
39
|
|
|
} |
|
40
|
|
|
|
|
41
|
|
|
/** |
|
42
|
|
|
* Determine whether the given limit and offset will make a difference to |
|
43
|
|
|
* a statement. |
|
44
|
|
|
* |
|
45
|
|
|
* Simply determines whether both are non-zero integers. |
|
46
|
|
|
* |
|
47
|
|
|
* @param int $limit |
|
48
|
|
|
* @param int $offset |
|
49
|
|
|
* @return bool |
|
50
|
|
|
*/ |
|
51
|
|
|
protected static function limitIsUseful($limit, $offset) { |
|
52
|
|
|
return (int) $limit !== 0 || (int) $offset !== 0; |
|
53
|
|
|
} |
|
54
|
|
|
|
|
55
|
|
|
/** |
|
56
|
|
|
* Translate the given storage query into an SQL query. |
|
57
|
|
|
* |
|
58
|
|
|
* @param Storage\Query $storageQuery |
|
59
|
|
|
* @return Database\Query |
|
60
|
|
|
* @throws Exception |
|
61
|
|
|
*/ |
|
62
|
|
|
public function translate(Storage\Query $storageQuery) { |
|
63
|
|
|
$type = $storageQuery->type; |
|
64
|
|
|
|
|
65
|
|
|
$method = 'translate' . ucfirst($type); |
|
66
|
|
|
|
|
67
|
|
|
if (!method_exists($this, $method)) { |
|
68
|
|
|
throw new Exception("Could not translate query of unsupported type '$type'"); |
|
69
|
|
|
} |
|
70
|
|
|
|
|
71
|
|
|
$query = call_user_func_array(array($this, $method), array($storageQuery)); |
|
72
|
|
|
|
|
73
|
|
|
return $query; |
|
74
|
|
|
} |
|
75
|
|
|
|
|
76
|
|
|
/** |
|
77
|
|
|
* Translate a query that creates a record. |
|
78
|
|
|
* |
|
79
|
|
|
* @param Storage\Query $storageQuery |
|
80
|
|
|
* @return Database\Query |
|
81
|
|
|
*/ |
|
82
|
|
|
protected function translateCreate(Storage\Query $storageQuery) { |
|
83
|
|
|
return new Database\Query( |
|
84
|
|
|
$this->prepareInsert($storageQuery->resource, $storageQuery->data), |
|
85
|
|
|
$this->parameters($storageQuery) |
|
86
|
|
|
); |
|
87
|
|
|
} |
|
88
|
|
|
|
|
89
|
|
|
/** |
|
90
|
|
|
* Translate a query that reads records. |
|
91
|
|
|
* |
|
92
|
|
|
* @param Storage\Query $storageQuery |
|
93
|
|
|
* @return Database\Query |
|
94
|
|
|
*/ |
|
95
|
|
|
protected function translateRead(Storage\Query $storageQuery) { |
|
96
|
|
|
return new Database\Query( |
|
97
|
|
|
$this->prepareSelect($storageQuery->resource, |
|
98
|
|
|
$this->prepareColumns($storageQuery->fields), |
|
99
|
|
|
$this->prepareWhere($storageQuery->filter), |
|
100
|
|
|
$this->prepareOrderBy($storageQuery->order), |
|
101
|
|
|
$this->prepareLimit($storageQuery->limit, $storageQuery->offset), |
|
102
|
|
|
$storageQuery->distinct |
|
103
|
|
|
), |
|
104
|
|
|
$this->parameters($storageQuery) |
|
105
|
|
|
); |
|
106
|
|
|
} |
|
107
|
|
|
|
|
108
|
|
|
/** |
|
109
|
|
|
* Translate a query that updates records. |
|
110
|
|
|
* |
|
111
|
|
|
* @param Storage\Query $storageQuery |
|
112
|
|
|
* @return Database\Query |
|
113
|
|
|
*/ |
|
114
|
|
View Code Duplication |
protected function translateUpdate(Storage\Query $storageQuery) { |
|
|
|
|
|
|
115
|
|
|
return new Database\Query( |
|
116
|
|
|
$this->prepareUpdate($storageQuery->resource, $storageQuery->data, |
|
117
|
|
|
$this->prepareWhere($storageQuery->filter), |
|
118
|
|
|
$this->prepareLimit($storageQuery->limit, $storageQuery->offset) |
|
119
|
|
|
), |
|
120
|
|
|
$this->parameters($storageQuery) |
|
121
|
|
|
); |
|
122
|
|
|
} |
|
123
|
|
|
|
|
124
|
|
|
/** |
|
125
|
|
|
* Translate a query that deletes records. |
|
126
|
|
|
* |
|
127
|
|
|
* @param Storage\Query $storageQuery |
|
128
|
|
|
* @return Database\Query |
|
129
|
|
|
*/ |
|
130
|
|
View Code Duplication |
protected function translateDelete(Storage\Query $storageQuery) { |
|
|
|
|
|
|
131
|
|
|
return new Database\Query( |
|
132
|
|
|
$this->prepareDelete($storageQuery->resource, |
|
133
|
|
|
$this->prepareWhere($storageQuery->filter), |
|
134
|
|
|
$this->prepareLimit($storageQuery->limit, $storageQuery->offset) |
|
135
|
|
|
), |
|
136
|
|
|
$this->parameters($storageQuery) |
|
137
|
|
|
); |
|
138
|
|
|
} |
|
139
|
|
|
|
|
140
|
|
|
/** |
|
141
|
|
|
* Escape the given identifier. |
|
142
|
|
|
* |
|
143
|
|
|
* If the value is an array, it is recursively escaped. |
|
144
|
|
|
* |
|
145
|
|
|
* If the value is not a string, it is returned unmodified. |
|
146
|
|
|
* |
|
147
|
|
|
* @param mixed $identifier |
|
148
|
|
|
* @return mixed |
|
149
|
|
|
*/ |
|
150
|
|
|
abstract protected function identifier($identifier); |
|
151
|
|
|
|
|
152
|
|
|
/** |
|
153
|
|
|
* Prepare the given value. |
|
154
|
|
|
* |
|
155
|
|
|
* If the value is an array, it is recursively prepared. |
|
156
|
|
|
* |
|
157
|
|
|
* @param array|string $value |
|
158
|
|
|
* @return array|string |
|
159
|
|
|
*/ |
|
160
|
|
|
protected function value($value) { |
|
161
|
|
|
if (is_array($value)) { |
|
162
|
|
|
return array_map(array($this, 'value'), $value); |
|
163
|
|
|
} |
|
164
|
|
|
|
|
165
|
|
|
return $this->resolve($value); |
|
166
|
|
|
} |
|
167
|
|
|
|
|
168
|
|
|
/** |
|
169
|
|
|
* Resolve a placeholder or constant for the given parameter value. |
|
170
|
|
|
* |
|
171
|
|
|
* @param mixed $value |
|
172
|
|
|
* @return string |
|
173
|
|
|
*/ |
|
174
|
|
|
protected function resolve($value) { |
|
175
|
|
|
if ($value === null) { |
|
176
|
|
|
return 'NULL'; |
|
177
|
|
|
} |
|
178
|
|
|
|
|
179
|
|
|
if (is_bool($value)) { |
|
180
|
|
|
return $value ? 'TRUE' : 'FALSE'; |
|
181
|
|
|
} |
|
182
|
|
|
|
|
183
|
|
|
return '?'; |
|
184
|
|
|
} |
|
185
|
|
|
|
|
186
|
|
|
/** |
|
187
|
|
|
* Determine whether the given value resolves a placeholder. |
|
188
|
|
|
* |
|
189
|
|
|
* @param mixed $value |
|
190
|
|
|
* @return bool |
|
191
|
|
|
*/ |
|
192
|
|
|
protected function resolvesPlaceholder($value) { |
|
193
|
|
|
return $this->resolve($value) === '?'; |
|
194
|
|
|
} |
|
195
|
|
|
|
|
196
|
|
|
/** |
|
197
|
|
|
* Prepare the given columns as a string. |
|
198
|
|
|
* |
|
199
|
|
|
* @param array|string $columns |
|
200
|
|
|
* @return string |
|
201
|
|
|
*/ |
|
202
|
|
|
protected function prepareColumns($columns) { |
|
203
|
|
|
if (empty($columns)) { |
|
204
|
|
|
return '*'; |
|
205
|
|
|
} |
|
206
|
|
|
|
|
207
|
|
|
$columns = (array) $this->identifier($columns); |
|
208
|
|
|
|
|
209
|
|
|
return implode(', ', $columns); |
|
210
|
|
|
} |
|
211
|
|
|
|
|
212
|
|
|
/** |
|
213
|
|
|
* Prepare a default operator for the given value. |
|
214
|
|
|
* |
|
215
|
|
|
* @param string $operator |
|
216
|
|
|
* @param mixed $value |
|
217
|
|
|
* @return string |
|
218
|
|
|
*/ |
|
219
|
|
|
protected function prepareOperator($operator, $value) { |
|
220
|
|
|
$operator = in_array(strtolower($operator), $this->operators) ? strtoupper($operator) : '='; |
|
221
|
|
|
|
|
222
|
|
View Code Duplication |
if (!$this->resolvesPlaceholder($value)) { |
|
|
|
|
|
|
223
|
|
|
if ($operator === '=') { |
|
224
|
|
|
$operator = 'IS'; |
|
225
|
|
|
} |
|
226
|
|
|
|
|
227
|
|
|
if ($operator === '!=') { |
|
228
|
|
|
$operator = 'IS NOT'; |
|
229
|
|
|
} |
|
230
|
|
|
} |
|
231
|
|
|
|
|
232
|
|
|
if (is_array($value)) { |
|
233
|
|
|
if ($operator === '=') { |
|
234
|
|
|
$operator = 'IN'; |
|
235
|
|
|
} |
|
236
|
|
|
|
|
237
|
|
|
if ($operator === '!=') { |
|
238
|
|
|
$operator = 'NOT IN'; |
|
239
|
|
|
} |
|
240
|
|
|
} |
|
241
|
|
|
|
|
242
|
|
|
return $operator; |
|
243
|
|
|
} |
|
244
|
|
|
|
|
245
|
|
|
/** |
|
246
|
|
|
* Prepare an individual filter condition. |
|
247
|
|
|
* |
|
248
|
|
|
* @param string $column |
|
249
|
|
|
* @param mixed $value |
|
250
|
|
|
* @return string |
|
251
|
|
|
*/ |
|
252
|
|
|
protected function prepareFilter($column, $value) { |
|
253
|
|
|
list($column, $operator) = array_pad(explode(' ', $column, 2), 2, null); |
|
254
|
|
|
|
|
255
|
|
|
$column = $this->prepareColumns($column); |
|
256
|
|
|
$operator = $this->prepareOperator($operator, $value); |
|
257
|
|
|
$value = $this->value($value); |
|
258
|
|
|
|
|
259
|
|
|
if (is_array($value)) { |
|
260
|
|
|
$value = "(" . implode(", ", $value) . ")"; |
|
261
|
|
|
} |
|
262
|
|
|
|
|
263
|
|
|
return "$column $operator $value"; |
|
264
|
|
|
} |
|
265
|
|
|
|
|
266
|
|
|
/** |
|
267
|
|
|
* Prepare a WHERE clause using the given filter and comparison operator. |
|
268
|
|
|
* |
|
269
|
|
|
* Example filter key-values and their SQL equivalents: |
|
270
|
|
|
* 'id' => 1, // id = '1' |
|
271
|
|
|
* 'name like' => 'Chris', // name LIKE 'Chris' |
|
272
|
|
|
* 'count >' => 10, // count > '10' |
|
273
|
|
|
* 'type in' => [1, 2], // type IN (1, 2) |
|
274
|
|
|
* 'type' => [3, 4] // type IN (3, 4) |
|
275
|
|
|
* |
|
276
|
|
|
* Comparison operator between conditions defaults to 'AND'. |
|
277
|
|
|
* |
|
278
|
|
|
* @param array $filter |
|
279
|
|
|
* @param string $comparison [optional] |
|
280
|
|
|
* @param bool $excludeWhere [optional] |
|
281
|
|
|
* @return string |
|
282
|
|
|
*/ |
|
283
|
|
|
protected function prepareWhere(array $filter, $comparison = 'AND', $excludeWhere = false) { |
|
284
|
|
|
$conditions = array(); |
|
285
|
|
|
|
|
286
|
|
|
foreach ($filter as $column => $value) { |
|
287
|
|
|
if (strtolower($column) == 'or') { |
|
288
|
|
|
$conditions[] = '(' . $this->prepareWhere($value, 'OR', true) . ')'; |
|
289
|
|
|
} else { |
|
290
|
|
|
$conditions[] = $this->prepareFilter($column, $value); |
|
291
|
|
|
} |
|
292
|
|
|
} |
|
293
|
|
|
|
|
294
|
|
|
if (!count($conditions)) { |
|
295
|
|
|
return null; |
|
296
|
|
|
} |
|
297
|
|
|
|
|
298
|
|
|
$where = $excludeWhere ? '' : 'WHERE '; |
|
299
|
|
|
$where .= implode(" $comparison ", $conditions); |
|
300
|
|
|
|
|
301
|
|
|
return $where; |
|
302
|
|
|
} |
|
303
|
|
|
|
|
304
|
|
|
/** |
|
305
|
|
|
* Prepare an individual order condition. |
|
306
|
|
|
* |
|
307
|
|
|
* @param string $column |
|
308
|
|
|
* @param string $direction [optional] |
|
309
|
|
|
* @return string |
|
310
|
|
|
*/ |
|
311
|
|
|
protected function prepareOrder($column, $direction = null) { |
|
312
|
|
|
$column = $this->identifier($column); |
|
313
|
|
|
$direction = $direction !== null ? strtoupper($direction) : 'ASC'; |
|
314
|
|
|
|
|
315
|
|
|
return !empty($column) ? "$column $direction" : null; |
|
316
|
|
|
} |
|
317
|
|
|
|
|
318
|
|
|
/** |
|
319
|
|
|
* Prepare an ORDER BY clause using the given order. |
|
320
|
|
|
* |
|
321
|
|
|
* Example order key-values: |
|
322
|
|
|
* 'column', |
|
323
|
|
|
* 'other_column' => 'ASC', |
|
324
|
|
|
* 'another_column' => 'DESC |
|
325
|
|
|
* |
|
326
|
|
|
* Ordered ascending by default. |
|
327
|
|
|
* |
|
328
|
|
|
* @param array|string $order |
|
329
|
|
|
* @return string |
|
330
|
|
|
*/ |
|
331
|
|
|
protected function prepareOrderBy($order) { |
|
332
|
|
|
$conditions = array(); |
|
333
|
|
|
|
|
334
|
|
|
foreach ((array) $order as $key => $value) { |
|
335
|
|
|
if (is_numeric($key)) { |
|
336
|
|
|
$conditions[] = $this->prepareOrder($value); |
|
337
|
|
|
} else { |
|
338
|
|
|
$conditions[] = $this->prepareOrder($key, $value); |
|
339
|
|
|
} |
|
340
|
|
|
} |
|
341
|
|
|
|
|
342
|
|
|
return count($conditions) ? 'ORDER BY ' . implode(', ', $conditions) : null; |
|
343
|
|
|
} |
|
344
|
|
|
|
|
345
|
|
|
/** |
|
346
|
|
|
* Prepare a SELECT statement using the given columns, table, clauses and |
|
347
|
|
|
* options. |
|
348
|
|
|
* |
|
349
|
|
|
* @param string $table |
|
350
|
|
|
* @param array|string $columns |
|
351
|
|
|
* @param string $where [optional] |
|
352
|
|
|
* @param string $order [optional] |
|
353
|
|
|
* @param string $limit [optional] |
|
354
|
|
|
* @param bool $distinct [optional] |
|
355
|
|
|
* @return string |
|
356
|
|
|
*/ |
|
357
|
|
|
abstract protected function prepareSelect($table, $columns, $where = null, $order = null, $limit = null, $distinct = false); |
|
358
|
|
|
|
|
359
|
|
|
/** |
|
360
|
|
|
* Prepare an INSERT INTO statement using the given table and data. |
|
361
|
|
|
* |
|
362
|
|
|
* @param string $table |
|
363
|
|
|
* @param array $data |
|
364
|
|
|
* @return string |
|
365
|
|
|
*/ |
|
366
|
|
|
protected function prepareInsert($table, array $data) { |
|
367
|
|
|
$table = $this->identifier($table); |
|
368
|
|
|
|
|
369
|
|
|
$columns = $this->identifier(array_keys($data)); |
|
370
|
|
|
$values = $this->value(array_values($data)); |
|
371
|
|
|
|
|
372
|
|
|
$columns = "(" . implode(", ", $columns) . ")"; |
|
373
|
|
|
$values = "(" . implode(", ", $values) . ")"; |
|
374
|
|
|
|
|
375
|
|
|
return static::concatenate(array('INSERT INTO', $table, $columns, 'VALUES', $values)); |
|
376
|
|
|
} |
|
377
|
|
|
|
|
378
|
|
|
/** |
|
379
|
|
|
* Prepare an UPDATE statement with the given table, data and clauses. |
|
380
|
|
|
* |
|
381
|
|
|
* @param string $table |
|
382
|
|
|
* @param array $data |
|
383
|
|
|
* @param string $where [optional] |
|
384
|
|
|
* @param string $limit [optional] |
|
385
|
|
|
* @return string |
|
386
|
|
|
*/ |
|
387
|
|
|
abstract protected function prepareUpdate($table, $data, $where = null, $limit = null); |
|
388
|
|
|
|
|
389
|
|
|
/** |
|
390
|
|
|
* Prepare a DELETE statement with the given table and clauses. |
|
391
|
|
|
* |
|
392
|
|
|
* @param string $table |
|
393
|
|
|
* @param string $where [optional] |
|
394
|
|
|
* @param string $limit [optional] |
|
395
|
|
|
* @return string |
|
396
|
|
|
*/ |
|
397
|
|
|
abstract protected function prepareDelete($table, $where = null, $limit = null); |
|
398
|
|
|
|
|
399
|
|
|
/** |
|
400
|
|
|
* Prepare the given filter as an array of prepared query parameters. |
|
401
|
|
|
* |
|
402
|
|
|
* @return array |
|
403
|
|
|
*/ |
|
404
|
|
|
protected function filterParameters($filter) { |
|
405
|
|
|
$parameters = array(); |
|
406
|
|
|
|
|
407
|
|
|
foreach ($filter as $index => $value) { |
|
408
|
|
|
if (is_array($value)) { |
|
409
|
|
|
if (strtolower($index) === 'or') { |
|
410
|
|
|
$parameters = array_merge($parameters, $this->filterParameters($value)); |
|
411
|
|
|
} else { |
|
412
|
|
|
foreach ($value as $in) { |
|
413
|
|
|
if ($this->resolvesPlaceholder($value)) { |
|
414
|
|
|
$parameters[] = $in; |
|
415
|
|
|
} |
|
416
|
|
|
} |
|
417
|
|
|
} |
|
418
|
|
|
} else { |
|
419
|
|
|
if ($this->resolvesPlaceholder($value)) { |
|
420
|
|
|
$parameters[] = $value; |
|
421
|
|
|
} |
|
422
|
|
|
} |
|
423
|
|
|
} |
|
424
|
|
|
|
|
425
|
|
|
return $parameters; |
|
426
|
|
|
} |
|
427
|
|
|
|
|
428
|
|
|
/** |
|
429
|
|
|
* Retrieve an array of parameters from the given query for executing a |
|
430
|
|
|
* prepared query. |
|
431
|
|
|
* |
|
432
|
|
|
* @param Storage\Query $query |
|
433
|
|
|
* @return array |
|
434
|
|
|
*/ |
|
435
|
|
|
public function parameters(Storage\Query $query) { |
|
436
|
|
|
$parameters = array(); |
|
437
|
|
|
|
|
438
|
|
|
foreach ($query->data as $value) { |
|
439
|
|
|
if ($this->resolvesPlaceholder($value)) { |
|
440
|
|
|
$parameters[] = $value; |
|
441
|
|
|
} |
|
442
|
|
|
} |
|
443
|
|
|
|
|
444
|
|
|
$parameters = array_merge($parameters, $this->filterParameters($query->filter)); |
|
445
|
|
|
|
|
446
|
|
|
return $parameters; |
|
447
|
|
|
} |
|
448
|
|
|
|
|
449
|
|
|
} |
|
450
|
|
|
|
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.