Completed
Push — feature/player-elo ( f5ce1b...b1d122 )
by Vladimir
02:50
created

QueryBuilder::addColumnCondition()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 20
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 11
nc 5
nop 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
    /**
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
     * Extra columns that are generated from the SQL query (this should be a comma separated string or null)
42
     * @var string|null
43
     */
44
    protected $extraColumns = null;
45
46
    /**
47
     * The conditions to include in WHERE
48
     * @var string[]
49
     */
50
    protected $whereConditions = array();
51
52
    /**
53
     * The conditions to include in HAVING
54
     * @var string[]
55
     */
56
    protected $havingConditions = array();
57
58
    /**
59
     * The MySQL value parameters
60
     * @var array
61
     */
62
    protected $parameters = array();
63
64
    /**
65
     * The MySQL value parameters for pagination
66
     * @var array
67
     */
68
    protected $paginationParameters = array();
69
70
    /**
71
     * Extra MySQL query string to pass
72
     * @var string
73
     */
74
    protected $extras = '';
75
76
    /**
77
     * Extra MySQL query groupby string to pass
78
     * @var string
79
     */
80
    protected $groupQuery = '';
81
82
    /**
83
     * A column based on which we should sort the results
84
     * @var string|null
85
     */
86
    private $sortBy = null;
87
88
    /**
89
     * Whether to reverse the results
90
     * @var bool
91
     */
92
    private $reverseSort = false;
93
94
    /**
95
     * The currently selected column
96
     * @var string|null
97
     */
98
    private $currentColumn = null;
99
100
    /**
101
     * Either 'where' or 'having'
102
     * @var string
103
     */
104
    private $currentColumnMode = '';
105
106
    /**
107
     * The currently selected column without the table name (unless it was
108
     * explicitly provided)
109
     * @var string|null
110
     */
111
    protected $currentColumnRaw = null;
112
113
    /**
114
     * A column to consider the name of the model
115
     * @var string|null
116
     */
117
    private $nameColumn = null;
118
119
    /**
120
     * The page to return
121
     * @var int|null
122
     */
123
    private $page = null;
124
125
    /**
126
     * Whether the ID of the first/last element has been provided
127
     * @var bool
128
     */
129
    private $limited = false;
130
131
    /**
132
     * The number of elements on every page
133
     * @var int
134
     */
135
    protected $resultsPerPage = 30;
136
137
    /**
138
     * Create a new QueryBuilder
139
     *
140
     * A new query builder should be created on a static getQueryBuilder()
141
     * method on each model. The options array can contain the following
142
     * properties:
143
     *
144
     * - `columns`: An associative array - the key of each entry is the column
145
     *   name that will be used by other methods, while the value is
146
     *   is the column name that is used in the database structure
147
     *
148
     * - `activeStatuses`: If the model has a status column, this should be
149
     *                     a list of values that make the entry be considered
150
     *                     "active"
151
     *
152
     * - `name`: The name of the column which represents the name of the object
153
     *
154
     * @param string $type    The type of the Model (e.g. "Player" or "Match")
155
     * @param array  $options The options to pass to the builder (see above)
156
     */
157
    public function __construct($type, $options = array())
158
    {
159
        $this->type = $type;
160
161
        if (isset($options['columns'])) {
162
            $this->columns += $options['columns'];
163
        }
164
165
        if (isset($options['name'])) {
166
            $this->nameColumn = $options['name'];
167
        }
168
    }
169
170
    /**
171
     * Select a column
172
     *
173
     * `$queryBuilder->where('username')->equals('administrator');`
174
     *
175
     * @param  string $column The column to select
176
     * @return static
177
     */
178
    public function where($column)
179
    {
180
        return $this->grabColumn($column, 'where');
181
    }
182
183
    /**
184
     * Select an alias from an aggregate function
185
     *
186
     * `$queryBuilder->where('activity')->greaterThan(0);`
187
     *
188
     * @param  string $column The column to select
189
     * @return static
190
     */
191
    public function having($column)
192
    {
193
        return $this->grabColumn($column, 'having');
194
    }
195
196
    /**
197
     * Request that a column equals a string (case-insensitive)
198
     *
199
     * @param  string $string The string that the column's value should equal to
200
     * @return static
201
     */
202
    public function equals($string)
203
    {
204
        $this->addColumnCondition("= ?", $string);
205
206
        return $this;
207
    }
208
209
    /**
210
     * Request that a column doesNOT equals a string (case-insensitive)
211
     *
212
     * @param  string $string The string that the column's value should equal to
213
     * @return static
214
     */
215
    public function notEquals($string)
216
    {
217
        $this->addColumnCondition("!= ?", $string);
218
219
        return $this;
220
    }
221
222
    /**
223
     * Request that a column is not null
224
     *
225
     * @return static
226
     */
227
    public function isNotNull()
228
    {
229
        $this->addColumnCondition('IS NOT NULL', null);
230
231
        return $this;
232
    }
233
234
    /**
235
     * Request that a column is null
236
     *
237
     * @return static
238
     */
239
    public function isNull()
240
    {
241
        $this->addColumnCondition('IS NULL', null);
242
243
        return $this;
244
    }
245
246
    /**
247
     * Request that a column is greater than a quantity
248
     *
249
     * @param  string $quantity The quantity to test against
250
     * @return static
251
     */
252
    public function greaterThan($quantity)
253
    {
254
        $this->addColumnCondition("> ?", $quantity);
255
256
        return $this;
257
    }
258
259
    /**
260
     * Request that a column is less than a quantity
261
     *
262
     * @param  string $quantity The quantity to test against
263
     * @return static
264
     */
265
    public function lessThan($quantity)
266
    {
267
        $this->addColumnCondition("< ?", $quantity);
268
269
        return $this;
270
    }
271
272
    /**
273
     * Request that a timestamp is before the specified time
274
     *
275
     * @param string|TimeDate $time      The timestamp to compare to
276
     * @param bool            $inclusive Whether to include the given timestamp
277
     * @param bool            $reverse   Whether to reverse the results
278
     *
279
     * @return static
280
     */
281
    public function isBefore($time, $inclusive = false, $reverse = false)
282
    {
283
        return $this->isAfter($time, $inclusive, !$reverse);
284
    }
285
286
    /**
287
     * Request that a timestamp is after the specified time
288
     *
289
     * @param string|TimeDate $time      The timestamp to compare to
290
     * @param bool            $inclusive Whether to include the given timestamp
291
     * @param bool            $reverse   Whether to reverse the results
292
     *
293
     * @return static
294
     */
295
    public function isAfter($time, $inclusive = false, $reverse = false)
296
    {
297
        if ($time instanceof TimeDate) {
298
            $time = $time->toMysql();
299
        }
300
301
        $comparison  = ($reverse)   ? '<' : '>';
302
        $comparison .= ($inclusive) ? '=' : '';
303
304
        $this->addColumnCondition("$comparison ?",  $time);
305
306
        return $this;
307
    }
308
309
    /**
310
     * Request that a column equals a number
311
     *
312
     * @param  int|Model|null $number The number that the column's value should
313
     *                                equal to. If a Model is provided, use the
314
     *                                model's ID, while null values are ignored.
315
     * @return static
316
     */
317 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...
318
    {
319
        if ($number === null) {
320
            return $this;
321
        }
322
323
        if ($number instanceof Model) {
324
            $number = $number->getId();
325
        }
326
327
        $this->addColumnCondition("= ?", $number);
328
329
        return $this;
330
    }
331
332
    /**
333
     * Request that a column equals one of some strings
334
     *
335
     * @todo   Improve for PDO
336
     * @param  string[] $strings The list of accepted values for the column
337
     * @return static
338
     */
339
    public function isOneOf($strings)
340
    {
341
        $count = count($strings);
342
        $questionMarks = str_repeat(',?', $count);
343
344
        // Remove first comma from questionMarks so that MySQL can read our query
345
        $questionMarks = ltrim($questionMarks, ',');
346
347
        $this->addColumnCondition("IN ($questionMarks)", $strings);
348
349
        return $this;
350
    }
351
352
    /**
353
     * Request that a column value starts with a string (case-insensitive)
354
     *
355
     * @param  string $string The substring that the column's value should start with
356
     * @return static
357
     */
358
    public function startsWith($string)
359
    {
360
        $this->addColumnCondition("LIKE CONCAT(?, '%')", $string);
361
362
        return $this;
363
    }
364
365
    /**
366
     * Request that a specific model is not returned
367
     *
368
     * @param  Model|int $model The ID or model you don't want to get
369
     * @return static
370
     */
371 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...
372
    {
373
        if ($model instanceof Model) {
374
            $model = $model->getId();
375
        }
376
377
        $this->where('id');
378
        $this->addColumnCondition("!= ?", $model);
379
380
        return $this;
381
    }
382
383
    /**
384
     * Return the results sorted by the value of a column
385
     *
386
     * @param  string $column The column based on which the results should be ordered
387
     * @return static
388
     */
389
    public function sortBy($column)
390
    {
391
        if (!isset($this->columns[$column])) {
392
            throw new Exception("Unknown column");
393
        }
394
395
        $this->sortBy = $this->columns[$column];
396
397
        return $this;
398
    }
399
400
    /**
401
     * Reverse the order
402
     *
403
     * Note: This only works if you have specified a column in the sortBy() method
404
     *
405
     * @return static
406
     */
407
    public function reverse()
408
    {
409
        $this->reverseSort = !$this->reverseSort;
410
411
        return $this;
412
    }
413
414
    /**
415
     * Specify the number of results per page
416
     *
417
     * @param  int  $count The number of results
418
     * @return static
419
     */
420
    public function limit($count)
421
    {
422
        $this->resultsPerPage = $count;
423
424
        return $this;
425
    }
426
427
    /**
428
     * Only show results from a specific page
429
     *
430
     * @param  int|null $page The page number (or null to show all pages - counting starts from 0)
431
     * @return static
432
     */
433
    public function fromPage($page)
434
    {
435
        $this->page = $page;
436
437
        return $this;
438
    }
439
440
    /**
441
     * End with a specific result
442
     *
443
     * @param  int|Model $model     The model (or database ID) after the first result
444
     * @param  bool   $inclusive Whether to include the provided model
445
     * @param  bool   $reverse   Whether to reverse the results
446
     * @return static
447
     */
448
    public function endAt($model, $inclusive = false, $reverse = false)
449
    {
450
        return $this->startAt($model, $inclusive, !$reverse);
451
    }
452
453
    /**
454
     * Start with a specific result
455
     *
456
     * @param  int|Model $model     The model (or database ID) before the first result
457
     * @param  bool   $inclusive Whether to include the provided model
458
     * @param  bool   $reverse   Whether to reverse the results
459
     * @return static
460
     */
461
    public function startAt($model, $inclusive = false, $reverse = false)
462
    {
463
        if (!$model) {
464
            return $this;
465
        } elseif ($model instanceof Model && !$model->isValid()) {
466
            return $this;
467
        }
468
469
        $this->column($this->sortBy);
0 ignored issues
show
Bug introduced by
The call to column() misses a required argument $mode.

This check looks for function calls that miss required arguments.

Loading history...
470
        $this->limited = true;
471
        $column = $this->currentColumn;
472
        $table  = $this->getTable();
473
474
        $comparison  = $this->reverseSort ^ $reverse;
475
        $comparison  = ($comparison) ? '>' : '<';
476
        $comparison .= ($inclusive)  ? '=' : '';
477
        $id = ($model instanceof Model) ? $model->getId() : $model;
478
479
        // Compare an element's timestamp to the timestamp of $model; if it's the
480
        // same, perform the comparison using IDs
481
        $this->addColumnCondition(
482
            "$comparison (SELECT $column FROM $table WHERE id = ?) OR ($column = (SELECT $column FROM $table WHERE id = ?) AND id $comparison ?)",
483
            array($id, $id, $id)
484
        );
485
486
        return $this;
487
    }
488
489
    /**
490
     * Request that only "active" Models should be returned
491
     *
492
     * @return static
493
     */
494
    public function active()
495
    {
496
        if (!isset($this->columns['status'])) {
497
            return $this;
498
        }
499
500
        $type = $this->type;
501
502
        return $this->where('status')->isOneOf($type::getActiveStatuses());
503
    }
504
505
    /**
506
     * Make sure that Models invisible to a player are not returned
507
     *
508
     * Note that this method does not take PermissionModel::canBeSeenBy() into
509
     * consideration for performance purposes, so you will have to override this
510
     * in your query builder if necessary.
511
     *
512
     * @param  Player  $player      The player in question
513
     * @param  bool $showDeleted false to hide deleted models even from admins
514
     * @return static
515
     */
516
    public function visibleTo($player, $showDeleted = false)
517
    {
518
        $type = $this->type;
519
520
        if (is_subclass_of($type, "PermissionModel")
521
         && $player->hasPermission($type::EDIT_PERMISSION)) {
522
            // The player is an admin who can see hidden models
523
            if (!$showDeleted) {
524
                if (isset($this->columns['status'])) {
525
                    return $this->where('status')->notEquals('deleted');
526
                }
527
            }
528
        } else {
529
            return $this->active();
530
        }
531
532
        return $this;
533
    }
534
535
    /**
536
     * Perform the query and get back the results in an array of names
537
     *
538
     * @return string[] An array of the type $id => $name
539
     */
540
    public function getNames()
541
    {
542
        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...
543
            throw new Exception("You haven't specified a name column");
544
        }
545
546
        $results = $this->getArray($this->nameColumn);
547
548
        return array_column($results, $this->nameColumn, 'id');
549
    }
550
551
    /**
552
     * Perform the query and get back the results in a list of arrays
553
     *
554
     * @todo   Play with partial models?
555
     * @param  string|string[] $columns The column(s) that should be returned
556
     * @return array[]
557
     */
558
    public function getArray($columns)
559
    {
560
        if (!is_array($columns)) {
561
            $columns = array($columns);
562
        }
563
564
        $db = Database::getInstance();
565
566
        return $db->query($this->createQuery($columns), $this->getParameters());
567
    }
568
569
    /**
570
     * An alias for QueryBuilder::getModels(), with fast fetching on by default
571
     * and no return of results
572
     *
573
     * @param  bool $fastFetch Whether to perform one query to load all
574
     *                            the model data instead of fetching them
575
     *                            one by one
576
     * @return void
577
     */
578
    public function addToCache($fastFetch = true)
579
    {
580
        $this->getModels($fastFetch);
581
    }
582
583
    /**
584
     * Perform the query and get the results as Models
585
     *
586
     * @todo Fix fast fetch for queries with multiple tables
587
     * @param  bool $fastFetch Whether to perform one query to load all
588
     *                            the model data instead of fetching them
589
     *                            one by one (ignores cache)
590
     * @return array
591
     */
592
    public function getModels($fastFetch = false)
593
    {
594
        $db   = Database::getInstance();
595
        $type = $this->type;
596
597
        $columns = ($fastFetch) ? $type::getEagerColumns($this->getFromAlias()) : array();
598
599
        if (is_string($columns) && !empty($this->extraColumns)) {
600
            $columns .= ',' . $this->extraColumns;
601
        }
602
603
        // Storing the value in a variable allows for quicker debugging
604
        $query = $this->createQuery($columns);
605
        $results = $db->query($query, $this->getParameters());
606
607
        if ($fastFetch) {
608
            return $type::createFromDatabaseResults($results);
609
        } else {
610
            return $type::arrayIdToModel(array_column($results, 'id'));
611
        }
612
    }
613
614
    /**
615
     * Count the results
616
     *
617
     * @return int
618
     */
619
    public function count()
620
    {
621
        $table  = $this->getTable();
622
        $params = $this->createQueryParams(false);
623
        $db     = Database::getInstance();
624
        $query  = "SELECT COUNT(*) FROM $table $params";
625
626
        // We don't want pagination to affect our results so don't use the functions that combine
627
        // pagination results
628
        $results = $db->query($query, $this->parameters);
629
630
        return $results[0]['COUNT(*)'];
631
    }
632
633
    /**
634
     * Count the number of pages that all the models could be separated into
635
     */
636
    public function countPages()
637
    {
638
        return ceil($this->count() / $this->getResultsPerPage());
639
    }
640
641
    /**
642
     * Find if there is any result
643
     *
644
     * @return bool
645
     */
646
    public function any()
647
    {
648
        // Make sure that we don't mess with the user's options
649
        $query = clone $this;
650
651
        $query->limit(1);
652
653
        return $query->count() > 0;
654
    }
655
656
    /**
657
     * Get the amount of results that are returned per page
658
     * @return int
659
     */
660
    public function getResultsPerPage()
661
    {
662
        return $this->resultsPerPage;
663
    }
664
665
    /**
666
     * Select a column to perform opeations on
667
     *
668
     * This is identical to the `where()` method, except that the column is
669
     * specified as a MySQL column and not as a column name given by the model
670
     *
671
     * @param  string $column The column to select
672
     * @return static
673
     */
674
    protected function column($column, $mode)
675
    {
676
        $this->currentColumnMode = $mode;
677
678
        if (strpos($column, '.') === false) {
679
            // Add the table name to the column if it isn't there already so that
680
            // MySQL knows what to do when handling multiple tables
681
            $this->currentColumn = ($this->currentColumnMode == 'having') ? "$column" : "`{$this->getFromAlias()}`.`$column`";
682
        } else {
683
            $this->currentColumn = $column;
684
        }
685
686
        $this->currentColumnRaw = $column;
687
688
        return $this;
689
    }
690
691
    /**
692
     * Add a condition for the column
693
     * @param  string $condition The MySQL condition
694
     * @param  mixed  $value     Value(s) to pass to MySQL
695
     * @return void
696
     */
697
    protected function addColumnCondition($condition, $value)
698
    {
699
        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...
700
            throw new Exception("You haven't selected a column!");
701
        }
702
703
        if (!is_array($value) && $value !== null) {
704
            $value = array($value);
705
        }
706
707
        $array = $this->currentColumnMode . 'Conditions';
708
        $this->{$array}[] = "{$this->currentColumn} $condition";
709
710
        if ($value !== null) {
711
            $this->parameters = array_merge($this->parameters, $value);
712
        }
713
714
        $this->currentColumn = null;
715
        $this->currentColumnRaw = null;
716
    }
717
718
    /**
719
     * Get the MySQL extra parameters
720
     *
721
     * @param  bool $respectPagination Whether to respect pagination or not; useful for when pagination should be ignored such as count
722
     * @return string
723
     */
724
    protected function createQueryParams($respectPagination = true)
725
    {
726
        $extras     = $this->extras;
727
        $conditions = $this->createQueryConditions('where');
728
        $groupQuery = $this->groupQuery;
729
        $havingClause = $this->createQueryConditions('having');
730
        $order      = $this->createQueryOrder();
731
        $pagination = "";
732
733
        if ($respectPagination) {
734
            $pagination = $this->createQueryPagination();
735
        }
736
737
        return "$extras $conditions $groupQuery $havingClause $order $pagination";
738
    }
739
740
    /**
741
     * Get the query parameters
742
     *
743
     * @return array
744
     */
745
    protected function getParameters()
746
    {
747
        return array_merge($this->parameters, $this->paginationParameters);
748
    }
749
750
    /**
751
     * Get the alias used for the table in the FROM clause
752
     *
753
     * @return null|string
754
     */
755
    protected function getFromAlias()
756
    {
757
        return $this->getTable();
758
    }
759
760
    /**
761
     * Get the table of the model
762
     *
763
     * @return string
764
     */
765
    protected function getTable()
766
    {
767
        $type = $this->type;
768
769
        return $type::TABLE;
770
    }
771
772
    /**
773
     * Get a MySQL query string in the requested format
774
     * @param  string|string[] $columns The columns that should be included
775
     *                                  (without the ID, if an array is provided)
776
     * @return string The query
777
     */
778
    protected function createQuery($columns = array())
779
    {
780
        $type     = $this->type;
781
        $table    = $type::TABLE;
782
        $params   = $this->createQueryParams();
783
784
        if (is_array($columns)) {
785
            $columns = $this->createQueryColumns($columns);
786
        } elseif (empty($columns)) {
787
            $columns = $this->createQueryColumns();
788
        }
789
790
        return "SELECT $columns FROM $table $params";
791
    }
792
793
    /**
794
     * Generate the columns for the query
795
     * @param  string[] $columns The columns that should be included (without the ID)
796
     * @return string
797
     */
798
    private function createQueryColumns($columns = array())
799
    {
800
        $type = $this->type;
801
        $table = $type::TABLE;
802
        $columnStrings = array("`$table`.id");
803
804
        foreach ($columns as $returnName) {
805
            if (strpos($returnName, ' ') === false) {
806
                $dbName = $this->columns[$returnName];
807
                $columnStrings[] = "`$table`.`$dbName` as `$returnName`";
808
            } else {
809
                // "Column" contains a space, pass it as is
810
                $columnStrings[] = $returnName;
811
            }
812
        }
813
814
        return implode(',', $columnStrings);
815
    }
816
817
    /**
818
     * Generates all the WHERE conditions for the query
819
     * @return string
820
     */
821
    private function createQueryConditions($mode)
822
    {
823
        $array = $mode . 'Conditions';
824
825
        if ($this->{$array}) {
826
            // Add parentheses around the conditions to prevent conflicts due
827
            // to the order of operations
828
            $conditions = array_map(function ($value) { return "($value)"; }, $this->{$array});
829
830
            return strtoupper($mode) . ' ' . implode(' AND ', $conditions);
831
        }
832
833
        return '';
834
    }
835
836
    /**
837
     * Generates the sorting instructions for the query
838
     * @return string
839
     */
840
    private function createQueryOrder()
841
    {
842
        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...
843
            $order = 'ORDER BY ' . $this->sortBy;
844
845
            // Sort by ID if the sorting columns are equal
846
            $id = "`{$this->getFromAlias()}`.`id`";
847
            if ($this->reverseSort) {
848
                $order .= " DESC, $id DESC";
849
            } else {
850
                $order .= ", $id";
851
            }
852
        } else {
853
            $order = '';
854
        }
855
856
        return $order;
857
    }
858
859
    /**
860
     * Generates the pagination instructions for the query
861
     * @return string
862
     */
863
    private function createQueryPagination()
864
    {
865
        // Reset mysqli params just in case createQueryParagination()
866
        // had been called earlier
867
        $this->paginationParameters = array();
868
869
        if ($this->page === null && !$this->limited) {
870
            return '';
871
        }
872
873
        $offset = '';
874
        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...
875
            $firstElement = ($this->page - 1) * $this->resultsPerPage;
876
            $this->paginationParameters[] = $firstElement;
877
878
            $offset = '?,';
879
        }
880
881
        $this->paginationParameters[] = $this->resultsPerPage;
882
883
        return "LIMIT $offset ?";
884
    }
885
886
    /**
887
     * Set the current column we're working on
888
     *
889
     * @param string $column The column we're selecting
890
     * @param string $mode   Either 'where' or 'having'
891
     *
892
     * @return $this
893
     */
894
    private function grabColumn($column, $mode)
895
    {
896
        if (!isset($this->columns[$column])) {
897
            throw new InvalidArgumentException("Unknown column '$column'");
898
        }
899
900
        $this->column($this->columns[$column], $mode);
901
902
        return $this;
903
    }
904
}
905