Completed
Pull Request — master (#160)
by Vladimir
05:55 queued 02:59
created

QueryBuilder::isBefore()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 4
ccs 0
cts 2
cp 0
rs 10
cc 1
eloc 2
nc 1
nop 3
crap 2
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
     * Select a column
175
     *
176
     * `$queryBuilder->where('username')->equals('administrator');`
177
     *
178
     * @param  string $column The column to select
179
     * @return static
180
     */
181 5
    public function where($column)
182
    {
183 5
        return $this->grabColumn($column, 'where');
184
    }
185
186
    /**
187
     * Select an alias from an aggregate function
188
     *
189
     * `$queryBuilder->where('activity')->greaterThan(0);`
190
     *
191
     * @param  string $column The column to select
192
     * @return static
193
     */
194
    public function having($column)
195
    {
196
        return $this->grabColumn($column, 'having');
197
    }
198
199
    /**
200
     * Request that a column equals a string (case-insensitive)
201
     *
202
     * @param  string $string The string that the column's value should equal to
203
     * @return static
204
     */
205 3
    public function equals($string)
206
    {
207 3
        $this->addColumnCondition("= ?", $string);
208
209 3
        return $this;
210
    }
211
212
    /**
213
     * Request that a column doesNOT equals a string (case-insensitive)
214
     *
215
     * @param  string $string The string that the column's value should equal to
216
     * @return static
217
     */
218 2
    public function notEquals($string)
219
    {
220 2
        $this->addColumnCondition("!= ?", $string);
221
222 2
        return $this;
223
    }
224
225
    /**
226
     * Request that a column is not null
227
     *
228
     * @return static
229
     */
230 1
    public function isNotNull()
231
    {
232 1
        $this->addColumnCondition('IS NOT NULL', null);
233
234 1
        return $this;
235
    }
236
237
    /**
238
     * Request that a column is null
239
     *
240
     * @return static
241
     */
242
    public function isNull()
243
    {
244
        $this->addColumnCondition('IS NULL', null);
245
246
        return $this;
247
    }
248
249
    /**
250
     * Request that a column is greater than a quantity
251
     *
252
     * @param  string $quantity The quantity to test against
253
     * @return static
254
     */
255
    public function greaterThan($quantity)
256
    {
257
        $this->addColumnCondition("> ?", $quantity);
258
259
        return $this;
260
    }
261
262
    /**
263
     * Request that a column is less than a quantity
264
     *
265
     * @param  string $quantity The quantity to test against
266
     * @return static
267
     */
268
    public function lessThan($quantity)
269
    {
270
        $this->addColumnCondition("< ?", $quantity);
271
272
        return $this;
273
    }
274
275
    /**
276
     * Request that a timestamp is before the specified time
277
     *
278
     * @param string|TimeDate $time      The timestamp to compare to
279
     * @param bool            $inclusive Whether to include the given timestamp
280
     * @param bool            $reverse   Whether to reverse the results
281
     *
282
     * @return static
283
     */
284
    public function isBefore($time, $inclusive = false, $reverse = false)
285
    {
286
        return $this->isAfter($time, $inclusive, !$reverse);
287
    }
288
289
    /**
290
     * Request that a timestamp is after the specified time
291
     *
292
     * @param string|TimeDate $time      The timestamp to compare to
293
     * @param bool            $inclusive Whether to include the given timestamp
294
     * @param bool            $reverse   Whether to reverse the results
295
     *
296
     * @return static
297
     */
298 2
    public function isAfter($time, $inclusive = false, $reverse = false)
299
    {
300 2
        if ($time instanceof TimeDate) {
301 2
            $time = $time->toMysql();
302
        }
303
304 2
        $comparison  = ($reverse)   ? '<' : '>';
305 2
        $comparison .= ($inclusive) ? '=' : '';
306
307 2
        $this->addColumnCondition("$comparison ?",  $time);
308
309 2
        return $this;
310
    }
311
312
    /**
313
     * Request that a column equals a number
314
     *
315
     * @param  int|Model|null $number The number that the column's value should
316
     *                                equal to. If a Model is provided, use the
317
     *                                model's ID, while null values are ignored.
318
     * @return static
319
     */
320 View Code Duplication
    public function is($number)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
321
    {
322
        if ($number === null) {
323
            return $this;
324
        }
325
326
        if ($number instanceof Model) {
327
            $number = $number->getId();
328
        }
329
330
        $this->addColumnCondition("= ?", $number);
331
332 1
        return $this;
333
    }
334 1
335
    /**
336
     * Request that a column equals one of some strings
337
     *
338 1
     * @todo   Improve for PDO
339 1
     * @param  string[] $strings The list of accepted values for the column
340
     * @return static
341
     */
342 1
    public function isOneOf($strings)
343
    {
344 1
        $count = count($strings);
345
        $questionMarks = str_repeat(',?', $count);
346
347
        // Remove first comma from questionMarks so that MySQL can read our query
348
        $questionMarks = ltrim($questionMarks, ',');
349
350
        $this->addColumnCondition("IN ($questionMarks)", $strings);
351
352
        return $this;
353
    }
354 3
355
    /**
356 3
     * Request that a column value starts with a string (case-insensitive)
357 3
     *
358
     * @param  string $string The substring that the column's value should start with
359
     * @return static
360 3
     */
361
    public function startsWith($string)
362 3
    {
363
        $this->addColumnCondition("LIKE CONCAT(?, '%')", $string);
364 3
365
        return $this;
366
    }
367
368
    /**
369
     * Request that a specific model is not returned
370
     *
371
     * @param  Model|int $model The ID or model you don't want to get
372
     * @return static
373
     */
374 View Code Duplication
    public function except($model)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
375
    {
376
        if ($model instanceof Model) {
377
            $model = $model->getId();
378
        }
379
380
        $this->where('id');
381
        $this->addColumnCondition("!= ?", $model);
382
383
        return $this;
384
    }
385
386
    /**
387
     * Return the results sorted by the value of a column
388
     *
389
     * @param  string $column The column based on which the results should be ordered
390
     * @return static
391
     */
392
    public function sortBy($column)
393
    {
394
        if (!isset($this->columns[$column])) {
395
            throw new Exception("Unknown column");
396
        }
397
398
        $this->sortBy = $this->columns[$column];
399
400
        return $this;
401
    }
402
403
    /**
404 3
     * Reverse the order
405
     *
406 3
     * Note: This only works if you have specified a column in the sortBy() method
407
     *
408
     * @return static
409
     */
410 3
    public function reverse()
411
    {
412 3
        $this->reverseSort = !$this->reverseSort;
413
414
        return $this;
415
    }
416
417
    /**
418
     * Specify the number of results per page
419
     *
420
     * @param  int  $count The number of results
421
     * @return static
422 2
     */
423
    public function limit($count)
424 2
    {
425
        $this->resultsPerPage = $count;
426 2
427
        return $this;
428
    }
429
430
    /**
431
     * Only show results from a specific page. This will
432
     *
433
     * @param  int|null $page The page number (or null to show all pages - counting starts from 0)
434
     * @return static
435 2
     */
436
    public function fromPage($page)
437 2
    {
438
        if ($page === null) {
439 2
            $this->page = $page;
440
            return $this;
441
        }
442
443
        $page = intval($page);
444
        $page = ($page <= 0) ? 1 : $page;
445
446
        $this->page = min($page, $this->countPages());
0 ignored issues
show
Documentation Bug introduced by
It seems like min($page, $this->countPages()) can also be of type double. However, the property $page is declared as type integer|null. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
447
448 2
        return $this;
449
    }
450 2
451
    /**
452 2
     * End with a specific result
453
     *
454
     * @param  int|Model $model     The model (or database ID) after the first result
455
     * @param  bool   $inclusive Whether to include the provided model
456
     * @param  bool   $reverse   Whether to reverse the results
457
     * @return static
458
     */
459
    public function endAt($model, $inclusive = false, $reverse = false)
460
    {
461
        return $this->startAt($model, $inclusive, !$reverse);
462
    }
463 1
464
    /**
465 1
     * Start with a specific result
466
     *
467
     * @param  int|Model $model     The model (or database ID) before the first result
468
     * @param  bool   $inclusive Whether to include the provided model
469
     * @param  bool   $reverse   Whether to reverse the results
470
     * @return static
471
     */
472
    public function startAt($model, $inclusive = false, $reverse = false)
473
    {
474
        if (!$model) {
475
            return $this;
476 1
        } elseif ($model instanceof Model && !$model->isValid()) {
477
            return $this;
478 1
        }
479 1
480
        $this->column($this->sortBy);
481
        $this->limited = true;
482
        $column = $this->currentColumn;
483
        $table  = $this->getTable();
484
485
        $comparison  = $this->reverseSort ^ $reverse;
486
        $comparison  = ($comparison) ? '>' : '<';
487
        $comparison .= ($inclusive)  ? '=' : '';
488
        $id = ($model instanceof Model) ? $model->getId() : $model;
489
490
        // Compare an element's timestamp to the timestamp of $model; if it's the
491
        // same, perform the comparison using IDs
492
        $this->addColumnCondition(
493
            "$comparison (SELECT $column FROM $table WHERE id = ?) OR ($column = (SELECT $column FROM $table WHERE id = ?) AND id $comparison ?)",
494
            array($id, $id, $id)
495
        );
496
497
        return $this;
498
    }
499
500
    /**
501
     * Request that only "active" Models should be returned
502
     *
503
     * @return static
504
     */
505
    public function active()
506
    {
507
        if (!isset($this->columns['status'])) {
508
            return $this;
509 3
        }
510
511 3
        $type = $this->type;
512
513
        return $this->where('status')->isOneOf($type::getActiveStatuses());
514
    }
515 3
516
    /**
517 3
     * Make sure that Models invisible to a player are not returned
518
     *
519
     * Note that this method does not take PermissionModel::canBeSeenBy() into
520
     * consideration for performance purposes, so you will have to override this
521
     * in your query builder if necessary.
522
     *
523
     * @param  Player  $player      The player in question
524
     * @param  bool $showDeleted false to hide deleted models even from admins
525
     * @return static
526
     */
527
    public function visibleTo($player, $showDeleted = false)
528
    {
529
        $type = $this->type;
530
531 1
        if (is_subclass_of($type, "PermissionModel")
532
         && $player->hasPermission($type::EDIT_PERMISSION)) {
533 1
            // The player is an admin who can see hidden models
534
            if (!$showDeleted) {
535 1
                if (isset($this->columns['status'])) {
536 1
                    return $this->where('status')->notEquals('deleted');
537
                }
538 1
            }
539 1
        } else {
540 1
            return $this->active();
541
        }
542
543
        return $this;
544 1
    }
545
546
    /**
547
     * Perform the query and get back the results in an array of names
548
     *
549
     * @return string[] An array of the type $id => $name
550
     */
551
    public function getNames()
552
    {
553
        if (!$this->nameColumn) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->nameColumn of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
554
            throw new Exception("You haven't specified a name column");
555 1
        }
556
557 1
        $results = $this->getArray($this->nameColumn);
558
559
        return array_column($results, $this->nameColumn, 'id');
560
    }
561 1
562
    /**
563 1
     * Perform the query and get back the results in a list of arrays
564
     *
565
     * @todo   Play with partial models?
566
     * @param  string|string[] $columns The column(s) that should be returned
567
     * @return array[]
568
     */
569
    public function getArray($columns)
570
    {
571
        if (!is_array($columns)) {
572
            $columns = array($columns);
573 1
        }
574
575 1
        $db = Database::getInstance();
576 1
577
        return $db->query($this->createQuery($columns), $this->getParameters());
578
    }
579 1
580
    /**
581 1
     * An alias for QueryBuilder::getModels(), with fast fetching on by default
582
     * and no return of results
583
     *
584
     * @param  bool $fastFetch Whether to perform one query to load all
585
     *                            the model data instead of fetching them
586
     *                            one by one
587
     * @return void
588
     */
589
    public function addToCache($fastFetch = true)
590
    {
591
        $this->getModels($fastFetch);
592
    }
593
594
    /**
595
     * Perform the query and get the results as Models
596
     *
597
     * @todo Fix fast fetch for queries with multiple tables
598
     * @param  bool $fastFetch Whether to perform one query to load all
599
     *                            the model data instead of fetching them
600
     *                            one by one (ignores cache)
601
     * @return array
602
     */
603
    public function getModels($fastFetch = false)
604
    {
605
        $db   = Database::getInstance();
606
        $type = $this->type;
607 27
608
        $columns = ($fastFetch) ? $type::getEagerColumns($this->getFromAlias()) : array();
609 27
610 27
        if (is_string($columns) && !empty($this->extraColumns)) {
611
            $columns .= ',' . $this->extraColumns;
612 27
        }
613
614 27
        // Storing the value in a variable allows for quicker debugging
615
        $query = $this->createQuery($columns);
616
        $results = $db->query($query, $this->getParameters());
617
618
        if ($fastFetch) {
619 27
            return $type::createFromDatabaseResults($results);
620 27
        } else {
621
            return $type::arrayIdToModel(array_column($results, 'id'));
622 27
        }
623 25
    }
624
625 3
    /**
626
     * Count the results
627
     *
628
     * @return int
629
     */
630
    public function count()
631
    {
632
        $table  = $this->getTable();
633
        $params = $this->createQueryParams(false);
634 1
        $db     = Database::getInstance();
635
        $query  = "SELECT COUNT(*) FROM $table $params";
636 1
637 1
        // We don't want pagination to affect our results so don't use the functions that combine
638 1
        // pagination results
639 1
        $results = $db->query($query, $this->parameters);
640
641
        return $results[0]['COUNT(*)'];
642
    }
643 1
644
    /**
645 1
     * Count the number of pages that all the models could be separated into
646
     */
647
    public function countPages()
648
    {
649
        return ceil($this->count() / $this->getResultsPerPage());
650
    }
651 1
652
    /**
653 1
     * Find if there is any result
654
     *
655
     * @return bool
656
     */
657
    public function any()
658
    {
659
        // Make sure that we don't mess with the user's options
660
        $query = clone $this;
661 1
662
        $query->limit(1);
663
664 1
        return $query->count() > 0;
665
    }
666 1
667
    /**
668 1
     * Get the amount of results that are returned per page
669
     * @return int
670
     */
671
    public function getResultsPerPage()
672
    {
673
        return $this->resultsPerPage;
674
    }
675 1
676
    /**
677 1
     * Select a column to perform opeations on
678
     *
679
     * This is identical to the `where()` method, except that the column is
680
     * specified as a MySQL column and not as a column name given by the model
681
     *
682
     * @param  string $column The column to select
683
     * @param  string $mode   Whether this column is static or is a column from an aggregate function; Either 'having' or 'where'
684
     *
685
     * @return static
686
     */
687
    protected function column($column, $mode = self::COL_WHERE)
688
    {
689
        $this->currentColumnMode = $mode;
690
691 5
        if (strpos($column, '.') === false) {
692
            // Add the table name to the column if it isn't there already so that
693 5
            // MySQL knows what to do when handling multiple tables
694
            $this->currentColumn = ($this->currentColumnMode == self::COL_HAVING) ? "$column" : "`{$this->getFromAlias()}`.`$column`";
695 5
        } else {
696
            $this->currentColumn = $column;
697
        }
698 5
699
        $this->currentColumnRaw = $column;
700 1
701
        return $this;
702
    }
703 5
704
    /**
705 5
     * Add a condition for the column
706
     * @param  string $condition The MySQL condition
707
     * @param  mixed  $value     Value(s) to pass to MySQL
708
     * @return void
709
     */
710
    protected function addColumnCondition($condition, $value)
711
    {
712
        if (!$this->currentColumn) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->currentColumn of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
713
            throw new Exception("You haven't selected a column!");
714 5
        }
715
716 5
        if (!is_array($value) && $value !== null) {
717
            $value = array($value);
718
        }
719
720 5
        $array = $this->currentColumnMode . 'Conditions';
721 3
        $this->{$array}[] = "{$this->currentColumn} $condition";
722
723
        if ($value !== null) {
724 5
            $this->parameters = array_merge($this->parameters, $value);
725 5
        }
726
727 5
        $this->currentColumn = null;
728 5
        $this->currentColumnRaw = null;
729
    }
730
731 5
    /**
732 5
     * Get the MySQL extra parameters
733 5
     *
734
     * @param  bool $respectPagination Whether to respect pagination or not; useful for when pagination should be ignored such as count
735
     * @return string
736
     */
737
    protected function createQueryParams($respectPagination = true)
738
    {
739
        $extras     = $this->extras;
740
        $conditions = $this->createQueryConditions('where');
741 27
        $groupQuery = $this->groupQuery;
742
        $havingClause = $this->createQueryConditions('having');
743 27
        $order      = $this->createQueryOrder();
744 27
        $pagination = "";
745 27
746 27
        if ($respectPagination) {
747 27
            $pagination = $this->createQueryPagination();
748 27
        }
749
750 27
        return "$extras $conditions $groupQuery $havingClause $order $pagination";
751 27
    }
752
753
    /**
754 27
     * Get the query parameters
755
     *
756
     * @return array
757
     */
758
    protected function getParameters()
759
    {
760
        return array_merge($this->parameters, $this->paginationParameters);
761
    }
762 27
763
    /**
764 27
     * Get the alias used for the table in the FROM clause
765
     *
766
     * @return null|string
767
     */
768
    protected function getFromAlias()
769
    {
770
        return $this->getTable();
771
    }
772 27
773
    /**
774 27
     * Get the table of the model
775
     *
776
     * @return string
777
     */
778
    protected function getTable()
779
    {
780
        $type = $this->type;
781
782 27
        return $type::TABLE;
783
    }
784 27
785
    /**
786 27
     * Get a MySQL query string in the requested format
787
     * @param  string|string[] $columns The columns that should be included
788
     *                                  (without the ID, if an array is provided)
789
     * @return string The query
790
     */
791
    protected function createQuery($columns = array())
792
    {
793
        $type     = $this->type;
794
        $table    = $type::TABLE;
795 27
        $params   = $this->createQueryParams();
796
797 27
        if (is_array($columns)) {
798 27
            $columns = $this->createQueryColumns($columns);
799 27
        } elseif (empty($columns)) {
800
            $columns = $this->createQueryColumns();
801 27
        }
802 3
803
        return "SELECT $columns FROM $table $params";
804
    }
805
806
    /**
807 27
     * Generate the columns for the query
808
     * @param  string[] $columns The columns that should be included (without the ID)
809
     * @return string
810
     */
811
    private function createQueryColumns($columns = array())
812
    {
813
        $type = $this->type;
814
        $table = $type::TABLE;
815 3
        $columnStrings = array("`$table`.id");
816
817 3
        foreach ($columns as $returnName) {
818 3
            if (strpos($returnName, ' ') === false) {
819 3
                $dbName = $this->columns[$returnName];
820
                $columnStrings[] = "`$table`.`$dbName` as `$returnName`";
821 3
            } else {
822 1
                // "Column" contains a space, pass it as is
823 1
                $columnStrings[] = $returnName;
824 1
            }
825
        }
826
827 1
        return implode(',', $columnStrings);
828
    }
829
830
    /**
831 3
     * Generates all the WHERE conditions for the query
832
     * @return string
833
     */
834
    private function createQueryConditions($mode)
835
    {
836
        $array = $mode . 'Conditions';
837
838 27
        if ($this->{$array}) {
839
            // Add parentheses around the conditions to prevent conflicts due
840 27
            // to the order of operations
841
            $conditions = array_map(function ($value) { return "($value)"; }, $this->{$array});
842 27
843
            return strtoupper($mode) . ' ' . implode(' AND ', $conditions);
844
        }
845
846
        return '';
847 5
    }
848
849
    /**
850 27
     * Generates the sorting instructions for the query
851
     * @return string
852
     */
853
    private function createQueryOrder()
854
    {
855
        if ($this->sortBy) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->sortBy of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
856
            $order = 'ORDER BY ' . $this->sortBy;
857 27
858
            // Sort by ID if the sorting columns are equal
859 27
            $id = "`{$this->getFromAlias()}`.`id`";
860 3
            if ($this->reverseSort) {
861
                $order .= " DESC, $id DESC";
862
            } else {
863 3
                $order .= ", $id";
864 3
            }
865 2
        } else {
866
            $order = '';
867 3
        }
868
869
        return $order;
870 25
    }
871
872
    /**
873 27
     * Generates the pagination instructions for the query
874
     * @return string
875
     */
876
    private function createQueryPagination()
877
    {
878
        // Reset mysqli params just in case createQueryParagination()
879
        // had been called earlier
880 27
        $this->paginationParameters = array();
881
882
        if ($this->page === null && !$this->limited) {
883
            return '';
884 27
        }
885
886 27
        $offset = '';
887 26
        if ($this->page) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->page of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
888
            $firstElement = ($this->page - 1) * $this->resultsPerPage;
889
            $this->paginationParameters[] = $firstElement;
890 2
891 2
            $offset = '?,';
892 2
        }
893 2
894
        $this->paginationParameters[] = $this->resultsPerPage;
895 2
896
        return "LIMIT $offset ?";
897
    }
898 2
899
    /**
900 2
     * Set the current column we're working on
901
     *
902
     * @param string $column The column we're selecting
903
     * @param string $mode   Either 'where' or 'having'
904
     *
905
     * @return $this
906
     */
907
    private function grabColumn($column, $mode)
908
    {
909
        if (!isset($this->columns[$column])) {
910
            throw new InvalidArgumentException("Unknown column '$column'");
911 5
        }
912
913 5
        $this->column($this->columns[$column], $mode);
914
915
        return $this;
916
    }
917
}
918