1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* Class DbQueryConstructor |
5
|
|
|
* |
6
|
|
|
* @method DbQueryConstructor fields(mixed $value, int $mergeStrategy = HelperArray::MERGE_PHP) |
7
|
|
|
* @method DbQueryConstructor join(mixed $value, int $mergeStrategy = HelperArray::MERGE_PHP) |
8
|
|
|
* @method DbQueryConstructor where(array $value, int $mergeStrategy = HelperArray::MERGE_PHP) |
9
|
|
|
* @method DbQueryConstructor groupBy(array $value, int $mergeStrategy = HelperArray::MERGE_PHP) |
10
|
|
|
* @method DbQueryConstructor orderBy(array $value, int $mergeStrategy = HelperArray::MERGE_PHP) |
11
|
|
|
* @method DbQueryConstructor having(mixed $value, int $mergeStrategy = HelperArray::MERGE_PHP) |
12
|
|
|
* @method DbQueryConstructor setFetchOne(bool $fetchOne = true) |
13
|
|
|
* @method DbQueryConstructor setForUpdate(bool $forUpdate = true) |
14
|
|
|
* @method DbQueryConstructor setSkipLock(bool $skipLock = true) |
15
|
|
|
* |
16
|
|
|
*/ |
17
|
|
|
class DbQueryConstructor extends DbSqlAware { |
18
|
|
|
|
19
|
|
|
const SELECT = 'SELECT'; |
20
|
|
|
const INSERT = 'INSERT'; |
21
|
|
|
const UPDATE = 'UPDATE'; |
22
|
|
|
const DELETE = 'DELETE'; |
23
|
|
|
const REPLACE = 'REPLACE'; |
24
|
|
|
|
25
|
|
|
protected static $allowedOperations = array( |
26
|
|
|
self::SELECT, |
27
|
|
|
); |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* List of array properties names that should be merged on each call of setter |
31
|
|
|
* |
32
|
|
|
* @var string[] $propListArrayMerge |
33
|
|
|
*/ |
34
|
|
|
protected static $propListArrayMerge = array('fields', 'join', 'where', 'groupBy', 'orderBy', 'having'); |
35
|
|
|
/** |
36
|
|
|
* List of setters that will simple set property with default value TRUE |
37
|
|
|
* |
38
|
|
|
* @var string[] $propListSetDefaultTrue |
39
|
|
|
*/ |
40
|
|
|
protected static $propListSetDefaultTrue = array('setFetchOne', 'setForUpdate', 'setSkipLock'); |
41
|
|
|
|
42
|
|
|
public $operation = ''; |
43
|
|
|
|
44
|
|
|
public $table = ''; |
45
|
|
|
public $alias = ''; |
46
|
|
|
|
47
|
|
|
public $idField = ''; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* @var array |
51
|
|
|
*/ |
52
|
|
|
public $fields = array(); |
53
|
|
|
|
54
|
|
|
public $join = array(); |
55
|
|
|
|
56
|
|
|
public $where = array(); |
57
|
|
|
public $groupBy = array(); |
58
|
|
|
public $orderBy = array(); |
59
|
|
|
public $having = array(); |
60
|
|
|
|
61
|
|
|
public $limit = 0; |
62
|
|
|
public $offset = 0; |
63
|
|
|
|
64
|
|
|
public $fetchOne = false; |
65
|
|
|
public $forUpdate = false; |
66
|
|
|
public $skipLock = false; |
67
|
|
|
|
68
|
|
|
public $variables = array(); |
69
|
|
|
|
70
|
|
|
protected $_compiledQuery = array(); |
71
|
|
|
|
72
|
|
|
/** |
73
|
|
|
* @param string $fieldName |
74
|
|
|
* |
75
|
|
|
* @return $this |
76
|
|
|
*/ |
77
|
2 |
|
public function setIdField($fieldName) { |
78
|
2 |
|
$this->idField = $fieldName; |
79
|
|
|
|
80
|
2 |
|
return $this; |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* Sets internal variables |
85
|
|
|
* |
86
|
|
|
* @param array $values |
87
|
|
|
* |
88
|
|
|
* @return $this |
89
|
|
|
*/ |
90
|
|
|
public function variables($values) { |
91
|
|
|
$this->variables = $values; |
92
|
|
|
|
93
|
|
|
return $this; |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* @param string $alias |
99
|
|
|
* |
100
|
|
|
* @return $this |
101
|
|
|
*/ |
102
|
1 |
|
public function setAlias($alias) { |
103
|
1 |
|
$this->alias = $alias; |
104
|
|
|
|
105
|
1 |
|
return $this; |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
/** |
109
|
|
|
* @return $this |
110
|
|
|
*/ |
111
|
3 |
|
public function select() { |
112
|
3 |
|
$this->operation = DbQueryConstructor::SELECT; |
113
|
|
|
// if (empty($this->fields) && $initFields) { |
|
|
|
|
114
|
|
|
// $this->fields = array('*'); |
115
|
|
|
// } |
116
|
|
|
|
117
|
3 |
|
return $this; |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
|
121
|
|
|
/** |
122
|
|
|
* @param string $tableName |
123
|
|
|
* @param string $alias |
124
|
|
|
* |
125
|
|
|
* @return $this |
126
|
|
|
*/ |
127
|
2 |
|
public function from($tableName, $alias = '') { |
128
|
2 |
|
$this->table = $tableName; |
129
|
2 |
|
$this->setAlias($alias); |
130
|
|
|
|
131
|
2 |
|
return $this; |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
/** |
135
|
|
|
* @param mixed ... |
136
|
|
|
* |
137
|
|
|
* @return $this |
138
|
|
|
*/ |
139
|
1 |
|
public function field() { |
140
|
1 |
|
$arguments = func_get_args(); |
141
|
|
|
|
142
|
|
|
// Special case - call method with array of fields |
143
|
1 |
|
if (count($arguments) == 1 && is_array($arguments[0])) { |
144
|
|
|
$arguments = array_shift($arguments); |
145
|
|
|
} |
146
|
|
|
|
147
|
1 |
|
foreach ($arguments as $arg) { |
|
|
|
|
148
|
1 |
|
$this->fields[] = $arg; |
149
|
1 |
|
} |
150
|
|
|
|
151
|
1 |
|
return $this; |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
public function fieldLiteral($field = '0', $alias = DbSqlLiteral::SQL_LITERAL_ALIAS_NONE) { |
|
|
|
|
155
|
|
|
return $this->field(DbSqlLiteral::build($this->db)->literal($field)); |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
public function fieldSingleFunction($functionName, $field = '*', $alias = DbSqlLiteral::SQL_LITERAL_ALIAS_NONE) { |
159
|
|
|
return $this->field(DbSqlLiteral::build($this->db)->buildSingleArgument($functionName, $field, $alias)); |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
public function fieldCount($field = '*', $alias = DbSqlLiteral::SQL_LITERAL_ALIAS_NONE) { |
163
|
|
|
return $this->field(DbSqlLiteral::build($this->db)->count($field, $alias)); |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
public function fieldIsNull($field = '*', $alias = DbSqlLiteral::SQL_LITERAL_ALIAS_NONE) { |
167
|
|
|
return $this->field(DbSqlLiteral::build($this->db)->isNull($field, $alias)); |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
public function fieldMax($field = '*', $alias = DbSqlLiteral::SQL_LITERAL_ALIAS_NONE) { |
171
|
|
|
return $this->field(DbSqlLiteral::build($this->db)->max($field, $alias)); |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
/** |
175
|
|
|
* @param string $name |
176
|
|
|
* @param mixed[] $arguments |
177
|
|
|
* |
178
|
|
|
* @return $this |
179
|
|
|
*/ |
180
|
4 |
|
public function __call($name, $arguments) { |
181
|
4 |
|
if (in_array($name, self::$propListArrayMerge)) { |
182
|
|
|
// array_unshift($arguments, ''); |
|
|
|
|
183
|
|
|
// $arguments[0] = &$this->$name; |
184
|
|
|
// call_user_func_array('HelperArray::merge', $arguments); |
185
|
3 |
|
HelperArray::merge($this->$name, $arguments[0], HelperArray::keyExistsOr($arguments, 1, HelperArray::MERGE_PHP)); |
186
|
4 |
|
} elseif (in_array($name, self::$propListSetDefaultTrue)) { |
187
|
2 |
|
$varName = lcfirst(substr($name, 3)); |
188
|
2 |
|
if (!array_key_exists(0, $arguments)) { |
189
|
2 |
|
$arguments[0] = true; |
190
|
2 |
|
} |
191
|
2 |
|
$this->$varName = $arguments[0]; |
192
|
2 |
|
} |
193
|
|
|
// TODO - make all setters protected ?? |
|
|
|
|
194
|
|
|
// elseif(method_exists($this, $name)) { |
195
|
|
|
// call_user_func_array(array($this, $name), $arguments); |
196
|
|
|
// } |
197
|
|
|
|
198
|
4 |
|
return $this; |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
/** |
202
|
|
|
* @param int $limit |
203
|
|
|
* |
204
|
|
|
* @return $this |
205
|
|
|
*/ |
206
|
2 |
|
public function limit($limit) { |
207
|
2 |
|
$this->limit = is_numeric($limit) ? $limit : 0; |
|
|
|
|
208
|
|
|
|
209
|
2 |
|
return $this; |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
/** |
213
|
|
|
* @param int $offset |
214
|
|
|
* |
215
|
|
|
* @return $this |
216
|
|
|
*/ |
217
|
2 |
|
public function offset($offset) { |
218
|
2 |
|
$this->offset = is_numeric($offset) ? $offset : 0; |
|
|
|
|
219
|
|
|
|
220
|
2 |
|
return $this; |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
/** |
224
|
|
|
* @param string $className |
225
|
|
|
* |
226
|
|
|
* @return $this |
227
|
|
|
*/ |
228
|
|
|
// TODO - rewrite |
229
|
2 |
|
public function getParamsFromStaticClass($className) { |
230
|
2 |
|
if (is_string($className) && $className && class_exists($className)) { |
231
|
2 |
|
if(method_exists($className, 'getDb')) { |
232
|
2 |
|
$this->setDb($className::getDb()); |
233
|
2 |
|
} |
234
|
2 |
|
$this->from($className::$_table); |
235
|
2 |
|
$this->setIdField($className::$_idField); |
236
|
2 |
|
} |
237
|
|
|
|
238
|
2 |
|
return $this; |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
/** |
242
|
|
|
* @param db_mysql|null $db |
243
|
|
|
* @param string|object|DBStaticRecord|DbSqlAware $className |
244
|
|
|
* |
245
|
|
|
* @return static |
246
|
|
|
*/ |
247
|
1 |
|
public static function build($db = null, $className = '') { |
248
|
|
|
/** |
249
|
|
|
* @var static $result |
250
|
|
|
*/ |
251
|
1 |
|
$result = parent::build(null); |
252
|
1 |
|
$result->getParamsFromStaticClass($className); |
253
|
|
|
|
254
|
1 |
|
return $result; |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
/** |
258
|
|
|
* Resets statement |
259
|
|
|
* |
260
|
|
|
* @param bool $full |
261
|
|
|
* |
262
|
|
|
* @return static |
263
|
|
|
*/ |
264
|
1 |
|
protected function _reset($full = true) { |
265
|
1 |
|
if ($full) { |
266
|
1 |
|
$this->operation = ''; |
267
|
1 |
|
$this->table = ''; |
268
|
1 |
|
$this->alias = ''; |
269
|
1 |
|
$this->idField = ''; |
270
|
1 |
|
} |
271
|
|
|
|
272
|
1 |
|
$this->fields = array(); |
273
|
1 |
|
$this->where = array(); |
274
|
1 |
|
$this->groupBy = array(); |
275
|
1 |
|
$this->orderBy = array(); |
276
|
1 |
|
$this->having = array(); |
277
|
|
|
|
278
|
1 |
|
$this->limit = 0; |
279
|
1 |
|
$this->offset = 0; |
280
|
|
|
|
281
|
1 |
|
$this->fetchOne = false; |
282
|
1 |
|
$this->forUpdate = false; |
283
|
1 |
|
$this->skipLock = false; |
284
|
|
|
|
285
|
1 |
|
return $this; |
286
|
|
|
} |
287
|
|
|
|
288
|
|
|
|
289
|
|
|
/** |
290
|
|
|
* @param array $array |
291
|
|
|
* |
292
|
|
|
* @return array |
293
|
|
|
*/ |
294
|
|
|
protected function arrayEscape(&$array) { |
295
|
|
|
$result = array(); |
296
|
|
|
foreach ($array as $key => &$value) { |
297
|
|
|
$result[$key] = $this->escapeString($value); |
298
|
|
|
} |
299
|
|
|
|
300
|
|
|
return $result; |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
protected function compileOperation() { |
304
|
|
|
if (empty($this->operation)) { |
305
|
|
|
throw new ExceptionDbOperationEmpty(); |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
if (!in_array($this->operation, self::$allowedOperations)) { |
309
|
|
|
throw new ExceptionDbOperationRestricted(); |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
$this->_compiledQuery[] = $this->escapeString($this->operation); |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
protected function compileSubject() { |
316
|
|
|
$this->_compiledQuery[] = $this->selectFieldsToString($this->fields); |
317
|
|
|
} |
318
|
|
|
|
319
|
|
|
protected function compileFrom() { |
320
|
|
|
$this->_compiledQuery[] = 'FROM `{{' . $this->escapeString($this->table) . '}}`'; |
321
|
|
|
if (!empty($this->alias)) { |
322
|
|
|
$this->_compiledQuery[] = 'AS `' . $this->escapeString($this->alias) . '`'; |
323
|
|
|
} |
324
|
|
|
} |
325
|
|
|
|
326
|
|
|
protected function compileJoin() { |
327
|
|
|
!empty($this->join) ? $this->_compiledQuery[] = implode(' ', $this->join) : false; |
328
|
|
|
} |
329
|
|
|
|
330
|
|
|
protected function compileWhere() { |
331
|
|
|
// TODO - fields should be escaped !! |
332
|
|
|
!empty($this->where) ? $this->_compiledQuery[] = 'WHERE ' . implode(' AND ', $this->where) : false; |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
protected function compileGroupBy() { |
336
|
|
|
// TODO - fields should be escaped !! |
|
|
|
|
337
|
|
|
// !empty($this->groupBy) ? $this->_compiledQuery[] = 'GROUP BY ' . implode(',', $this->arrayEscape($this->groupBy)) : false; |
338
|
|
|
!empty($this->groupBy) ? $this->_compiledQuery[] = 'GROUP BY ' . $this->selectFieldsToString($this->groupBy) : false; |
339
|
|
|
} |
340
|
|
|
|
341
|
|
|
protected function compileOrderBy() { |
342
|
|
|
// TODO - fields should be escaped !! |
343
|
|
|
!empty($this->orderBy) ? $this->_compiledQuery[] = 'ORDER BY ' . implode(',', $this->arrayEscape($this->orderBy)) : false; |
344
|
|
|
} |
345
|
|
|
|
346
|
|
|
protected function compileHaving() { |
347
|
|
|
// TODO - fields should be escaped !! |
348
|
|
|
!empty($this->having) ? $this->_compiledQuery[] = 'HAVING ' . implode(' AND ', $this->having) : false; |
349
|
|
|
} |
350
|
|
|
|
351
|
|
|
protected function compileLimit() { |
352
|
|
|
// TODO - fields should be escaped !! |
353
|
|
|
if ($limit = $this->fetchOne ? 1 : $this->limit) { |
354
|
|
|
$this->_compiledQuery[] = 'LIMIT ' . $limit . (!empty($this->offset) ? ' OFFSET ' . $this->offset : ''); |
355
|
|
|
} |
356
|
|
|
} |
357
|
|
|
|
358
|
|
|
protected function compileForUpdate() { |
359
|
|
|
$this->_compiledQuery[] = |
360
|
|
|
// forUpdate flag forces select with row locking - didn't look at skipLock flag |
361
|
|
|
$this->forUpdate |
362
|
|
|
|| |
363
|
|
|
// Also row locked when transaction is up and skipLock flag is not set |
364
|
|
|
(classSupernova::db_transaction_check(false) && !$this->skipLock) ? 'FOR UPDATE' : ''; |
365
|
|
|
} |
366
|
|
|
|
367
|
|
|
/** |
368
|
|
|
* @param array|mixed $fields |
369
|
|
|
* |
370
|
|
|
* @return string |
371
|
|
|
* @throws ExceptionDBFieldEmpty |
372
|
|
|
*/ |
373
|
16 |
|
protected function selectFieldsToString($fields) { |
374
|
16 |
|
HelperArray::makeArrayRef($fields); |
375
|
|
|
|
376
|
16 |
|
$result = array(); |
377
|
16 |
|
foreach ($fields as $fieldName) { |
378
|
15 |
|
$string = $this->processField($fieldName); |
379
|
15 |
|
if ($string !== '') { |
380
|
13 |
|
$result[] = $string; |
381
|
13 |
|
} |
382
|
16 |
|
} |
383
|
|
|
|
384
|
16 |
|
if (empty($result)) { |
385
|
3 |
|
throw new ExceptionDBFieldEmpty(); |
386
|
|
|
} |
387
|
|
|
|
388
|
13 |
|
return implode(',', $result); |
389
|
|
|
} |
390
|
|
|
|
391
|
|
|
/** |
392
|
|
|
* @param mixed $fieldName |
393
|
|
|
* |
394
|
|
|
* @return string |
395
|
|
|
*/ |
396
|
13 |
|
protected function processField($fieldName) { |
397
|
13 |
|
if (is_bool($fieldName)) { |
398
|
4 |
|
$result = (string)intval($fieldName); |
399
|
13 |
|
} elseif (is_numeric($fieldName)) { |
400
|
5 |
|
$result = $fieldName; |
401
|
11 |
|
} elseif (is_null($fieldName)) { |
402
|
2 |
|
$result = 'NULL'; |
403
|
2 |
|
} else { |
404
|
|
|
// Field has other type - string or should be convertible to string |
405
|
6 |
|
$result = (string)$fieldName; |
406
|
6 |
|
if (!$fieldName instanceof DbSqlLiteral) { |
407
|
5 |
|
$result = $this->quoteField($fieldName); |
|
|
|
|
408
|
5 |
|
} |
409
|
|
|
} |
410
|
|
|
|
411
|
13 |
|
return $result; |
412
|
|
|
} |
413
|
|
|
|
414
|
|
|
|
415
|
|
|
/** |
416
|
|
|
* @return string |
417
|
|
|
* @throws ExceptionDbOperationEmpty |
418
|
|
|
* @throws ExceptionDbOperationRestricted |
419
|
|
|
*/ |
420
|
2 |
|
public function __toString() { |
421
|
2 |
|
$this->_compiledQuery = array(); |
422
|
|
|
|
423
|
2 |
|
$this->compileOperation(); |
424
|
|
|
$this->compileSubject(); |
425
|
|
|
$this->compileFrom(); |
426
|
|
|
$this->compileJoin(); |
427
|
|
|
$this->compileWhere(); |
428
|
|
|
$this->compileGroupBy(); |
429
|
|
|
$this->compileOrderBy(); |
430
|
|
|
$this->compileHaving(); |
431
|
|
|
$this->compileLimit(); |
432
|
|
|
$this->compileForUpdate(); |
433
|
|
|
|
434
|
|
|
return implode(' ', $this->_compiledQuery); |
435
|
|
|
} |
436
|
|
|
|
437
|
|
|
/** |
438
|
|
|
* @param bool $skip_query_check |
439
|
|
|
* |
440
|
|
|
* @return DbEmptyIterator|DbMysqliResultIterator |
441
|
|
|
*/ |
442
|
|
|
public function selectIterator($skip_query_check = false) { |
443
|
|
|
return $this->getDb()->doQueryIterator($this->select()->__toString(), $skip_query_check); |
444
|
|
|
} |
445
|
|
|
|
446
|
|
|
/** |
447
|
|
|
* @param bool $skip_query_check |
448
|
|
|
* |
449
|
|
|
* @return array |
450
|
|
|
*/ |
451
|
|
|
public function selectRow($skip_query_check = false) { |
452
|
|
|
$result = $this->getDb()->doQueryFetch($this->select()->setFetchOne()->__toString(), $skip_query_check); |
453
|
|
|
|
454
|
|
|
return is_array($result) ? $result : array(); |
455
|
|
|
} |
456
|
|
|
|
457
|
|
|
/** |
458
|
|
|
* @param bool $skip_query_check |
459
|
|
|
* |
460
|
|
|
* @return mixed|null |
461
|
|
|
*/ |
462
|
|
|
public function selectValue($skip_query_check = false) { |
463
|
|
|
$result = $this->selectRow($skip_query_check); |
464
|
|
|
|
465
|
|
|
return is_array($result) ? reset($result) : null; |
466
|
|
|
} |
467
|
|
|
|
468
|
|
|
} |
469
|
|
|
|
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.