1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* @package toolkit |
5
|
|
|
*/ |
6
|
|
|
|
7
|
|
|
/** |
8
|
|
|
* This class holds all the required data to enable dynamic SQL statement creation. |
9
|
|
|
* The way it currently works is by keeping an array for SQL parts. Each operation |
10
|
|
|
* must add their corresponding SQL part. |
11
|
|
|
* |
12
|
|
|
* When appending parts, developers should make sure that the SQL is safe. |
13
|
|
|
* The class also offers methods to sanitize and validate field values. |
14
|
|
|
* |
15
|
|
|
* Finally, the class can be inherited by specialized class for particular queries. |
16
|
|
|
* |
17
|
|
|
* @see DatabaseQuery |
18
|
|
|
* @see DatabaseCreate |
19
|
|
|
* @see DatabaseUpdate |
20
|
|
|
* @see DatabaseDelete |
21
|
|
|
* @see DatabaseShow |
22
|
|
|
* @see DatabaseCreate |
23
|
|
|
* @see DatabaseAlter |
24
|
|
|
* @see DatabaseDrop |
25
|
|
|
* @see DatabaseTruncate |
26
|
|
|
* @see DatabaseOptimize |
27
|
|
|
* @see DatabaseSet |
28
|
|
|
* @see DatabaseStatementException |
29
|
|
|
*/ |
30
|
|
|
class DatabaseStatement |
31
|
|
|
{ |
32
|
|
|
/** |
33
|
|
|
* List of element delimiter |
34
|
|
|
* @var string |
35
|
|
|
*/ |
36
|
|
|
const LIST_DELIMITER = ', '; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* The SQL values delimiter |
40
|
|
|
*/ |
41
|
|
|
const VALUES_DELIMITER = ','; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* The SQL part delimiter |
45
|
|
|
* @var string |
46
|
|
|
*/ |
47
|
|
|
const STATEMENTS_DELIMITER = ' '; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* The SQL part end of line |
51
|
|
|
*/ |
52
|
|
|
const FORMATTED_PART_EOL = "\n"; |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* The SQL part tab character |
56
|
|
|
*/ |
57
|
|
|
const FORMATTED_PART_TAB = "\t"; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* The SQL part delimiter |
61
|
|
|
*/ |
62
|
|
|
const FORMATTED_PART_DELIMITER = self::FORMATTED_PART_EOL . self::FORMATTED_PART_TAB; |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* Regular Expression that matches SQL functions |
66
|
|
|
* @var string |
67
|
|
|
*/ |
68
|
|
|
const FCT_PATTERN = '/^([A-Za-z_]+)\((.*)\)$/'; |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* The SQL functions arguments delimiter |
72
|
|
|
*/ |
73
|
|
|
const FCT_ARGS_DELIMITER = ','; |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* Regular Expression that matches SQL operators +, -, *, / |
77
|
|
|
* @var string |
78
|
|
|
*/ |
79
|
|
|
const OP_PATTERN = '/\s+([\-\+\*\/])\s+/'; |
80
|
|
|
|
81
|
|
|
/** |
82
|
|
|
* Database object reference |
83
|
|
|
* @var Database |
84
|
|
|
*/ |
85
|
|
|
private $db; |
86
|
|
|
|
87
|
|
|
/** |
88
|
|
|
* SQL parts array |
89
|
|
|
* @var array |
90
|
|
|
*/ |
91
|
|
|
private $sql = []; |
92
|
|
|
|
93
|
|
|
/** |
94
|
|
|
* SQL values array |
95
|
|
|
* @see appendValues() |
96
|
|
|
* @var array |
97
|
|
|
*/ |
98
|
|
|
private $values = []; |
99
|
|
|
|
100
|
|
|
/** |
101
|
|
|
* SQL parameters cache |
102
|
|
|
* @see convertToParameterName() |
103
|
|
|
* @var array |
104
|
|
|
*/ |
105
|
|
|
private $parameters = []; |
106
|
|
|
|
107
|
|
|
/** |
108
|
|
|
* Placeholder flag: Developer should check if the statement supports name |
109
|
|
|
* parameters, which is on by default. |
110
|
|
|
* @var bool |
111
|
|
|
*/ |
112
|
|
|
private $usePlaceholders = false; |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* Safe flag: Allows old code to still inject illegal characters in their SQL. |
116
|
|
|
* @see Database::validateSQLQuery() |
117
|
|
|
* @var boolean |
118
|
|
|
*/ |
119
|
|
|
private $safe = true; |
120
|
|
|
|
121
|
|
|
/** |
122
|
|
|
* Creates a new DatabaseStatement object, linked to the $db parameter |
123
|
|
|
* and containing the optional $statement. |
124
|
|
|
* |
125
|
|
|
* @param Database $db |
126
|
|
|
* The Database reference |
127
|
|
|
* @param string $statement |
128
|
|
|
* An optional string of SQL that will be appended right from the start. |
129
|
|
|
* Defaults to an empty string. |
130
|
|
|
*/ |
131
|
|
|
public function __construct(Database $db, $statement = '') |
132
|
|
|
{ |
133
|
|
|
General::ensureType([ |
134
|
|
|
'statement' => ['var' => $statement, 'type' => 'string'], |
135
|
|
|
]); |
136
|
|
|
$this->db = $db; |
137
|
|
|
if (!empty($statement)) { |
138
|
|
|
$this->unsafeAppendSQLPart('statement', $statement); |
139
|
|
|
} |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
/** |
143
|
|
|
* Destroys all underlying resources |
144
|
|
|
*/ |
145
|
|
|
public function __destruct() |
146
|
|
|
{ |
147
|
|
|
$this->db = null; |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
/** |
151
|
|
|
* Getter for the underlying database object. |
152
|
|
|
* |
153
|
|
|
* @return Database |
154
|
|
|
*/ |
155
|
|
|
final protected function getDB() |
156
|
|
|
{ |
157
|
|
|
return $this->db; |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
/** |
161
|
|
|
* Getter for the underlying SQL parts array. |
162
|
|
|
* |
163
|
|
|
* @return array |
164
|
|
|
*/ |
165
|
|
|
final protected function getSQL() |
166
|
|
|
{ |
167
|
|
|
return $this->sql; |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
/** |
171
|
|
|
* Returns all the parts for the specified type |
172
|
|
|
* |
173
|
|
|
* @param string $type |
174
|
|
|
* The type value for the parts to retrieve |
175
|
|
|
* @return array |
176
|
|
|
*/ |
177
|
|
|
final public function getSQLParts($type) |
178
|
|
|
{ |
179
|
|
|
return array_filter($this->getSQL(), function ($part) use ($type) { |
180
|
|
|
if (is_array($type)) { |
|
|
|
|
181
|
|
|
return in_array(current(array_keys($part)), $type); |
182
|
|
|
} |
183
|
|
|
return current(array_keys($part)) === $type; |
184
|
|
|
}); |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
/** |
188
|
|
|
* Returns true if the statement contains the specified part. |
189
|
|
|
* |
190
|
|
|
* @see getSQLParts() |
191
|
|
|
* @param string $type |
192
|
|
|
* The type value for the parts to check for |
193
|
|
|
* @return bool |
194
|
|
|
*/ |
195
|
|
|
final public function containsSQLParts($type) |
196
|
|
|
{ |
197
|
|
|
return !empty($this->getSQLParts($type)); |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
/** |
201
|
|
|
* Returns the order in which parts needs to be generated. |
202
|
|
|
* Only those parts will be included when calling generateSQL(). |
203
|
|
|
* When multiple parts can share the same order, use a sub-array. |
204
|
|
|
* Control characters can be used to merge parts together. |
205
|
|
|
* Those characters are: |
206
|
|
|
* - `(` and `)` which wraps one or more parts in parenthesis |
207
|
|
|
* - `,` which joins part with a comma if both the preceding and next parts are not empty |
208
|
|
|
* |
209
|
|
|
* @see getSQLParts() |
210
|
|
|
* @see generateSQL() |
211
|
|
|
* @return array |
212
|
|
|
*/ |
213
|
|
|
protected function getStatementStructure() |
214
|
|
|
{ |
215
|
|
|
return ['statement']; |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
/** |
219
|
|
|
* Merges the ordered SQL parts array into a string, joined with the content of the |
220
|
|
|
* `STATEMENTS_DELIMITER` constant. |
221
|
|
|
* The order in which the part are merged are given by getStatementStructure(). |
222
|
|
|
* |
223
|
|
|
* @see generateOrderedSQLParts() |
224
|
|
|
* @see getStatementStructure() |
225
|
|
|
* @return string |
226
|
|
|
* The resulting SQL string |
227
|
|
|
*/ |
228
|
|
|
final public function generateSQL() |
229
|
|
|
{ |
230
|
|
|
return implode(self::STATEMENTS_DELIMITER, array_map(function ($part) { |
231
|
|
|
return current($part); |
232
|
|
|
}, $this->generateOrderedSQLParts())); |
|
|
|
|
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
/** |
236
|
|
|
* Merges the ordered SQL parts array into a string, joined with specific string in |
237
|
|
|
* order to create a formatted, human friendly representation of the resulting SQL. |
238
|
|
|
* The order in which the part are merged are given by getStatementStructure(). |
239
|
|
|
* The string used for each SQL part is given by getSeparatorForPartType(). |
240
|
|
|
* |
241
|
|
|
* @see FORMATTED_PART_DELIMITER |
242
|
|
|
* @see FORMATTED_PART_EOL |
243
|
|
|
* @see FORMATTED_PART_TAB |
244
|
|
|
* @see getSeparatorForPartType() |
245
|
|
|
* @see generateOrderedSQLParts() |
246
|
|
|
* @see getStatementStructure() |
247
|
|
|
* @return string |
248
|
|
|
* The resulting formatted SQL string |
249
|
|
|
*/ |
250
|
|
|
final public function generateFormattedSQL() |
251
|
|
|
{ |
252
|
|
|
$parts = $this->generateOrderedSQLParts(); |
253
|
|
|
return array_reduce($parts, function ($memo, $part) { |
|
|
|
|
254
|
|
|
$type = current(array_keys($part)); |
255
|
|
|
$value = current($part); |
256
|
|
|
$sep = $this->getSeparatorForPartType($type); |
257
|
|
|
if (!$memo) { |
258
|
|
|
return $value; |
259
|
|
|
} |
260
|
|
|
return "{$memo}{$sep}{$value}"; |
261
|
|
|
}, null); |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
/** |
265
|
|
|
* Gets the proper separator string for the given $type SQL part type, when |
266
|
|
|
* generating a formatted SQL statement. |
267
|
|
|
* The default implementation simply returns value of the `STATEMENTS_DELIMITER` constant. |
268
|
|
|
* |
269
|
|
|
* @see generateFormattedSQL() |
270
|
|
|
* @param string $type |
271
|
|
|
* The SQL part type. |
272
|
|
|
* @return string |
273
|
|
|
* The string to use to separate the formatted SQL parts. |
274
|
|
|
*/ |
275
|
|
|
public function getSeparatorForPartType($type) |
276
|
|
|
{ |
277
|
|
|
General::ensureType([ |
278
|
|
|
'type' => ['var' => $type, 'type' => 'string'], |
279
|
|
|
]); |
280
|
|
|
return self::STATEMENTS_DELIMITER; |
281
|
|
|
} |
282
|
|
|
|
283
|
|
|
/** |
284
|
|
|
* Creates the ordered SQL parts array. |
285
|
|
|
* The order in which the part sorted are given by getStatementStructure(). |
286
|
|
|
* |
287
|
|
|
* @see getStatementStructure() |
288
|
|
|
* @return string |
289
|
|
|
* The resulting SQL string |
290
|
|
|
*/ |
291
|
|
|
final public function generateOrderedSQLParts() |
292
|
|
|
{ |
293
|
|
|
$allParts = $this->getStatementStructure(); |
294
|
|
|
$orderedParts = []; |
295
|
|
|
foreach ($allParts as $ti => $type) { |
296
|
|
|
if (in_array($type, ['(', ')'])) { |
297
|
|
|
$orderedParts[] = [$type => $type]; |
298
|
|
|
continue; |
299
|
|
|
} elseif ($type === self::VALUES_DELIMITER) { |
300
|
|
|
$before = $this->getSQLParts($allParts[$ti - 1]); |
301
|
|
|
$after = $this->getSQLParts($allParts[$ti + 1]); |
302
|
|
|
if (!empty($before) && !empty($after)) { |
303
|
|
|
$orderedParts[] = [$type => $type]; |
304
|
|
|
} |
305
|
|
|
continue; |
306
|
|
|
} |
307
|
|
|
$parts = $this->getSQLParts($type); |
308
|
|
|
foreach ($parts as $pt => $part) { |
309
|
|
|
$orderedParts[] = $part; |
310
|
|
|
} |
311
|
|
|
} |
312
|
|
|
return $orderedParts; |
|
|
|
|
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
/** |
316
|
|
|
* @internal |
317
|
|
|
* Appends part $part of type $type into the SQL parts array. |
318
|
|
|
* Type $type is just a tag value, used to classify parts. |
319
|
|
|
* This can allow things like filtering out some parts. |
320
|
|
|
* |
321
|
|
|
* Only allowed parts will be accepted. The only valid part by default is 'statement'. |
322
|
|
|
* |
323
|
|
|
* BEWARE: This method does not validate or sanitize anything, except the |
324
|
|
|
* type of both parameters, which must be string. This method should be |
325
|
|
|
* used as a last resort or with properly sanitized values. |
326
|
|
|
* |
327
|
|
|
* @see getStatementStructure() |
328
|
|
|
* @param string $type |
329
|
|
|
* The type value for this part |
330
|
|
|
* @param string $part |
331
|
|
|
* The actual SQL code part |
332
|
|
|
* @return DatabaseStatement |
333
|
|
|
* The current instance |
334
|
|
|
* @throws DatabaseStatementException |
335
|
|
|
*/ |
336
|
|
|
final public function unsafeAppendSQLPart($type, $part) |
337
|
|
|
{ |
338
|
|
|
General::ensureType([ |
339
|
|
|
'type' => ['var' => $type, 'type' => 'string'], |
340
|
|
|
'part' => ['var' => $part, 'type' => 'string'], |
341
|
|
|
]); |
342
|
|
|
if (!General::in_array_multi($type, $this->getStatementStructure(), true)) { |
|
|
|
|
343
|
|
|
$class = get_class($this); |
344
|
|
|
throw new DatabaseStatementException("SQL Part type `$type` is not valid for class `$class`"); |
345
|
|
|
} |
346
|
|
|
$this->sql[] = [$type => $part]; |
347
|
|
|
return $this; |
348
|
|
|
} |
349
|
|
|
|
350
|
|
|
/** |
351
|
|
|
* Getter for the array of SQL values sent with the statement |
352
|
|
|
* to the database server. |
353
|
|
|
* |
354
|
|
|
* @return array |
355
|
|
|
*/ |
356
|
|
|
final public function getValues() |
357
|
|
|
{ |
358
|
|
|
return $this->values; |
359
|
|
|
} |
360
|
|
|
|
361
|
|
|
/** |
362
|
|
|
* Appends the specified $values to the SQL values array. |
363
|
|
|
* This is the proper way to send user input, as those values |
364
|
|
|
* are send along the SQL statement without any concatenation. |
365
|
|
|
* It is safer and faster. |
366
|
|
|
* |
367
|
|
|
* It supports keyed and numeric arrays. |
368
|
|
|
* When using a keyed arrays, keys should be used as SQL named parameters. |
369
|
|
|
* When using a numeric array, parameters should be place holders (?) |
370
|
|
|
* |
371
|
|
|
* @see usePlaceholders() |
372
|
|
|
* @see convertToParameterName() |
373
|
|
|
* @param array $values |
374
|
|
|
* The values to send to the database |
375
|
|
|
* @return DatabaseStatement |
376
|
|
|
* The current instance |
377
|
|
|
*/ |
378
|
|
|
final protected function appendValues(array $values) |
379
|
|
|
{ |
380
|
|
|
if ($this->isUsingPlaceholders()) { |
381
|
|
|
$values = array_values($values); |
382
|
|
|
} else { |
383
|
|
|
foreach ($values as $key => $value) { |
384
|
|
|
if (is_string($key)) { |
385
|
|
|
$safeKey = $this->convertToParameterName($key, $value); |
386
|
|
|
if ($key !== $safeKey) { |
387
|
|
|
unset($values[$key]); |
388
|
|
|
$values[$safeKey] = $value; |
389
|
|
|
} |
390
|
|
|
} |
391
|
|
|
} |
392
|
|
|
} |
393
|
|
|
$this->values = array_merge($this->values, $values); |
394
|
|
|
return $this; |
395
|
|
|
} |
396
|
|
|
|
397
|
|
|
/** |
398
|
|
|
* Statement parameter setter. This function bypasses the automatic parameter generation |
399
|
|
|
* to allow the developer to set values as if using PDO directly. |
400
|
|
|
* This is sometimes needed when dealing with complex custom queries. |
401
|
|
|
* You should rather consider to sub class the DatabaseStatement and use appendValues() instead. |
402
|
|
|
* |
403
|
|
|
* @param mixed $key |
404
|
|
|
* The key of the value, either its index or name |
405
|
|
|
* @param mixed $value |
406
|
|
|
* The actual user provided value |
407
|
|
|
* @return DatabaseStatement |
408
|
|
|
* The current instance |
409
|
|
|
* @throws DatabaseStatementException |
410
|
|
|
* If the key is not the proper type: numeric when using place holders, string if not. |
411
|
|
|
* If the key is already set. |
412
|
|
|
*/ |
413
|
|
|
final public function setValue($key, $value) |
414
|
|
|
{ |
415
|
|
|
if (General::intval($key) > 0) { |
416
|
|
|
$key = General::intval($key); |
417
|
|
|
if (!$this->isUsingPlaceholders()) { |
418
|
|
|
throw new DatabaseStatementException( |
419
|
|
|
'Can not use numeric index when using named parameters' |
420
|
|
|
); |
421
|
|
|
} |
422
|
|
|
} elseif (!is_string($key)) { |
423
|
|
|
throw new DatabaseStatementException('Key for parameter must be a string'); |
424
|
|
|
} |
425
|
|
|
if (isset($this->values[$key])) { |
426
|
|
|
throw new DatabaseStatementException("Value for parameter `$key` is already defined"); |
427
|
|
|
} |
428
|
|
|
$this->values[$key] = $value; |
429
|
|
|
return $this; |
430
|
|
|
} |
431
|
|
|
|
432
|
|
|
/** |
433
|
|
|
* Enable the use of placeholders (?) in the query instead of named parameters. |
434
|
|
|
* |
435
|
|
|
* @return DatabaseStatement |
436
|
|
|
* The current instance |
437
|
|
|
*/ |
438
|
|
|
final public function usePlaceholders() |
439
|
|
|
{ |
440
|
|
|
$this->usePlaceholders = true; |
441
|
|
|
return $this; |
442
|
|
|
} |
443
|
|
|
|
444
|
|
|
/** |
445
|
|
|
* If the current statement uses placeholders (?). |
446
|
|
|
* |
447
|
|
|
* @return bool |
448
|
|
|
* true is the statement uses placeholders |
449
|
|
|
*/ |
450
|
|
|
final public function isUsingPlaceholders() |
451
|
|
|
{ |
452
|
|
|
return $this->usePlaceholders; |
453
|
|
|
} |
454
|
|
|
|
455
|
|
|
/** |
456
|
|
|
* Marks the statement as not safe. |
457
|
|
|
* This disables strict validation |
458
|
|
|
* |
459
|
|
|
* @return DatabaseStatement |
460
|
|
|
* The current instance |
461
|
|
|
*/ |
462
|
|
|
final public function unsafe() |
463
|
|
|
{ |
464
|
|
|
$this->safe = false; |
465
|
|
|
return $this; |
466
|
|
|
} |
467
|
|
|
|
468
|
|
|
/** |
469
|
|
|
* If the current statement is deem safe. |
470
|
|
|
* Safe statements are validated more strictly |
471
|
|
|
* |
472
|
|
|
* @return bool |
473
|
|
|
* true is the statement uses placeholders |
474
|
|
|
*/ |
475
|
|
|
final public function isSafe() |
476
|
|
|
{ |
477
|
|
|
return $this->safe; |
478
|
|
|
} |
479
|
|
|
|
480
|
|
|
/** |
481
|
|
|
* @internal This method is not meant to be called directly. Use execute(). |
482
|
|
|
* Appends any remaining part of the statement. |
483
|
|
|
* Called just before validation and the actual sending of the statement to |
484
|
|
|
* the SQL server. |
485
|
|
|
* |
486
|
|
|
* @see execute() |
487
|
|
|
* @return DatabaseStatement |
488
|
|
|
* The current instance |
489
|
|
|
*/ |
490
|
|
|
public function finalize() |
491
|
|
|
{ |
492
|
|
|
return $this; |
493
|
|
|
} |
494
|
|
|
|
495
|
|
|
/** |
496
|
|
|
* Send the query and all associated values to the server for execution. |
497
|
|
|
* Calls finalize before sending creating and sending the query to the server. |
498
|
|
|
* |
499
|
|
|
* @see Database::execute() |
500
|
|
|
* @return DatabaseStatementResult |
501
|
|
|
* @throws DatabaseException |
502
|
|
|
*/ |
503
|
|
|
final public function execute() |
504
|
|
|
{ |
505
|
|
|
return $this |
506
|
|
|
->finalize() |
507
|
|
|
->getDB() |
508
|
|
|
->execute($this); |
509
|
|
|
} |
510
|
|
|
|
511
|
|
|
/** |
512
|
|
|
* Factory function that creates a new DatabaseStatementResult based upon the $result |
513
|
|
|
* and $stm parameters. |
514
|
|
|
* Child classes can overwrite this method to return a specialized version of the |
515
|
|
|
* DatabaseStatementResult class. |
516
|
|
|
* |
517
|
|
|
* @param bool $success |
518
|
|
|
* If the DatabaseStatement creating this instance succeeded or not. |
519
|
|
|
* @param PDOStatement $stm |
520
|
|
|
* The PDOStatement created by the execution of the DatabaseStatement. |
521
|
|
|
* @return DatabaseStatementResult |
522
|
|
|
*/ |
523
|
|
|
public function results($success, PDOStatement $stm) |
524
|
|
|
{ |
525
|
|
|
General::ensureType([ |
526
|
|
|
'success' => ['var' => $success, 'type' => 'bool'], |
527
|
|
|
]); |
528
|
|
|
return new DatabaseStatementResult($success, $stm); |
529
|
|
|
} |
530
|
|
|
|
531
|
|
|
/** |
532
|
|
|
* @internal |
533
|
|
|
* Given a string, replace the default table prefixes with the |
534
|
|
|
* table prefix for this database instance. |
535
|
|
|
* |
536
|
|
|
* @param string $query |
537
|
|
|
* @return string |
538
|
|
|
*/ |
539
|
|
|
final public function replaceTablePrefix($table) |
540
|
|
|
{ |
541
|
|
|
General::ensureType([ |
542
|
|
|
'table' => ['var' => $table, 'type' => 'string'], |
543
|
|
|
]); |
544
|
|
|
if ($this->getDB()->getPrefix() != 'tbl_') { |
545
|
|
|
$table = preg_replace('/tbl_(\S+?)([\s\.,]|$)/', $this->getDB()->getPrefix() .'\\1\\2', trim($table)); |
546
|
|
|
} |
547
|
|
|
|
548
|
|
|
return $table; |
549
|
|
|
} |
550
|
|
|
|
551
|
|
|
/** |
552
|
|
|
* @internal |
553
|
|
|
* Given a valid field name, returns its variant as a SQL parameter. |
554
|
|
|
* If the $key string is numeric, it will default to placeholders. |
555
|
|
|
* If enabled, it will use named parameters. |
556
|
|
|
* |
557
|
|
|
* @see validateFieldName() |
558
|
|
|
* @see isUsingPlaceholders() |
559
|
|
|
* @see usePlaceholders() |
560
|
|
|
* @see convertToParameterName() |
561
|
|
|
* @param string $key |
562
|
|
|
* The key from which to derive the parameter name from |
563
|
|
|
* @param mixed $value |
564
|
|
|
* The associated value for this key |
565
|
|
|
* @return string |
566
|
|
|
* The parameter expression |
567
|
|
|
*/ |
568
|
|
|
final public function asPlaceholderString($key, $value) |
569
|
|
|
{ |
570
|
|
|
if (!$this->isUsingPlaceholders() && General::intval($key) === -1) { |
571
|
|
|
$this->validateFieldName($key); |
572
|
|
|
$key = $this->convertToParameterName($key, $value); |
573
|
|
|
return ":$key"; |
574
|
|
|
} |
575
|
|
|
return '?'; |
576
|
|
|
} |
577
|
|
|
|
578
|
|
|
/** |
579
|
|
|
* Given an array of valid field names, maps `asPlaceholderString` on each |
580
|
|
|
* keys and then implodes the resulting array using LIST_DELIMITER |
581
|
|
|
* |
582
|
|
|
* @see asPlaceholderString() |
583
|
|
|
* @see LIST_DELIMITER |
584
|
|
|
* @param array $values |
585
|
|
|
* @return void |
586
|
|
|
*/ |
587
|
|
|
final public function asPlaceholdersList(array $values) |
588
|
|
|
{ |
589
|
|
|
return implode(self::LIST_DELIMITER, General::array_map([$this, 'asPlaceholderString'], $values)); |
|
|
|
|
590
|
|
|
} |
591
|
|
|
|
592
|
|
|
/** |
593
|
|
|
* @internal Actually does the tick formatting on the $value string. |
594
|
|
|
* It makes sure all ticks are removed before validating the value. |
595
|
|
|
* If the string contains a dot, it will explode it before adding the ticks. |
596
|
|
|
* |
597
|
|
|
* @uses validateFieldName() |
598
|
|
|
* @param string $value |
599
|
|
|
* The value to surrounded with ticks |
600
|
|
|
* @return string |
601
|
|
|
* The value surrounded by ticks |
602
|
|
|
*/ |
603
|
|
|
final public function tickString($value) |
604
|
|
|
{ |
605
|
|
|
General::ensureType([ |
606
|
|
|
'value' => ['var' => $value, 'type' => 'string'], |
607
|
|
|
]); |
608
|
|
|
$value = str_replace('`', '', $value); |
609
|
|
|
$this->validateFieldName($value); |
610
|
|
|
if (strpos($value, '.') !== false) { |
611
|
|
|
return implode('.', array_map([$this, 'asTickedString'], explode('.', $value))); |
612
|
|
|
} |
613
|
|
|
return "`$value`"; |
614
|
|
|
} |
615
|
|
|
|
616
|
|
|
/** |
617
|
|
|
* @internal Splits the arguments of function calls. |
618
|
|
|
* Arguments are only separated: no formatting is made. |
619
|
|
|
* Each value should to pass to asTickedString() before being used in SQL queries. |
620
|
|
|
* |
621
|
|
|
* @param string $arguments |
622
|
|
|
* The argument string to parse |
623
|
|
|
* @return array |
624
|
|
|
* The arguments array |
625
|
|
|
*/ |
626
|
|
|
final public function splitFunctionArguments($arguments) |
627
|
|
|
{ |
628
|
|
|
$arguments = str_split($arguments); |
629
|
|
|
$current = []; |
630
|
|
|
$args = []; |
631
|
|
|
$openParenthesisCount = 0; |
632
|
|
|
foreach ($arguments as $char) { |
633
|
|
|
if (!trim($char)) { |
634
|
|
|
continue; |
635
|
|
|
} elseif ($openParenthesisCount === 0 && $char === self::FCT_ARGS_DELIMITER) { |
636
|
|
|
if (!empty($current)) { |
637
|
|
|
$args[] = implode('', $current); |
638
|
|
|
} |
639
|
|
|
$current = []; |
640
|
|
|
continue; |
641
|
|
|
} |
642
|
|
|
$current[] = $char; |
643
|
|
|
if ($char === '(') { |
644
|
|
|
$openParenthesisCount++; |
645
|
|
|
} elseif ($char === ')') { |
646
|
|
|
$openParenthesisCount--; |
647
|
|
|
} |
648
|
|
|
} |
649
|
|
|
if ($openParenthesisCount !== 0) { |
650
|
|
|
throw new DatabaseStatementException('Imbalanced number of parenthesis in function arguments'); |
651
|
|
|
} |
652
|
|
|
if (!empty($current)) { |
653
|
|
|
$args[] = implode('', $current); |
654
|
|
|
} |
655
|
|
|
return $args; |
656
|
|
|
} |
657
|
|
|
|
658
|
|
|
/** |
659
|
|
|
* @internal |
660
|
|
|
* Given some value, it will create a ticked string, i.e. "`string`". |
661
|
|
|
* If the $value parameter is: |
662
|
|
|
* 1. an array, it will call asPlaceholdersList(); |
663
|
|
|
* 2. the string '*', it will keep it as is; |
664
|
|
|
* 3. a string matching a function call, it will parse it; |
665
|
|
|
* 4. a string with a mathematical operator (+, -, *, /), it will parse it; |
666
|
|
|
* 5. a string with comma, it will explode that string and call |
667
|
|
|
* asTickedList() with the resulting array; |
668
|
|
|
* 6. a string starting with a colon, it will be used as named parameter; |
669
|
|
|
* 7. a plain string, it will surround all words with ticks. |
670
|
|
|
* |
671
|
|
|
* For other type of value, it will throw an Exception. |
672
|
|
|
* |
673
|
|
|
* @see asTickedList() |
674
|
|
|
* @uses tickString() |
675
|
|
|
* @uses splitFunctionArguments() |
676
|
|
|
* @param string|array $value |
677
|
|
|
* The value or list of values to surround with ticks. |
678
|
|
|
* @param string $alias |
679
|
|
|
* Used as an alias, create `x` AS `y` expressions. |
680
|
|
|
* @return string |
681
|
|
|
* The resulting ticked string or list |
682
|
|
|
* @throws Exception |
683
|
|
|
*/ |
684
|
|
|
final public function asTickedString($value, $alias = null) |
685
|
|
|
{ |
686
|
|
|
if (!$value) { |
687
|
|
|
return ''; |
688
|
|
|
} |
689
|
|
|
// 1. deal with array |
690
|
|
|
if (is_array($value)) { |
691
|
|
|
return $this->asTickedList($value); |
692
|
|
|
} |
693
|
|
|
General::ensureType([ |
694
|
|
|
'value' => ['var' => $value, 'type' => 'string'], |
695
|
|
|
]); |
696
|
|
|
|
697
|
|
|
$fctMatches = []; |
698
|
|
|
$opMatches = []; |
699
|
|
|
$value = trim($value); |
700
|
|
|
|
701
|
|
|
// 2. '*' |
702
|
|
|
if ($value === '*') { |
703
|
|
|
return $value; |
704
|
|
|
// 3. function |
705
|
|
|
} elseif (preg_match(self::FCT_PATTERN, $value, $fctMatches) === 1) { |
706
|
|
|
$args = $this->splitFunctionArguments($fctMatches[2]); |
707
|
|
|
$fxCall = $fctMatches[1] . '(' . $this->asTickedList($args) . ')'; |
708
|
|
|
if ($alias) { |
709
|
|
|
$alias = $this->tickString($alias); |
710
|
|
|
return "$fxCall AS $alias"; |
711
|
|
|
} |
712
|
|
|
return $fxCall; |
713
|
|
|
// 4. op |
714
|
|
|
} elseif (preg_match(self::OP_PATTERN, $value, $opMatches) === 1) { |
715
|
|
|
$op = $opMatches[1]; |
716
|
|
|
if (!$op) { |
717
|
|
|
throw new DatabaseStatementException("Failed to parse operator in `$value`"); |
718
|
|
|
} |
719
|
|
|
$parts = array_map('trim', explode($op, $value, 2)); |
720
|
|
|
$parts = array_map(function ($p) { |
721
|
|
|
// TODO: add support for params |
722
|
|
|
$ip = General::intval($p); |
723
|
|
|
return $ip === -1 ? $this->asTickedString($p) : "$ip"; |
724
|
|
|
}, $parts); |
725
|
|
|
$value = implode(" $op ", $parts); |
726
|
|
|
if ($alias) { |
727
|
|
|
$alias = $this->tickString($alias); |
728
|
|
|
return "($value) AS $alias"; |
729
|
|
|
} |
730
|
|
|
return $value; |
731
|
|
|
// 5. comma |
732
|
|
|
} elseif (strpos($value, self::VALUES_DELIMITER) !== false) { |
733
|
|
|
return $this->asTickedList(explode(self::VALUES_DELIMITER, $value)); |
734
|
|
|
// 6. colon |
735
|
|
|
} elseif (strpos($value, ':') === 0) { |
736
|
|
|
$this->validateFieldName(substr($value, 1)); |
737
|
|
|
return $value; |
738
|
|
|
} |
739
|
|
|
|
740
|
|
|
// 7. plain string |
741
|
|
|
$value = $this->tickString($value); |
742
|
|
|
if ($alias) { |
743
|
|
|
$alias = $this->tickString($alias); |
744
|
|
|
return "$value AS $alias"; |
745
|
|
|
} |
746
|
|
|
return $value; |
747
|
|
|
} |
748
|
|
|
|
749
|
|
|
/** |
750
|
|
|
* @internal |
751
|
|
|
* Given an array, this method will call asTickedString() on each values and |
752
|
|
|
* then implode the results with LIST_DELIMITER. |
753
|
|
|
* If the array contains named keys, they become the value and the value in the array |
754
|
|
|
* is used as an alias, create `x` AS `y` expressions. |
755
|
|
|
* |
756
|
|
|
* @see asTickedString() |
757
|
|
|
* @param array $values |
758
|
|
|
* @return string |
759
|
|
|
* The resulting list of ticked strings |
760
|
|
|
*/ |
761
|
|
|
final public function asTickedList(array $values) |
762
|
|
|
{ |
763
|
|
|
return implode(self::LIST_DELIMITER, General::array_map(function ($key, $value) { |
|
|
|
|
764
|
|
|
if (General::intval($key) === -1) { |
765
|
|
|
return $this->asTickedString($key, $value); |
766
|
|
|
} |
767
|
|
|
return $this->asTickedString($value); |
768
|
|
|
}, $values)); |
769
|
|
|
} |
770
|
|
|
|
771
|
|
|
/** |
772
|
|
|
* @internal |
773
|
|
|
* Given an array, this method will call asTickedList() on each values and |
774
|
|
|
* then implode the results with LIST_DELIMITER. |
775
|
|
|
* If the value is a DatabaseQuery object, the key is used as the alias. |
776
|
|
|
* |
777
|
|
|
* @see asTickedList() |
778
|
|
|
* @param array $values |
779
|
|
|
* @return string |
780
|
|
|
* The resulting list of ticked strings |
781
|
|
|
*/ |
782
|
|
|
final public function asProjectionList(array $values) |
783
|
|
|
{ |
784
|
|
|
return implode(self::LIST_DELIMITER, General::array_map(function ($key, $value) { |
|
|
|
|
785
|
|
|
if ($value instanceof DatabaseSubQuery) { |
786
|
|
|
$sql = $value->generateSQL(); |
787
|
|
|
$key = $this->asTickedString($key); |
788
|
|
|
return "($sql) AS $key"; |
789
|
|
|
} |
790
|
|
|
return $this->asTickedList([$key => $value]); |
791
|
|
|
}, $values)); |
792
|
|
|
} |
793
|
|
|
|
794
|
|
|
/** |
795
|
|
|
* @internal |
796
|
|
|
* This method validates that the string $field is a valid field name |
797
|
|
|
* in SQL. If it is not, it throws DatabaseStatementException |
798
|
|
|
* |
799
|
|
|
* @param string $field |
800
|
|
|
* @return void |
801
|
|
|
* @throws DatabaseStatementException |
802
|
|
|
* @throws Exception |
803
|
|
|
*/ |
804
|
|
|
final protected function validateFieldName($field) |
805
|
|
|
{ |
806
|
|
|
General::ensureType([ |
807
|
|
|
'field' => ['var' => $field, 'type' => 'string'], |
808
|
|
|
]); |
809
|
|
|
if (preg_match('/^[0-9a-zA-Z_]+$/', $field) === false) { |
810
|
|
|
throw new DatabaseStatementException( |
811
|
|
|
"Field name '$field' is not valid since it contains illegal characters" |
812
|
|
|
); |
813
|
|
|
} |
814
|
|
|
} |
815
|
|
|
|
816
|
|
|
/** |
817
|
|
|
* @internal |
818
|
|
|
* This function converts a valid field name into a suitable value |
819
|
|
|
* to use as a SQL parameter name. |
820
|
|
|
* It also makes sure that the returned parameter name is not currently used |
821
|
|
|
* for the specified $field, $value pair. |
822
|
|
|
* |
823
|
|
|
* @see formatParameterName() |
824
|
|
|
* @see validateFieldName() |
825
|
|
|
* @see appendValues() |
826
|
|
|
* @param string $field |
827
|
|
|
* The field name, as passed in the public API of the statement |
828
|
|
|
* @param mixed $value |
829
|
|
|
* The associated value for this field |
830
|
|
|
* @return string |
831
|
|
|
* The sanitized parameter name |
832
|
|
|
*/ |
833
|
|
|
final public function convertToParameterName($field, $value) |
834
|
|
|
{ |
835
|
|
|
General::ensureType([ |
836
|
|
|
'value' => ['var' => $field, 'type' => 'string'], |
837
|
|
|
]); |
838
|
|
|
$field = str_replace(['-', '.'], '_', $field); |
839
|
|
|
$field = preg_replace('/[^0-9a-zA-Z_]+/', '', $field); |
840
|
|
|
$field = $this->formatParameterName($field); |
841
|
|
|
|
842
|
|
|
$uniqueParameterKey = sha1(serialize($field) . serialize($value)); |
843
|
|
|
// Have we seen this (field, value) pair ? |
844
|
|
|
if (isset($this->parameters[$uniqueParameterKey])) { |
845
|
|
|
return $this->parameters[$uniqueParameterKey]; |
846
|
|
|
} |
847
|
|
|
// Have we seen this field ? |
848
|
|
|
$fieldLookup = $field; |
849
|
|
|
$fieldCount = 1; |
850
|
|
|
while (isset($this->parameters[$fieldLookup])) { |
851
|
|
|
$fieldCount++; |
852
|
|
|
$fieldLookup = "$field$fieldCount"; |
853
|
|
|
} |
854
|
|
|
// Special case for null |
855
|
|
|
if ($value === null) { |
856
|
|
|
$fieldLookup = "_null_"; |
857
|
|
|
} |
858
|
|
|
// Saved both for later |
859
|
|
|
$this->parameters[$uniqueParameterKey] = $this->parameters[$fieldLookup] = $fieldLookup; |
860
|
|
|
|
861
|
|
|
return $fieldLookup; |
862
|
|
|
} |
863
|
|
|
|
864
|
|
|
/** |
865
|
|
|
* @internal |
866
|
|
|
* Formats the given $parameter name to be used as SQL parameter. |
867
|
|
|
* |
868
|
|
|
* @param string $parameter |
869
|
|
|
* The parameter name |
870
|
|
|
* @return string |
871
|
|
|
* The formatted parameter name |
872
|
|
|
*/ |
873
|
|
|
protected function formatParameterName($parameter) |
874
|
|
|
{ |
875
|
|
|
return $parameter; |
876
|
|
|
} |
877
|
|
|
} |
878
|
|
|
|