Completed
Push — master ( bce3ff...b78e58 )
by Konstantinos
04:07
created

QueryBuilder::createQuery()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3.0987
Metric Value
dl 0
loc 14
ccs 7
cts 9
cp 0.7778
rs 9.4285
cc 3
eloc 9
nc 3
nop 1
crap 3.0987
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
    /**
29
     * The type of the model we're building a query for
30
     * @var string
31
     */
32
    protected $type;
33
34
    /**
35
     * The columns that the model provided us
36
     * @var array
37
     */
38
    protected $columns = array('id' => 'id');
39
40
    /**
41
     * The conditions to include in WHERE
42
     * @var string[]
43
     */
44
    protected $conditions = array();
45
46
    /**
47
     * The MySQL value parameters
48
     * @var array
49
     */
50
    protected $parameters = array();
51
52
    /**
53
     * The MySQL parameter types
54
     * @var string
55
     */
56
    protected $types = '';
57
58
    /**
59
     * The MySQL value parameters for pagination
60
     * @var array
61
     */
62
    protected $paginationParameters = array();
63
64
    /**
65
     * The MySQL parameter types for pagination
66
     * @var string
67
     */
68
    protected $paginationTypes = '';
69
70
    /**
71
     * Extra MySQL query string to pass
72
     * @var string
73
     */
74
    protected $extras = '';
75
76
    /**
77
     * A column based on which we should sort the results
78
     * @var string|null
79
     */
80
    private $sortBy = null;
81
82
    /**
83
     * Whether to reverse the results
84
     * @var bool
85
     */
86
    private $reverseSort = false;
87
88
    /**
89
     * The currently selected column
90
     * @var string|null
91
     */
92
    private $currentColumn = null;
93
94
    /**
95
     * The currently selected column without the table name (unless it was
96
     * explicitly provided)
97
     * @var string|null
98
     */
99
    protected $currentColumnRaw = null;
100
101
    /**
102
     * A column to consider the name of the model
103
     * @var string|null
104
     */
105
    private $nameColumn = null;
106
107
    /**
108
     * Whether to return the results as arrays instead of models
109
     * @var bool
110
     */
111
    private $returnArray = false;
0 ignored issues
show
Unused Code introduced by
The property $returnArray is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
112
113
    /**
114
     * The page to return
115
     * @var int|null
116
     */
117
    private $page = null;
118
119
    /**
120
     * Whether the ID of the first/last element has been provided
121
     * @var bool
122
     */
123
    private $limited = false;
124
125
    /**
126
     * The number of elements on every page
127
     * @var int
128
     */
129
    protected $resultsPerPage = 30;
130
131
    /**
132
     * Create a new QueryBuilder
133
     *
134
     * A new query builder should be created on a static getQueryBuilder()
135
     * method on each model. The options array can contain the following
136
     * properties:
137
     *
138
     * - `columns`: An associative array - the key of each entry is the column
139
     *   name that will be used by other methods, while the value is
140
     *   is the column name that is used in the database structure
141
     *
142
     * - `activeStatuses`: If the model has a status column, this should be
143
     *                     a list of values that make the entry be considered
144
     *                     "active"
145
     *
146
     * - `name`: The name of the column which represents the name of the object
147
     *
148
     * @param string $type    The type of the Model (e.g. "Player" or "Match")
149
     * @param array  $options The options to pass to the builder (see above)
150
     */
151 4
    public function __construct($type, $options = array())
152
    {
153 4
        $this->type = $type;
154
155 4
        if (isset($options['columns'])) {
156 4
            $this->columns += $options['columns'];
157
        }
158
159 4
        if (isset($options['name'])) {
160 2
            $this->nameColumn = $options['name'];
161
        }
162 4
    }
163
164
    /**
165
     * Select a column
166
     *
167
     * `$queryBuilder->where('username')->equals('administrator');`
168
     *
169
     * @param  string $column The column to select
170
     * @return self
171
     */
172 4
    public function where($column)
173
    {
174 4
        if (!isset($this->columns[$column])) {
175
            throw new InvalidArgumentException("Unknown column '$column'");
176
        }
177
178 4
        $this->column($this->columns[$column]);
179
180 4
        return $this;
181
    }
182
183
    /**
184
     * Request that a column equals a string (case-insensitive)
185
     *
186
     * @param  string $string The string that the column's value should equal to
187
     * @return self
188
     */
189 1
    public function equals($string)
190
    {
191 1
        $this->addColumnCondition("= ?", $string, 's');
192
193 1
        return $this;
194
    }
195
196
    /**
197
     * Request that a column doesNOT equals a string (case-insensitive)
198
     *
199
     * @param  string $string The string that the column's value should equal to
200
     * @return self
201
     */
202 1
    public function notEquals($string)
203
    {
204 1
        $this->addColumnCondition("!= ?", $string, 's');
205
206 1
        return $this;
207
    }
208
209
    /**
210
     * Request that a column is greater than a quantity
211
     *
212
     * @param  string $quantity The quantity to test against
213
     * @return self
214
     */
215
    public function greaterThan($quantity)
216
    {
217
        $this->addColumnCondition("> ?", $quantity, 's');
218
219
        return $this;
220
    }
221
222
    /**
223
     * Request that a column is less than a quantity
224
     *
225
     * @param  string $quantity The quantity to test against
226
     * @return self
227
     */
228
    public function lessThan($quantity)
229
    {
230
        $this->addColumnCondition("< ?", $quantity, 's');
231
232
        return $this;
233
    }
234
235
    /**
236
     * Request that a timestamp is before the specified time
237
     *
238
     * @param string|TimeDate $time      The timestamp to compare to
239
     * @param bool            $inclusive Whether to include the given timestamp
240
     * @param bool            $reverse   Whether to reverse the results
241
     */
242
    public function isBefore($time, $inclusive = false, $reverse = false)
243
    {
244
        return $this->isAfter($time, $inclusive, !$reverse);
245
    }
246
247
    /**
248
     * Request that a timestamp is after the specified time
249
     *
250
     * @param string|TimeDate $time      The timestamp to compare to
251
     * @param bool            $inclusive Whether to include the given timestamp
252
     * @param bool            $reverse   Whether to reverse the results
253
     */
254 1
    public function isAfter($time, $inclusive = false, $reverse = false)
255
    {
256 1
        if ($time instanceof TimeDate) {
257 1
            $time = $time->toMysql();
258
        }
259
260 1
        $comparison  = ($reverse)   ? '<' : '>';
261 1
        $comparison .= ($inclusive) ? '=' : '';
262
263 1
        $this->addColumnCondition("$comparison ?",  $time, 's');
264
265 1
        return $this;
266
    }
267
268
    /**
269
     * Request that a column equals a number
270
     *
271
     * @param  int|Model|null $number The number that the column's value should
272
     *                                equal to. If a Model is provided, use the
273
     *                                model's ID, while null values are ignored.
274
     * @return self
275
     */
276 1 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...
277
    {
278 1
        if ($number === null) {
279
            return $this;
280
        }
281
282 1
        if ($number instanceof Model) {
283 1
            $number = $number->getId();
284
        }
285
286 1
        $this->addColumnCondition("= ?", $number, 'i');
287
288 1
        return $this;
289
    }
290
291
    /**
292
     * Request that a column equals one of some strings
293
     *
294
     * @param  string[] $strings The list of accepted values for the column
295
     * @return self
296
     */
297 3
    public function isOneOf($strings)
298
    {
299 3
        $count = count($strings);
300 3
        $types = str_repeat('s', $count);
301 3
        $questionMarks = str_repeat(',?', $count);
302
303
        // Remove first comma from questionMarks so that MySQL can read our query
304 3
        $questionMarks = ltrim($questionMarks, ',');
305
306 3
        $this->addColumnCondition("IN ($questionMarks)", $strings, $types);
307
308 3
        return $this;
309
    }
310
311
    /**
312
     * Request that a column value starts with a string (case-insensitive)
313
     *
314
     * @param  string $string The substring that the column's value should start with
315
     * @return self
316
     */
317
    public function startsWith($string)
318
    {
319
        $this->addColumnCondition("LIKE CONCAT(?, '%')", $string, 's');
320
321
        return $this;
322
    }
323
324
    /**
325
     * Request that a specific model is not returned
326
     *
327
     * @param  Model|int $model The ID or model you don't want to get
328
     * @return self
329
     */
330 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...
331
    {
332
        if ($model instanceof Model) {
333
            $model = $model->getId();
334
        }
335
336
        $this->where('id');
337
        $this->addColumnCondition("!= ?", $model, 'i');
338
339
        return $this;
340
    }
341
342
    /**
343
     * Return the results sorted by the value of a column
344
     *
345
     * @param  string $column The column based on which the results should be ordered
346
     * @return self
347
     */
348 2
    public function sortBy($column)
349
    {
350 2
        if (!isset($this->columns[$column])) {
351
            throw new Exception("Unknown column");
352
        }
353
354 2
        $this->sortBy = $this->columns[$column];
355
356 2
        return $this;
357
    }
358
359
    /**
360
     * Reverse the order
361
     *
362
     * Note: This only works if you have specified a column in the sortBy() method
363
     *
364
     * @return self
365
     */
366 2
    public function reverse()
367
    {
368 2
        $this->reverseSort = !$this->reverseSort;
369
370 2
        return $this;
371
    }
372
373
    /**
374
     * Specify the number of results per page
375
     *
376
     * @param  int  $count The number of results
377
     * @return self
378
     */
379 2
    public function limit($count)
380
    {
381 2
        $this->resultsPerPage = $count;
382
383 2
        return $this;
384
    }
385
386
    /**
387
     * Only show results from a specific page
388
     *
389
     * @param  int|null $page The page number (or null to show all pages - counting starts from 0)
390
     * @return self
391
     */
392 2
    public function fromPage($page)
393
    {
394 2
        $this->page = $page;
395
396 2
        return $this;
397
    }
398
399
    /**
400
     * End with a specific result
401
     *
402
     * @param  int|Model $model     The model (or database ID) after the first result
403
     * @param  bool   $inclusive Whether to include the provided model
404
     * @param  bool   $reverse   Whether to reverse the results
405
     * @return self
406
     */
407 1
    public function endAt($model, $inclusive = false, $reverse = false)
408
    {
409 1
        return $this->startAt($model, $inclusive, !$reverse);
410
    }
411
412
    /**
413
     * Start with a specific result
414
     *
415
     * @param  int|Model $model     The model (or database ID) before the first result
416
     * @param  bool   $inclusive Whether to include the provided model
417
     * @param  bool   $reverse   Whether to reverse the results
418
     * @return self
419
     */
420 1
    public function startAt($model, $inclusive = false, $reverse = false)
421
    {
422 1
        if (!$model) {
423 1
            return $this;
424
        } elseif ($model instanceof Model && !$model->isValid()) {
425
            return $this;
426
        }
427
428
        $this->column($this->sortBy);
429
        $this->limited = true;
430
        $column = $this->currentColumn;
431
        $table  = $this->getTable();
432
433
        $comparison  = $this->reverseSort ^ $reverse;
434
        $comparison  = ($comparison) ? '>' : '<';
435
        $comparison .= ($inclusive)  ? '=' : '';
436
        $id = ($model instanceof Model) ? $model->getId() : $model;
437
438
        // Compare an element's timestamp to the timestamp of $model; if it's the
439
        // same, perform the comparison using IDs
440
        $this->addColumnCondition(
441
            "$comparison (SELECT $column FROM $table WHERE id = ?) OR ($column = (SELECT $column FROM $table WHERE id = ?) AND id $comparison ?)",
442
            array($id, $id, $id),
443
            'iii'
444
        );
445
446
        return $this;
447
    }
448
449
    /**
450
     * Request that only "active" Models should be returned
451
     *
452
     * @return self
453
     */
454 3
    public function active()
455
    {
456 3
        if (!isset($this->columns['status'])) {
457
            return $this;
458
        }
459
460 3
        $type = $this->type;
461
462 3
        return $this->where('status')->isOneOf($type::getActiveStatuses());
463
    }
464
465
    /**
466
     * Make sure that Models invisible to a player are not returned
467
     *
468
     * Note that this method does not take PermissionModel::canBeSeenBy() into
469
     * consideration for performance purposes, so you will have to override this
470
     * in your query builder if necessary.
471
     *
472
     * @param  Player  $player      The player in question
473
     * @param  bool $showDeleted false to hide deleted models even from admins
474
     * @return self
475
     */
476 1
    public function visibleTo($player, $showDeleted = false)
477
    {
478 1
        $type = $this->type;
479
480 1
        if (is_subclass_of($type, "PermissionModel")
481 1
         && $player->hasPermission($type::EDIT_PERMISSION)) {
482
            // The player is an admin who can see hidden models
483 1
            if ($showDeleted) {
484
                return $this;
485
            } else {
486 1
                return $this->where('status')->notEquals('deleted');
487
            }
488
        } else {
489 1
            return $this->active();
490
        }
491
    }
492
493
    /**
494
     * Perform the query and get back the results in an array of names
495
     *
496
     * @return string[] An array of the type $id => $name
497
     */
498 1
    public function getNames()
499
    {
500 1
        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...
501
            throw new Exception("You haven't specified a name column");
502
        }
503
504 1
        $results = $this->getArray($this->nameColumn);
505
506 1
        return array_column($results, $this->nameColumn, 'id');
507
    }
508
509
    /**
510
     * Perform the query and get back the results in a list of arrays
511
     *
512
     * @todo  Play with partial models?
513
     * @param string|string[] The column(s) that should be returned
514
     * @param  string  $columns
515
     * @return array[]
516
     */
517 1
    public function getArray($columns)
518
    {
519 1
        if (!is_array($columns)) {
520 1
            $columns = array($columns);
521
        }
522
523 1
        $db = Database::getInstance();
524
525 1
        return $db->query($this->createQuery($columns), $this->getTypes(), $this->getParameters());
526
    }
527
528
    /**
529
     * An alias for QueryBuilder::getModels(), with fast fetching on by default
530
     * and no return of results
531
     *
532
     * @param  boolean $fastFetch Whether to perform one query to load all
533
     *                            the model data instead of fetching them
534
     *                            one by one
535
     * @return void
536
     */
537
    public function addToCache($fastFetch = true)
538
    {
539
        $this->getModels($fastFetch);
540
    }
541
542
    /**
543
     * Perform the query and get the results as Models
544
     *
545
     * @todo Fix fast fetch for queries with multiple tables
546
     * @param  boolean $fastFetch Whether to perform one query to load all
547
     *                            the model data instead of fetching them
548
     *                            one by one (ignores cache)
549
     * @return array
550
     */
551 4
    public function getModels($fastFetch = false)
552
    {
553 4
        $db   = Database::getInstance();
554 4
        $type = $this->type;
555
556 4
        $columns = ($fastFetch) ? $type::getEagerColumns() : array();
557
558 4
        $results = $db->query($this->createQuery($columns), $this->getTypes(), $this->getParameters());
559
560 4
        if ($fastFetch) {
561 1
            return $type::createFromDatabaseResults($results);
562
        } else {
563 4
            return $type::arrayIdToModel(array_column($results, 'id'));
564
        }
565
    }
566
567
    /**
568
     * Count the results
569
     *
570
     * @return int
571
     */
572 1
    public function count()
573
    {
574 1
        $table  = $this->getTable();
575 1
        $params = $this->createQueryParams(false);
576 1
        $db     = Database::getInstance();
577 1
        $query  = "SELECT COUNT(*) FROM $table $params";
578
579
        // We don't want pagination to affect our results so don't use the functions that combine
580
        // pagination results
581 1
        $results = $db->query($query, $this->types, $this->parameters);
582
583 1
        return $results[0]['COUNT(*)'];
584
    }
585
586
    /**
587
     * Count the number of pages that all the models could be separated into
588
     */
589 1
    public function countPages()
590
    {
591 1
        return ceil($this->count() / $this->getResultsPerPage());
592
    }
593
594
    /**
595
     * Find if there is any result
596
     *
597
     * @return bool
598
     */
599 1
    public function any()
600
    {
601
        // Make sure that we don't mess with the user's options
602 1
        $query = clone $this;
603
604 1
        $query->limit(1);
605
606 1
        return $query->count() > 0;
607
    }
608
609
    /**
610
     * Get the amount of results that are returned per page
611
     * @return int
612
     */
613 1
    public function getResultsPerPage()
614
    {
615 1
        return $this->resultsPerPage;
616
    }
617
618
    /**
619
     * Select a column to perform opeations on
620
     *
621
     * This is identical to the `where()` method, except that the column is
622
     * specified as a MySQL column and not as a column name given by the model
623
     *
624
     * @param  string $column The column to select
625
     * @return self
626
     */
627 4
    protected function column($column)
628
    {
629 4
        if (strpos($column, '.') === false) {
630
            // Add the table name to the column if it isn't there already so that
631
            // MySQL knows what to do when handling multiple tables
632 4
            $table = $this->getTable();
633 4
            $this->currentColumn = "`$table`.`$column`";
634
        } else {
635 1
            $this->currentColumn = $column;
636
        }
637
638 4
        $this->currentColumnRaw = $column;
639
640 4
        return $this;
641
    }
642
643
    /**
644
     * Add a condition for the column
645
     * @param  string $condition The MySQL condition
646
     * @param  mixed  $value     Value(s) to pass to MySQL
647
     * @param  string $type      The type of the values
648
     * @return void
649
     */
650 4
    protected function addColumnCondition($condition, $value, $type)
651
    {
652 4
        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...
653
            throw new Exception("You haven't selected a column!");
654
        }
655
656 4
        if (!is_array($value)) {
657 2
            $value = array($value);
658
        }
659
660 4
        $this->conditions[] = "{$this->currentColumn} $condition";
661 4
        $this->parameters   = array_merge($this->parameters, $value);
662 4
        $this->types       .= $type;
663
664 4
        $this->currentColumn = null;
665 4
        $this->currentColumnRaw = null;
666 4
    }
667
668
    /**
669
     * Get the MySQL extra parameters
670
     *
671
     * @param  bool $respectPagination Whether to respect pagination or not; useful for when pagination should be ignored such as count
672
     * @return string
673
     */
674 4
    protected function createQueryParams($respectPagination = true)
675
    {
676 4
        $extras     = $this->extras;
677 4
        $conditions = $this->createQueryConditions();
678 4
        $order      = $this->createQueryOrder();
679 4
        $pagination = "";
680
681 4
        if ($respectPagination) {
682 4
            $pagination = $this->createQueryPagination();
683
        }
684
685 4
        return "$extras $conditions $order $pagination";
686
    }
687
688
    /**
689
     * Get the query parameters
690
     *
691
     * @return array
692
     */
693 4
    protected function getParameters()
694
    {
695 4
        return array_merge($this->parameters, $this->paginationParameters);
696
    }
697
698
    /**
699
     * Get the query types
700
     *
701
     * @return string
702
     */
703 4
    protected function getTypes()
704
    {
705 4
        return $this->types . $this->paginationTypes;
706
    }
707
708
    /**
709
     * Get the table of the model
710
     *
711
     * @return string
712
     */
713 4
    protected function getTable()
714
    {
715 4
        $type = $this->type;
716
717 4
        return $type::TABLE;
718
    }
719
720
    /**
721
     * Get a MySQL query string in the requested format
722
     * @param  string|string[] $columns The columns that should be included
723
     *                                  (without the ID, if an array is provided)
724
     * @return string The query
725
     */
726 4
    protected function createQuery($columns = array())
727
    {
728 4
        $type     = $this->type;
729 4
        $table    = $type::TABLE;
730 4
        $params   = $this->createQueryParams();
731
732 4
        if (is_array($columns)) {
733 4
            $columns = $this->createQueryColumns($columns);
734
        } elseif (empty($columns)) {
735
            $columns = $this->createQueryColumns();
736
        }
737
738 4
        return "SELECT $columns FROM $table $params";
739
    }
740
741
    /**
742
     * Generate the columns for the query
743
     * @param  string[] $columns The columns that should be included (without the ID)
744
     * @return string
745
     */
746 4
    private function createQueryColumns($columns = array())
747
    {
748 4
        $type = $this->type;
749 4
        $table = $type::TABLE;
750 4
        $columnStrings = array("`$table`.id");
751
752 4
        foreach ($columns as $returnName) {
753 1
            if (strpos($returnName, ' ') === false) {
754 1
                $dbName = $this->columns[$returnName];
755 1
                $columnStrings[] = "`$table`.`$dbName` as `$returnName`";
756
            } else {
757
                // "Column" contains a space, pass it as is
758 1
                $columnStrings[] = $returnName;
759
            }
760
        }
761
762 4
        return implode(',', $columnStrings);
763
    }
764
765
    /**
766
     * Generates all the WHERE conditions for the query
767
     * @return string
768
     */
769 4
    private function createQueryConditions()
770
    {
771 4
        if ($this->conditions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->conditions of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
772
            // Add parentheses around the conditions to prevent conflicts due
773
            // to the order of operations
774
            $conditions = array_map(function ($value) { return "($value)"; }, $this->conditions);
775
776 4
            return 'WHERE ' . implode(' AND ', $conditions);
777
        }
778
779
        return '';
780
    }
781
782
    /**
783
     * Generates the sorting instructions for the query
784
     * @return string
785
     */
786 4
    private function createQueryOrder()
787
    {
788 4
        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...
789 2
            $order = 'ORDER BY ' . $this->sortBy;
790
791
            // Sort by ID if the sorting columns are equal
792 2
            $id = '`' . $this->getTable() . '`.`id`';
793 2
            if ($this->reverseSort) {
794 2
                $order .= " DESC, $id DESC";
795
            } else {
796 2
                $order .= ", $id";
797
            }
798
        } else {
799 3
            $order = '';
800
        }
801
802 4
        return $order;
803
    }
804
805
    /**
806
     * Generates the pagination instructions for the query
807
     * @return string
808
     */
809 4
    private function createQueryPagination()
810
    {
811
        // Reset mysqli params and types just in case createQueryParagination()
812
        // had been called earlier
813 4
        $this->paginationParameters = array();
814 4
        $this->paginationTypes = "";
815
816 4
        if (!$this->page && !$this->limited) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->page of type integer|null is loosely compared to false; 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...
817 3
            return '';
818
        }
819
820 2
        $offset = '';
821 2
        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...
822 2
            $firstElement = ($this->page - 1) * $this->resultsPerPage;
823 2
            $this->paginationParameters[] = $firstElement;
824 2
            $this->paginationTypes       .= 'i';
825
826 2
            $offset = '?,';
827
        }
828
829 2
        $this->paginationParameters[] = $this->resultsPerPage;
830 2
        $this->paginationTypes       .= 'i';
831
832 2
        return "LIMIT $offset ?";
833
    }
834
}
835