1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* This file contains a class to quickly generate database queries for models |
4
|
|
|
* |
5
|
|
|
* @package BZiON\Models\QueryBuilder |
6
|
|
|
* @license https://github.com/allejo/bzion/blob/master/LICENSE.md GNU General Public License Version 3 |
7
|
|
|
*/ |
8
|
|
|
|
9
|
|
|
/** |
10
|
|
|
* This class can be used to search for models with specific characteristics in |
11
|
|
|
* the database. |
12
|
|
|
* |
13
|
|
|
* Note that most methods of this class return itself, so that you can easily |
14
|
|
|
* add a number of different filters. |
15
|
|
|
* |
16
|
|
|
* <code> |
17
|
|
|
* return Team::getQueryBuilder() |
18
|
|
|
* ->active() |
19
|
|
|
* ->where('name')->startsWith('a') |
20
|
|
|
* ->sortBy('name')->reverse() |
21
|
|
|
* ->getModels(); |
22
|
|
|
* </code> |
23
|
|
|
* |
24
|
|
|
* @package BZiON\Models\QueryBuilder |
25
|
|
|
*/ |
26
|
|
|
class QueryBuilder implements Countable |
27
|
|
|
{ |
28
|
|
|
const COL_HAVING = 'having'; |
29
|
|
|
const COL_WHERE = 'where'; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* The type of the model we're building a query for |
33
|
|
|
* @var string |
34
|
|
|
*/ |
35
|
|
|
protected $type; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* The columns that the model provided us |
39
|
|
|
* @var array |
40
|
|
|
*/ |
41
|
|
|
protected $columns = array('id' => 'id'); |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* Extra columns that are generated from the SQL query (this should be a comma separated string or null) |
45
|
|
|
* @var string|null |
46
|
|
|
*/ |
47
|
|
|
protected $extraColumns = null; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* The conditions to include in WHERE |
51
|
|
|
* @var string[] |
52
|
|
|
*/ |
53
|
|
|
protected $whereConditions = array(); |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* The conditions to include in HAVING |
57
|
|
|
* @var string[] |
58
|
|
|
*/ |
59
|
|
|
protected $havingConditions = array(); |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* The MySQL value parameters |
63
|
|
|
* @var array |
64
|
|
|
*/ |
65
|
|
|
protected $parameters = array(); |
66
|
|
|
|
67
|
|
|
/** |
68
|
|
|
* The MySQL value parameters for pagination |
69
|
|
|
* @var array |
70
|
|
|
*/ |
71
|
|
|
protected $paginationParameters = array(); |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* Extra MySQL query string to pass |
75
|
|
|
* @var string |
76
|
|
|
*/ |
77
|
|
|
protected $extras = ''; |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
* Extra MySQL query groupby string to pass |
81
|
|
|
* @var string |
82
|
|
|
*/ |
83
|
|
|
protected $groupQuery = ''; |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* A column based on which we should sort the results |
87
|
|
|
* @var string|null |
88
|
|
|
*/ |
89
|
|
|
private $sortBy = null; |
90
|
|
|
|
91
|
|
|
/** |
92
|
|
|
* Whether to reverse the results |
93
|
|
|
* @var bool |
94
|
|
|
*/ |
95
|
|
|
private $reverseSort = false; |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* The currently selected column |
99
|
|
|
* @var string|null |
100
|
|
|
*/ |
101
|
|
|
private $currentColumn = null; |
102
|
|
|
|
103
|
|
|
/** |
104
|
|
|
* Either 'where' or 'having' |
105
|
|
|
* @var string |
106
|
|
|
*/ |
107
|
|
|
private $currentColumnMode = ''; |
108
|
|
|
|
109
|
|
|
/** |
110
|
|
|
* The currently selected column without the table name (unless it was |
111
|
|
|
* explicitly provided) |
112
|
|
|
* @var string|null |
113
|
|
|
*/ |
114
|
|
|
protected $currentColumnRaw = null; |
115
|
|
|
|
116
|
|
|
/** |
117
|
|
|
* A column to consider the name of the model |
118
|
|
|
* @var string|null |
119
|
|
|
*/ |
120
|
|
|
private $nameColumn = null; |
121
|
|
|
|
122
|
|
|
/** |
123
|
|
|
* The page to return |
124
|
|
|
* @var int|null |
125
|
|
|
*/ |
126
|
|
|
private $page = null; |
127
|
|
|
|
128
|
|
|
/** |
129
|
|
|
* Whether the ID of the first/last element has been provided |
130
|
|
|
* @var bool |
131
|
|
|
*/ |
132
|
|
|
private $limited = false; |
133
|
|
|
|
134
|
|
|
/** |
135
|
|
|
* The number of elements on every page |
136
|
|
|
* @var int |
137
|
|
|
*/ |
138
|
|
|
protected $resultsPerPage = 30; |
139
|
|
|
|
140
|
|
|
/** |
141
|
|
|
* Create a new QueryBuilder |
142
|
|
|
* |
143
|
|
|
* A new query builder should be created on a static getQueryBuilder() |
144
|
|
|
* method on each model. The options array can contain the following |
145
|
|
|
* properties: |
146
|
|
|
* |
147
|
|
|
* - `columns`: An associative array - the key of each entry is the column |
148
|
|
|
* name that will be used by other methods, while the value is |
149
|
|
|
* is the column name that is used in the database structure |
150
|
|
|
* |
151
|
|
|
* - `activeStatuses`: If the model has a status column, this should be |
152
|
|
|
* a list of values that make the entry be considered |
153
|
|
|
* "active" |
154
|
|
|
* |
155
|
|
|
* - `name`: The name of the column which represents the name of the object |
156
|
|
|
* |
157
|
|
|
* @param string $type The type of the Model (e.g. "Player" or "Match") |
158
|
|
|
* @param array $options The options to pass to the builder (see above) |
159
|
|
|
*/ |
160
|
27 |
|
public function __construct($type, $options = array()) |
161
|
|
|
{ |
162
|
27 |
|
$this->type = $type; |
163
|
|
|
|
164
|
27 |
|
if (isset($options['columns'])) { |
165
|
27 |
|
$this->columns += $options['columns']; |
166
|
|
|
} |
167
|
|
|
|
168
|
27 |
|
if (isset($options['name'])) { |
169
|
2 |
|
$this->nameColumn = $options['name']; |
170
|
|
|
} |
171
|
27 |
|
} |
172
|
|
|
|
173
|
|
|
/** |
174
|
|
|
* Add a new column to select by in the query. |
175
|
|
|
* |
176
|
|
|
* @param string $alias An alias that can be used in the query builder |
177
|
|
|
* @param string|null $columnName The name of the column we're accessing |
178
|
|
|
* |
179
|
|
|
* @return $this |
180
|
|
|
*/ |
181
|
5 |
|
public function selectColumn($alias, $columnName = null) |
182
|
|
|
{ |
183
|
5 |
|
if ($columnName === null) { |
184
|
|
|
$columnName = $alias; |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
$this->columns[$alias] = $columnName; |
188
|
|
|
|
189
|
|
|
return $this; |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
/** |
193
|
|
|
* Select a column |
194
|
|
|
* |
195
|
|
|
* `$queryBuilder->where('username')->equals('administrator');` |
196
|
|
|
* |
197
|
|
|
* @param string $column The column to select |
198
|
|
|
* @return static |
199
|
|
|
*/ |
200
|
|
|
public function where($column) |
201
|
|
|
{ |
202
|
|
|
return $this->grabColumn($column, 'where'); |
203
|
|
|
} |
204
|
|
|
|
205
|
3 |
|
/** |
206
|
|
|
* Select an alias from an aggregate function |
207
|
3 |
|
* |
208
|
|
|
* `$queryBuilder->where('activity')->greaterThan(0);` |
209
|
3 |
|
* |
210
|
|
|
* @param string $column The column to select |
211
|
|
|
* @return static |
212
|
|
|
*/ |
213
|
|
|
public function having($column) |
214
|
|
|
{ |
215
|
|
|
return $this->grabColumn($column, 'having'); |
216
|
|
|
} |
217
|
|
|
|
218
|
2 |
|
/** |
219
|
|
|
* Request that a column equals a string (case-insensitive) |
220
|
2 |
|
* |
221
|
|
|
* @param string $string The string that the column's value should equal to |
222
|
2 |
|
* @return static |
223
|
|
|
*/ |
224
|
|
|
public function equals($string) |
225
|
|
|
{ |
226
|
|
|
$this->addColumnCondition("= ?", $string); |
227
|
|
|
|
228
|
|
|
return $this; |
229
|
|
|
} |
230
|
1 |
|
|
231
|
|
|
/** |
232
|
1 |
|
* Request that a column doesNOT equals a string (case-insensitive) |
233
|
|
|
* |
234
|
1 |
|
* @param string $string The string that the column's value should equal to |
235
|
|
|
* @return static |
236
|
|
|
*/ |
237
|
|
|
public function notEquals($string) |
238
|
|
|
{ |
239
|
|
|
$this->addColumnCondition("!= ?", $string); |
240
|
|
|
|
241
|
|
|
return $this; |
242
|
|
|
} |
243
|
|
|
|
244
|
|
|
/** |
245
|
|
|
* Request that a column is not null |
246
|
|
|
* |
247
|
|
|
* @return static |
248
|
|
|
*/ |
249
|
|
|
public function isNotNull() |
250
|
|
|
{ |
251
|
|
|
$this->addColumnCondition('IS NOT NULL', null); |
252
|
|
|
|
253
|
|
|
return $this; |
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
/** |
257
|
|
|
* Request that a column is null |
258
|
|
|
* |
259
|
|
|
* @return static |
260
|
|
|
*/ |
261
|
|
|
public function isNull() |
262
|
|
|
{ |
263
|
|
|
$this->addColumnCondition('IS NULL', null); |
264
|
|
|
|
265
|
|
|
return $this; |
266
|
|
|
} |
267
|
|
|
|
268
|
|
|
/** |
269
|
|
|
* Request that a column is greater than a quantity |
270
|
|
|
* |
271
|
|
|
* @param string $quantity The quantity to test against |
272
|
|
|
* @return static |
273
|
|
|
*/ |
274
|
|
|
public function greaterThan($quantity) |
275
|
|
|
{ |
276
|
|
|
$this->addColumnCondition("> ?", $quantity); |
277
|
|
|
|
278
|
|
|
return $this; |
279
|
|
|
} |
280
|
|
|
|
281
|
|
|
/** |
282
|
|
|
* Request that a column is less than a quantity |
283
|
|
|
* |
284
|
|
|
* @param string $quantity The quantity to test against |
285
|
|
|
* @return static |
286
|
|
|
*/ |
287
|
|
|
public function lessThan($quantity) |
288
|
|
|
{ |
289
|
|
|
$this->addColumnCondition("< ?", $quantity); |
290
|
|
|
|
291
|
|
|
return $this; |
292
|
|
|
} |
293
|
|
|
|
294
|
|
|
/** |
295
|
|
|
* Request that a timestamp is before the specified time |
296
|
|
|
* |
297
|
|
|
* @param string|TimeDate $time The timestamp to compare to |
298
|
2 |
|
* @param bool $inclusive Whether to include the given timestamp |
299
|
|
|
* @param bool $reverse Whether to reverse the results |
300
|
2 |
|
* |
301
|
2 |
|
* @return static |
302
|
|
|
*/ |
303
|
|
|
public function isBefore($time, $inclusive = false, $reverse = false) |
304
|
2 |
|
{ |
305
|
2 |
|
return $this->isAfter($time, $inclusive, !$reverse); |
306
|
|
|
} |
307
|
2 |
|
|
308
|
|
|
/** |
309
|
2 |
|
* Request that a timestamp is after the specified time |
310
|
|
|
* |
311
|
|
|
* @param string|TimeDate $time The timestamp to compare to |
312
|
|
|
* @param bool $inclusive Whether to include the given timestamp |
313
|
|
|
* @param bool $reverse Whether to reverse the results |
314
|
|
|
* |
315
|
|
|
* @return static |
316
|
|
|
*/ |
317
|
|
|
public function isAfter($time, $inclusive = false, $reverse = false) |
318
|
|
|
{ |
319
|
|
|
if ($time instanceof TimeDate) { |
320
|
|
|
$time = $time->toMysql(); |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
$comparison = ($reverse) ? '<' : '>'; |
324
|
|
|
$comparison .= ($inclusive) ? '=' : ''; |
325
|
|
|
|
326
|
|
|
$this->addColumnCondition("$comparison ?", $time); |
327
|
|
|
|
328
|
|
|
return $this; |
329
|
|
|
} |
330
|
|
|
|
331
|
|
|
/** |
332
|
1 |
|
* Request that a column equals a number |
333
|
|
|
* |
334
|
1 |
|
* @param int|Model|null $number The number that the column's value should |
335
|
|
|
* equal to. If a Model is provided, use the |
336
|
|
|
* model's ID, while null values are ignored. |
337
|
|
|
* @return static |
338
|
1 |
|
*/ |
339
|
1 |
View Code Duplication |
public function is($number) |
|
|
|
|
340
|
|
|
{ |
341
|
|
|
if ($number === null) { |
342
|
1 |
|
return $this; |
343
|
|
|
} |
344
|
1 |
|
|
345
|
|
|
if ($number instanceof Model) { |
346
|
|
|
$number = $number->getId(); |
347
|
|
|
} |
348
|
|
|
|
349
|
|
|
$this->addColumnCondition("= ?", $number); |
350
|
|
|
|
351
|
|
|
return $this; |
352
|
|
|
} |
353
|
|
|
|
354
|
3 |
|
public function isLike($string) |
355
|
|
|
{ |
356
|
3 |
|
if (empty($string)) { |
357
|
3 |
|
return $this; |
358
|
|
|
} |
359
|
|
|
|
360
|
3 |
|
$this->addColumnCondition('LIKE CONCAT("%", ?, "%")', $string); |
361
|
|
|
|
362
|
3 |
|
return $this; |
363
|
|
|
} |
364
|
3 |
|
|
365
|
|
|
/** |
366
|
|
|
* Request that a column equals one of some strings |
367
|
|
|
* |
368
|
|
|
* @todo Improve for PDO |
369
|
|
|
* @param string[] $strings The list of accepted values for the column |
370
|
|
|
* @return static |
371
|
|
|
*/ |
372
|
|
|
public function isOneOf($strings) |
373
|
|
|
{ |
374
|
|
|
$count = count($strings); |
375
|
|
|
$questionMarks = str_repeat(',?', $count); |
376
|
|
|
|
377
|
|
|
// Remove first comma from questionMarks so that MySQL can read our query |
378
|
|
|
$questionMarks = ltrim($questionMarks, ','); |
379
|
|
|
|
380
|
|
|
$this->addColumnCondition("IN ($questionMarks)", $strings); |
381
|
|
|
|
382
|
|
|
return $this; |
383
|
|
|
} |
384
|
|
|
|
385
|
|
|
/** |
386
|
|
|
* Request that a column value starts with a string (case-insensitive) |
387
|
|
|
* |
388
|
|
|
* @param string $string The substring that the column's value should start with |
389
|
|
|
* @return static |
390
|
|
|
*/ |
391
|
|
|
public function startsWith($string) |
392
|
|
|
{ |
393
|
|
|
$this->addColumnCondition("LIKE CONCAT(?, '%')", $string); |
394
|
|
|
|
395
|
|
|
return $this; |
396
|
|
|
} |
397
|
|
|
|
398
|
|
|
/** |
399
|
|
|
* Request that a specific model is not returned |
400
|
|
|
* |
401
|
|
|
* @param Model|int $model The ID or model you don't want to get |
402
|
|
|
* @return static |
403
|
|
|
*/ |
404
|
3 |
View Code Duplication |
public function except($model) |
|
|
|
|
405
|
|
|
{ |
406
|
3 |
|
if ($model instanceof Model) { |
407
|
|
|
$model = $model->getId(); |
408
|
|
|
} |
409
|
|
|
|
410
|
3 |
|
$this->where('id'); |
411
|
|
|
$this->addColumnCondition("!= ?", $model); |
412
|
3 |
|
|
413
|
|
|
return $this; |
414
|
|
|
} |
415
|
|
|
|
416
|
|
|
/** |
417
|
|
|
* Return the results sorted by the value of a column |
418
|
|
|
* |
419
|
|
|
* @param string $column The column based on which the results should be ordered |
420
|
|
|
* @return static |
421
|
|
|
*/ |
422
|
2 |
|
public function sortBy($column) |
423
|
|
|
{ |
424
|
2 |
|
if (!isset($this->columns[$column])) { |
425
|
|
|
throw new Exception("Unknown column"); |
426
|
2 |
|
} |
427
|
|
|
|
428
|
|
|
$this->sortBy = $this->columns[$column]; |
429
|
|
|
|
430
|
|
|
return $this; |
431
|
|
|
} |
432
|
|
|
|
433
|
|
|
/** |
434
|
|
|
* Reverse the order |
435
|
2 |
|
* |
436
|
|
|
* Note: This only works if you have specified a column in the sortBy() method |
437
|
2 |
|
* |
438
|
|
|
* @return static |
439
|
2 |
|
*/ |
440
|
|
|
public function reverse() |
441
|
|
|
{ |
442
|
|
|
$this->reverseSort = !$this->reverseSort; |
443
|
|
|
|
444
|
|
|
return $this; |
445
|
|
|
} |
446
|
|
|
|
447
|
|
|
/** |
448
|
2 |
|
* Specify the number of results per page |
449
|
|
|
* |
450
|
2 |
|
* @param int $count The number of results |
451
|
|
|
* @return static |
452
|
2 |
|
*/ |
453
|
|
|
public function limit($count) |
454
|
|
|
{ |
455
|
|
|
$this->resultsPerPage = $count; |
456
|
|
|
|
457
|
|
|
return $this; |
458
|
|
|
} |
459
|
|
|
|
460
|
|
|
/** |
461
|
|
|
* Only show results from a specific page. This will |
462
|
|
|
* |
463
|
1 |
|
* @param int|null $page The page number (or null to show all pages - counting starts from 0) |
464
|
|
|
* @return static |
465
|
1 |
|
*/ |
466
|
|
|
public function fromPage($page) |
467
|
|
|
{ |
468
|
|
|
if ($page === null) { |
469
|
|
|
$this->page = $page; |
470
|
|
|
return $this; |
471
|
|
|
} |
472
|
|
|
|
473
|
|
|
$page = intval($page); |
474
|
|
|
$page = ($page <= 0) ? 1 : $page; |
475
|
|
|
|
476
|
1 |
|
$this->page = min($page, $this->countPages()); |
|
|
|
|
477
|
|
|
|
478
|
1 |
|
return $this; |
479
|
1 |
|
} |
480
|
|
|
|
481
|
|
|
/** |
482
|
|
|
* End with a specific result |
483
|
|
|
* |
484
|
|
|
* @param int|Model $model The model (or database ID) after the first result |
485
|
|
|
* @param bool $inclusive Whether to include the provided model |
486
|
|
|
* @param bool $reverse Whether to reverse the results |
487
|
|
|
* @return static |
488
|
|
|
*/ |
489
|
|
|
public function endAt($model, $inclusive = false, $reverse = false) |
490
|
|
|
{ |
491
|
|
|
return $this->startAt($model, $inclusive, !$reverse); |
492
|
|
|
} |
493
|
|
|
|
494
|
|
|
/** |
495
|
|
|
* Start with a specific result |
496
|
|
|
* |
497
|
|
|
* @param int|Model $model The model (or database ID) before the first result |
498
|
|
|
* @param bool $inclusive Whether to include the provided model |
499
|
|
|
* @param bool $reverse Whether to reverse the results |
500
|
|
|
* @return static |
501
|
|
|
*/ |
502
|
|
|
public function startAt($model, $inclusive = false, $reverse = false) |
503
|
|
|
{ |
504
|
|
|
if (!$model) { |
505
|
|
|
return $this; |
506
|
|
|
} elseif ($model instanceof Model && !$model->isValid()) { |
507
|
|
|
return $this; |
508
|
|
|
} |
509
|
3 |
|
|
510
|
|
|
$this->column($this->sortBy); |
511
|
3 |
|
$this->limited = true; |
512
|
|
|
$column = $this->currentColumn; |
513
|
|
|
$table = $this->getTable(); |
514
|
|
|
|
515
|
3 |
|
$comparison = $this->reverseSort ^ $reverse; |
516
|
|
|
$comparison = ($comparison) ? '>' : '<'; |
517
|
3 |
|
$comparison .= ($inclusive) ? '=' : ''; |
518
|
|
|
$id = ($model instanceof Model) ? $model->getId() : $model; |
519
|
|
|
|
520
|
|
|
// Compare an element's timestamp to the timestamp of $model; if it's the |
521
|
|
|
// same, perform the comparison using IDs |
522
|
|
|
$this->addColumnCondition( |
523
|
|
|
"$comparison (SELECT $column FROM $table WHERE id = ?) OR ($column = (SELECT $column FROM $table WHERE id = ?) AND id $comparison ?)", |
524
|
|
|
array($id, $id, $id) |
525
|
|
|
); |
526
|
|
|
|
527
|
|
|
return $this; |
528
|
|
|
} |
529
|
|
|
|
530
|
|
|
/** |
531
|
1 |
|
* Request that only "active" Models should be returned |
532
|
|
|
* |
533
|
1 |
|
* @return static |
534
|
|
|
*/ |
535
|
1 |
|
public function active() |
536
|
1 |
|
{ |
537
|
|
|
if (!isset($this->columns['status'])) { |
538
|
1 |
|
return $this; |
539
|
1 |
|
} |
540
|
1 |
|
|
541
|
|
|
$type = $this->type; |
542
|
|
|
|
543
|
|
|
return $this->where('status')->isOneOf($type::getActiveStatuses()); |
544
|
1 |
|
} |
545
|
|
|
|
546
|
|
|
/** |
547
|
|
|
* Make sure that Models invisible to a player are not returned |
548
|
|
|
* |
549
|
|
|
* Note that this method does not take PermissionModel::canBeSeenBy() into |
550
|
|
|
* consideration for performance purposes, so you will have to override this |
551
|
|
|
* in your query builder if necessary. |
552
|
|
|
* |
553
|
|
|
* @param Player $player The player in question |
554
|
|
|
* @param bool $showDeleted false to hide deleted models even from admins |
555
|
1 |
|
* @return static |
556
|
|
|
*/ |
557
|
1 |
|
public function visibleTo($player, $showDeleted = false) |
558
|
|
|
{ |
559
|
|
|
$type = $this->type; |
560
|
|
|
|
561
|
1 |
|
if (is_subclass_of($type, "PermissionModel") |
562
|
|
|
&& $player->hasPermission($type::EDIT_PERMISSION)) { |
563
|
1 |
|
// The player is an admin who can see hidden models |
564
|
|
|
if (!$showDeleted) { |
565
|
|
|
if (isset($this->columns['status'])) { |
566
|
|
|
return $this->where('status')->notEquals('deleted'); |
567
|
|
|
} |
568
|
|
|
} |
569
|
|
|
} else { |
570
|
|
|
return $this->active(); |
571
|
|
|
} |
572
|
|
|
|
573
|
1 |
|
return $this; |
574
|
|
|
} |
575
|
1 |
|
|
576
|
1 |
|
/** |
577
|
|
|
* Perform the query and get back the results in an array of names |
578
|
|
|
* |
579
|
1 |
|
* @return string[] An array of the type $id => $name |
580
|
|
|
*/ |
581
|
1 |
|
public function getNames() |
582
|
|
|
{ |
583
|
|
|
if (!$this->nameColumn) { |
|
|
|
|
584
|
|
|
throw new Exception("You haven't specified a name column"); |
585
|
|
|
} |
586
|
|
|
|
587
|
|
|
$results = $this->getArray($this->nameColumn); |
588
|
|
|
|
589
|
|
|
return array_column($results, $this->nameColumn, 'id'); |
590
|
|
|
} |
591
|
|
|
|
592
|
|
|
/** |
593
|
|
|
* Perform the query and get back the results in a list of arrays |
594
|
|
|
* |
595
|
|
|
* @todo Play with partial models? |
596
|
|
|
* @param string|string[] $columns The column(s) that should be returned |
597
|
|
|
* @return array[] |
598
|
|
|
*/ |
599
|
|
|
public function getArray($columns) |
600
|
|
|
{ |
601
|
|
|
if (!is_array($columns)) { |
602
|
|
|
$columns = array($columns); |
603
|
|
|
} |
604
|
|
|
|
605
|
|
|
$db = Database::getInstance(); |
606
|
|
|
|
607
|
27 |
|
return $db->query($this->createQuery($columns), $this->getParameters()); |
608
|
|
|
} |
609
|
27 |
|
|
610
|
27 |
|
/** |
611
|
|
|
* An alias for QueryBuilder::getModels(), with fast fetching on by default |
612
|
27 |
|
* and no return of results |
613
|
|
|
* |
614
|
27 |
|
* @param bool $fastFetch Whether to perform one query to load all |
615
|
|
|
* the model data instead of fetching them |
616
|
|
|
* one by one |
617
|
|
|
* @return void |
618
|
|
|
*/ |
619
|
27 |
|
public function addToCache($fastFetch = true) |
620
|
27 |
|
{ |
621
|
|
|
$this->getModels($fastFetch); |
622
|
27 |
|
} |
623
|
25 |
|
|
624
|
|
|
/** |
625
|
3 |
|
* Perform the query and get the results as Models |
626
|
|
|
* |
627
|
|
|
* @todo Fix fast fetch for queries with multiple tables |
628
|
|
|
* @param bool $fastFetch Whether to perform one query to load all |
629
|
|
|
* the model data instead of fetching them |
630
|
|
|
* one by one (ignores cache) |
631
|
|
|
* @return array |
632
|
|
|
*/ |
633
|
|
|
public function getModels($fastFetch = false) |
634
|
1 |
|
{ |
635
|
|
|
$db = Database::getInstance(); |
636
|
1 |
|
$type = $this->type; |
637
|
1 |
|
|
638
|
1 |
|
$columns = ($fastFetch) ? $type::getEagerColumns($this->getFromAlias()) : array(); |
639
|
1 |
|
|
640
|
|
|
if (is_string($columns) && !empty($this->extraColumns)) { |
641
|
|
|
$columns .= ',' . $this->extraColumns; |
642
|
|
|
} |
643
|
1 |
|
|
644
|
|
|
// Storing the value in a variable allows for quicker debugging |
645
|
1 |
|
$query = $this->createQuery($columns); |
646
|
|
|
$results = $db->query($query, $this->getParameters()); |
647
|
|
|
|
648
|
|
|
if ($fastFetch) { |
649
|
|
|
return $type::createFromDatabaseResults($results); |
650
|
|
|
} else { |
651
|
1 |
|
return $type::arrayIdToModel(array_column($results, 'id')); |
652
|
|
|
} |
653
|
1 |
|
} |
654
|
|
|
|
655
|
|
|
/** |
656
|
|
|
* Count the results |
657
|
|
|
* |
658
|
|
|
* @return int |
659
|
|
|
*/ |
660
|
|
|
public function count() |
661
|
1 |
|
{ |
662
|
|
|
$table = $this->getTable(); |
663
|
|
|
$params = $this->createQueryParams(false); |
664
|
1 |
|
$db = Database::getInstance(); |
665
|
|
|
$query = "SELECT COUNT(*) FROM $table $params"; |
666
|
1 |
|
|
667
|
|
|
// We don't want pagination to affect our results so don't use the functions that combine |
668
|
1 |
|
// pagination results |
669
|
|
|
$results = $db->query($query, $this->parameters); |
670
|
|
|
|
671
|
|
|
return $results[0]['COUNT(*)']; |
672
|
|
|
} |
673
|
|
|
|
674
|
|
|
/** |
675
|
1 |
|
* Count the number of pages that all the models could be separated into |
676
|
|
|
*/ |
677
|
1 |
|
public function countPages() |
678
|
|
|
{ |
679
|
|
|
return ceil($this->count() / $this->getResultsPerPage()); |
680
|
|
|
} |
681
|
|
|
|
682
|
|
|
/** |
683
|
|
|
* Find if there is any result |
684
|
|
|
* |
685
|
|
|
* @return bool |
686
|
|
|
*/ |
687
|
|
|
public function any() |
688
|
|
|
{ |
689
|
|
|
// Make sure that we don't mess with the user's options |
690
|
|
|
$query = clone $this; |
691
|
5 |
|
|
692
|
|
|
$query->limit(1); |
693
|
5 |
|
|
694
|
|
|
return $query->count() > 0; |
695
|
5 |
|
} |
696
|
|
|
|
697
|
|
|
/** |
698
|
5 |
|
* Get the amount of results that are returned per page |
699
|
|
|
* @return int |
700
|
1 |
|
*/ |
701
|
|
|
public function getResultsPerPage() |
702
|
|
|
{ |
703
|
5 |
|
return $this->resultsPerPage; |
704
|
|
|
} |
705
|
5 |
|
|
706
|
|
|
/** |
707
|
|
|
* Select a column to perform opeations on |
708
|
|
|
* |
709
|
|
|
* This is identical to the `where()` method, except that the column is |
710
|
|
|
* specified as a MySQL column and not as a column name given by the model |
711
|
|
|
* |
712
|
|
|
* @param string $column The column to select |
713
|
|
|
* @param string $mode Whether this column is static or is a column from an aggregate function; Either 'having' or 'where' |
714
|
5 |
|
* |
715
|
|
|
* @return static |
716
|
5 |
|
*/ |
717
|
|
|
protected function column($column, $mode = self::COL_WHERE) |
718
|
|
|
{ |
719
|
|
|
$this->currentColumnMode = $mode; |
720
|
5 |
|
|
721
|
3 |
|
if (strpos($column, '.') === false) { |
722
|
|
|
// Add the table name to the column if it isn't there already so that |
723
|
|
|
// MySQL knows what to do when handling multiple tables |
724
|
5 |
|
$this->currentColumn = ($this->currentColumnMode == self::COL_HAVING) ? "$column" : "`{$this->getFromAlias()}`.`$column`"; |
725
|
5 |
|
} else { |
726
|
|
|
$this->currentColumn = $column; |
727
|
5 |
|
} |
728
|
5 |
|
|
729
|
|
|
$this->currentColumnRaw = $column; |
730
|
|
|
|
731
|
5 |
|
return $this; |
732
|
5 |
|
} |
733
|
5 |
|
|
734
|
|
|
/** |
735
|
|
|
* Add a condition for the column |
736
|
|
|
* @param string $condition The MySQL condition |
737
|
|
|
* @param mixed $value Value(s) to pass to MySQL |
738
|
|
|
* @return void |
739
|
|
|
*/ |
740
|
|
|
protected function addColumnCondition($condition, $value) |
741
|
27 |
|
{ |
742
|
|
|
if (!$this->currentColumn) { |
|
|
|
|
743
|
27 |
|
throw new Exception("You haven't selected a column!"); |
744
|
27 |
|
} |
745
|
27 |
|
|
746
|
27 |
|
if (!is_array($value) && $value !== null) { |
747
|
27 |
|
$value = array($value); |
748
|
27 |
|
} |
749
|
|
|
|
750
|
27 |
|
$array = $this->currentColumnMode . 'Conditions'; |
751
|
27 |
|
$this->{$array}[] = "{$this->currentColumn} $condition"; |
752
|
|
|
|
753
|
|
|
if ($value !== null) { |
754
|
27 |
|
$this->parameters = array_merge($this->parameters, $value); |
755
|
|
|
} |
756
|
|
|
|
757
|
|
|
$this->currentColumn = null; |
758
|
|
|
$this->currentColumnRaw = null; |
759
|
|
|
} |
760
|
|
|
|
761
|
|
|
/** |
762
|
27 |
|
* Get the MySQL extra parameters |
763
|
|
|
* |
764
|
27 |
|
* @param bool $respectPagination Whether to respect pagination or not; useful for when pagination should be ignored such as count |
765
|
|
|
* @return string |
766
|
|
|
*/ |
767
|
|
|
protected function createQueryParams($respectPagination = true) |
768
|
|
|
{ |
769
|
|
|
$extras = $this->extras; |
770
|
|
|
$conditions = $this->createQueryConditions('where'); |
771
|
|
|
$groupQuery = $this->groupQuery; |
772
|
27 |
|
$havingClause = $this->createQueryConditions('having'); |
773
|
|
|
$order = $this->createQueryOrder(); |
774
|
27 |
|
$pagination = ""; |
775
|
|
|
|
776
|
|
|
if ($respectPagination) { |
777
|
|
|
$pagination = $this->createQueryPagination(); |
778
|
|
|
} |
779
|
|
|
|
780
|
|
|
return "$extras $conditions $groupQuery $havingClause $order $pagination"; |
781
|
|
|
} |
782
|
27 |
|
|
783
|
|
|
/** |
784
|
27 |
|
* Get the query parameters |
785
|
|
|
* |
786
|
27 |
|
* @return array |
787
|
|
|
*/ |
788
|
|
|
protected function getParameters() |
789
|
|
|
{ |
790
|
|
|
return array_merge($this->parameters, $this->paginationParameters); |
791
|
|
|
} |
792
|
|
|
|
793
|
|
|
/** |
794
|
|
|
* Get the alias used for the table in the FROM clause |
795
|
27 |
|
* |
796
|
|
|
* @return null|string |
797
|
27 |
|
*/ |
798
|
27 |
|
protected function getFromAlias() |
799
|
27 |
|
{ |
800
|
|
|
return $this->getTable(); |
801
|
27 |
|
} |
802
|
3 |
|
|
803
|
|
|
/** |
804
|
|
|
* Get the table of the model |
805
|
|
|
* |
806
|
|
|
* @return string |
807
|
27 |
|
*/ |
808
|
|
|
protected function getTable() |
809
|
|
|
{ |
810
|
|
|
$type = $this->type; |
811
|
|
|
|
812
|
|
|
return $type::TABLE; |
813
|
|
|
} |
814
|
|
|
|
815
|
3 |
|
/** |
816
|
|
|
* Get a MySQL query string in the requested format |
817
|
3 |
|
* @param string|string[] $columns The columns that should be included |
818
|
3 |
|
* (without the ID, if an array is provided) |
819
|
3 |
|
* @return string The query |
820
|
|
|
*/ |
821
|
3 |
|
protected function createQuery($columns = array()) |
822
|
1 |
|
{ |
823
|
1 |
|
$type = $this->type; |
824
|
1 |
|
$table = $type::TABLE; |
825
|
|
|
$params = $this->createQueryParams(); |
826
|
|
|
|
827
|
1 |
|
if (is_array($columns)) { |
828
|
|
|
$columns = $this->createQueryColumns($columns); |
829
|
|
|
} elseif (empty($columns)) { |
830
|
|
|
$columns = $this->createQueryColumns(); |
831
|
3 |
|
} |
832
|
|
|
|
833
|
|
|
return "SELECT $columns FROM $table $params"; |
834
|
|
|
} |
835
|
|
|
|
836
|
|
|
/** |
837
|
|
|
* Generate the columns for the query |
838
|
27 |
|
* @param string[] $columns The columns that should be included (without the ID) |
839
|
|
|
* @return string |
840
|
27 |
|
*/ |
841
|
|
|
private function createQueryColumns($columns = array()) |
842
|
27 |
|
{ |
843
|
|
|
$type = $this->type; |
844
|
|
|
$table = $type::TABLE; |
845
|
|
|
$columnStrings = array("`$table`.id"); |
846
|
|
|
|
847
|
5 |
|
foreach ($columns as $returnName) { |
848
|
|
|
if (strpos($returnName, ' ') === false) { |
849
|
|
|
$dbName = $this->columns[$returnName]; |
850
|
27 |
|
$columnStrings[] = "`$table`.`$dbName` as `$returnName`"; |
851
|
|
|
} else { |
852
|
|
|
// "Column" contains a space, pass it as is |
853
|
|
|
$columnStrings[] = $returnName; |
854
|
|
|
} |
855
|
|
|
} |
856
|
|
|
|
857
|
27 |
|
return implode(',', $columnStrings); |
858
|
|
|
} |
859
|
27 |
|
|
860
|
3 |
|
/** |
861
|
|
|
* Generates all the WHERE conditions for the query |
862
|
|
|
* @return string |
863
|
3 |
|
*/ |
864
|
3 |
|
private function createQueryConditions($mode) |
865
|
2 |
|
{ |
866
|
|
|
$array = $mode . 'Conditions'; |
867
|
3 |
|
|
868
|
|
|
if ($this->{$array}) { |
869
|
|
|
// Add parentheses around the conditions to prevent conflicts due |
870
|
25 |
|
// to the order of operations |
871
|
|
|
$conditions = array_map(function ($value) { return "($value)"; }, $this->{$array}); |
872
|
|
|
|
873
|
27 |
|
return strtoupper($mode) . ' ' . implode(' AND ', $conditions); |
874
|
|
|
} |
875
|
|
|
|
876
|
|
|
return ''; |
877
|
|
|
} |
878
|
|
|
|
879
|
|
|
/** |
880
|
27 |
|
* Generates the sorting instructions for the query |
881
|
|
|
* @return string |
882
|
|
|
*/ |
883
|
|
|
private function createQueryOrder() |
884
|
27 |
|
{ |
885
|
|
|
if ($this->sortBy) { |
|
|
|
|
886
|
27 |
|
$order = 'ORDER BY ' . $this->sortBy; |
887
|
26 |
|
|
888
|
|
|
// Sort by ID if the sorting columns are equal |
889
|
|
|
$id = "`{$this->getFromAlias()}`.`id`"; |
890
|
2 |
|
if ($this->reverseSort) { |
891
|
2 |
|
$order .= " DESC, $id DESC"; |
892
|
2 |
|
} else { |
893
|
2 |
|
$order .= ", $id"; |
894
|
|
|
} |
895
|
2 |
|
} else { |
896
|
|
|
$order = ''; |
897
|
|
|
} |
898
|
2 |
|
|
899
|
|
|
return $order; |
900
|
2 |
|
} |
901
|
|
|
|
902
|
|
|
/** |
903
|
|
|
* Generates the pagination instructions for the query |
904
|
|
|
* @return string |
905
|
|
|
*/ |
906
|
|
|
private function createQueryPagination() |
907
|
|
|
{ |
908
|
|
|
// Reset mysqli params just in case createQueryParagination() |
909
|
|
|
// had been called earlier |
910
|
|
|
$this->paginationParameters = array(); |
911
|
5 |
|
|
912
|
|
|
if ($this->page === null && !$this->limited) { |
913
|
5 |
|
return ''; |
914
|
|
|
} |
915
|
|
|
|
916
|
|
|
$offset = ''; |
917
|
5 |
|
if ($this->page) { |
|
|
|
|
918
|
|
|
$firstElement = ($this->page - 1) * $this->resultsPerPage; |
919
|
5 |
|
$this->paginationParameters[] = $firstElement; |
920
|
|
|
|
921
|
|
|
$offset = '?,'; |
922
|
|
|
} |
923
|
|
|
|
924
|
|
|
$this->paginationParameters[] = $this->resultsPerPage; |
925
|
|
|
|
926
|
|
|
return "LIMIT $offset ?"; |
927
|
|
|
} |
928
|
|
|
|
929
|
|
|
/** |
930
|
|
|
* Set the current column we're working on |
931
|
|
|
* |
932
|
|
|
* @param string $column The column we're selecting |
933
|
|
|
* @param string $mode Either 'where' or 'having' |
934
|
|
|
* |
935
|
|
|
* @return $this |
936
|
|
|
*/ |
937
|
|
|
private function grabColumn($column, $mode) |
938
|
|
|
{ |
939
|
|
|
if (!isset($this->columns[$column])) { |
940
|
|
|
throw new InvalidArgumentException("Unknown column '$column'"); |
941
|
|
|
} |
942
|
|
|
|
943
|
|
|
$this->column($this->columns[$column], $mode); |
944
|
|
|
|
945
|
|
|
return $this; |
946
|
|
|
} |
947
|
|
|
} |
948
|
|
|
|
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.