Completed
Push — feature/player-elo-v3 ( 8c49df...69db9f )
by Vladimir
03:30
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
    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
    public function __construct($type, $options = array())
161
    {
162
        $this->type = $type;
163
164
        if (isset($options['columns'])) {
165
            $this->columns += $options['columns'];
166
        }
167
168
        if (isset($options['name'])) {
169
            $this->nameColumn = $options['name'];
170
        }
171
    }
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
    public function where($column)
182
    {
183
        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
    public function equals($string)
206
    {
207
        $this->addColumnCondition("= ?", $string);
208
209
        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
    public function notEquals($string)
219
    {
220
        $this->addColumnCondition("!= ?", $string);
221
222
        return $this;
223
    }
224
225
    /**
226
     * Request that a column is not null
227
     *
228
     * @return static
229
     */
230
    public function isNotNull()
231
    {
232
        $this->addColumnCondition('IS NOT NULL', null);
233
234
        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
    public function isAfter($time, $inclusive = false, $reverse = false)
299
    {
300
        if ($time instanceof TimeDate) {
301
            $time = $time->toMysql();
302
        }
303
304
        $comparison  = ($reverse)   ? '<' : '>';
305
        $comparison .= ($inclusive) ? '=' : '';
306
307
        $this->addColumnCondition("$comparison ?",  $time);
308
309
        return $this;
310
    }
311
312
    /**
313
     * Request that a timestamp is between a date range
314
     *
315
     * @param MonthDateRange $range
316
     * @param int|null       $year
317
     */
318
    public function betweenMonths(MonthDateRange $range, $year = null)
319
    {
320
        $this->isAfter($range->getStartOfRange($year), true);
321
        $this->isBefore($range->getEndOfRange($year), true);
322
    }
323
324
    /**
325
     * Request that a column equals a number
326
     *
327
     * @param  int|Model|null $number The number that the column's value should
328
     *                                equal to. If a Model is provided, use the
329
     *                                model's ID, while null values are ignored.
330
     * @return static
331
     */
332 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...
333
    {
334
        if ($number === null) {
335
            return $this;
336
        }
337
338
        if ($number instanceof Model) {
339
            $number = $number->getId();
340
        }
341
342
        $this->addColumnCondition("= ?", $number);
343
344
        return $this;
345
    }
346
347
    /**
348
     * Request that a column equals one of some strings
349
     *
350
     * @todo   Improve for PDO
351
     * @param  string[] $strings The list of accepted values for the column
352
     * @return static
353
     */
354
    public function isOneOf($strings)
355
    {
356
        $count = count($strings);
357
        $questionMarks = str_repeat(',?', $count);
358
359
        // Remove first comma from questionMarks so that MySQL can read our query
360
        $questionMarks = ltrim($questionMarks, ',');
361
362
        $this->addColumnCondition("IN ($questionMarks)", $strings);
363
364
        return $this;
365
    }
366
367
    /**
368
     * Request that a column value starts with a string (case-insensitive)
369
     *
370
     * @param  string $string The substring that the column's value should start with
371
     * @return static
372
     */
373
    public function startsWith($string)
374
    {
375
        $this->addColumnCondition("LIKE CONCAT(?, '%')", $string);
376
377
        return $this;
378
    }
379
380
    /**
381
     * Request that a specific model is not returned
382
     *
383
     * @param  Model|int $model The ID or model you don't want to get
384
     * @return static
385
     */
386 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...
387
    {
388
        if ($model instanceof Model) {
389
            $model = $model->getId();
390
        }
391
392
        $this->where('id');
393
        $this->addColumnCondition("!= ?", $model);
394
395
        return $this;
396
    }
397
398
    /**
399
     * Return the results sorted by the value of a column
400
     *
401
     * @param  string $column The column based on which the results should be ordered
402
     * @return static
403
     */
404
    public function sortBy($column)
405
    {
406
        if (!isset($this->columns[$column])) {
407
            throw new Exception("Unknown column");
408
        }
409
410
        $this->sortBy = $this->columns[$column];
411
412
        return $this;
413
    }
414
415
    /**
416
     * Reverse the order
417
     *
418
     * Note: This only works if you have specified a column in the sortBy() method
419
     *
420
     * @return static
421
     */
422
    public function reverse()
423
    {
424
        $this->reverseSort = !$this->reverseSort;
425
426
        return $this;
427
    }
428
429
    /**
430
     * Specify the number of results per page
431
     *
432
     * @param  int  $count The number of results
433
     * @return static
434
     */
435
    public function limit($count)
436
    {
437
        $this->resultsPerPage = $count;
438
439
        return $this;
440
    }
441
442
    /**
443
     * Only show results from a specific page
444
     *
445
     * @param  int|null $page The page number (or null to show all pages - counting starts from 0)
446
     * @return static
447
     */
448
    public function fromPage($page)
449
    {
450
        $this->page = $page;
451
452
        return $this;
453
    }
454
455
    /**
456
     * End with a specific result
457
     *
458
     * @param  int|Model $model     The model (or database ID) after the first result
459
     * @param  bool   $inclusive Whether to include the provided model
460
     * @param  bool   $reverse   Whether to reverse the results
461
     * @return static
462
     */
463
    public function endAt($model, $inclusive = false, $reverse = false)
464
    {
465
        return $this->startAt($model, $inclusive, !$reverse);
466
    }
467
468
    /**
469
     * Start with a specific result
470
     *
471
     * @param  int|Model $model     The model (or database ID) before the first result
472
     * @param  bool   $inclusive Whether to include the provided model
473
     * @param  bool   $reverse   Whether to reverse the results
474
     * @return static
475
     */
476
    public function startAt($model, $inclusive = false, $reverse = false)
477
    {
478
        if (!$model) {
479
            return $this;
480
        } elseif ($model instanceof Model && !$model->isValid()) {
481
            return $this;
482
        }
483
484
        $this->column($this->sortBy);
485
        $this->limited = true;
486
        $column = $this->currentColumn;
487
        $table  = $this->getTable();
488
489
        $comparison  = $this->reverseSort ^ $reverse;
490
        $comparison  = ($comparison) ? '>' : '<';
491
        $comparison .= ($inclusive)  ? '=' : '';
492
        $id = ($model instanceof Model) ? $model->getId() : $model;
493
494
        // Compare an element's timestamp to the timestamp of $model; if it's the
495
        // same, perform the comparison using IDs
496
        $this->addColumnCondition(
497
            "$comparison (SELECT $column FROM $table WHERE id = ?) OR ($column = (SELECT $column FROM $table WHERE id = ?) AND id $comparison ?)",
498
            array($id, $id, $id)
499
        );
500
501
        return $this;
502
    }
503
504
    /**
505
     * Request that only "active" Models should be returned
506
     *
507
     * @return static
508
     */
509
    public function active()
510
    {
511
        if (!isset($this->columns['status'])) {
512
            return $this;
513
        }
514
515
        $type = $this->type;
516
517
        return $this->where('status')->isOneOf($type::getActiveStatuses());
518
    }
519
520
    /**
521
     * Make sure that Models invisible to a player are not returned
522
     *
523
     * Note that this method does not take PermissionModel::canBeSeenBy() into
524
     * consideration for performance purposes, so you will have to override this
525
     * in your query builder if necessary.
526
     *
527
     * @param  Player  $player      The player in question
528
     * @param  bool $showDeleted false to hide deleted models even from admins
529
     * @return static
530
     */
531
    public function visibleTo($player, $showDeleted = false)
532
    {
533
        $type = $this->type;
534
535
        if (is_subclass_of($type, "PermissionModel")
536
         && $player->hasPermission($type::EDIT_PERMISSION)) {
537
            // The player is an admin who can see hidden models
538
            if (!$showDeleted) {
539
                if (isset($this->columns['status'])) {
540
                    return $this->where('status')->notEquals('deleted');
541
                }
542
            }
543
        } else {
544
            return $this->active();
545
        }
546
547
        return $this;
548
    }
549
550
    /**
551
     * Perform the query and get back the results in an array of names
552
     *
553
     * @return string[] An array of the type $id => $name
554
     */
555
    public function getNames()
556
    {
557
        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...
558
            throw new Exception("You haven't specified a name column");
559
        }
560
561
        $results = $this->getArray($this->nameColumn);
562
563
        return array_column($results, $this->nameColumn, 'id');
564
    }
565
566
    /**
567
     * Perform the query and get back the results in a list of arrays
568
     *
569
     * @todo   Play with partial models?
570
     * @param  string|string[] $columns The column(s) that should be returned
571
     * @return array[]
572
     */
573
    public function getArray($columns)
574
    {
575
        if (!is_array($columns)) {
576
            $columns = array($columns);
577
        }
578
579
        $db = Database::getInstance();
580
581
        return $db->query($this->createQuery($columns), $this->getParameters());
582
    }
583
584
    /**
585
     * An alias for QueryBuilder::getModels(), with fast fetching on by default
586
     * and no return of results
587
     *
588
     * @param  bool $fastFetch Whether to perform one query to load all
589
     *                            the model data instead of fetching them
590
     *                            one by one
591
     * @return void
592
     */
593
    public function addToCache($fastFetch = true)
594
    {
595
        $this->getModels($fastFetch);
596
    }
597
598
    /**
599
     * Perform the query and get the results as Models
600
     *
601
     * @todo Fix fast fetch for queries with multiple tables
602
     * @param  bool $fastFetch Whether to perform one query to load all
603
     *                            the model data instead of fetching them
604
     *                            one by one (ignores cache)
605
     * @return array
606
     */
607
    public function getModels($fastFetch = false)
608
    {
609
        $db   = Database::getInstance();
610
        $type = $this->type;
611
612
        $columns = ($fastFetch) ? $type::getEagerColumns($this->getFromAlias()) : array();
613
614
        if (is_string($columns) && !empty($this->extraColumns)) {
615
            $columns .= ',' . $this->extraColumns;
616
        }
617
618
        // Storing the value in a variable allows for quicker debugging
619
        $query = $this->createQuery($columns);
620
        $results = $db->query($query, $this->getParameters());
621
622
        if ($fastFetch) {
623
            return $type::createFromDatabaseResults($results);
624
        } else {
625
            return $type::arrayIdToModel(array_column($results, 'id'));
626
        }
627
    }
628
629
    /**
630
     * Count the results
631
     *
632
     * @return int
633
     */
634
    public function count()
635
    {
636
        $table  = $this->getTable();
637
        $params = $this->createQueryParams(false);
638
        $db     = Database::getInstance();
639
        $query  = "SELECT COUNT(*) FROM $table $params";
640
641
        // We don't want pagination to affect our results so don't use the functions that combine
642
        // pagination results
643
        $results = $db->query($query, $this->parameters);
644
645
        return $results[0]['COUNT(*)'];
646
    }
647
648
    /**
649
     * Count the number of pages that all the models could be separated into
650
     */
651
    public function countPages()
652
    {
653
        return ceil($this->count() / $this->getResultsPerPage());
654
    }
655
656
    /**
657
     * Find if there is any result
658
     *
659
     * @return bool
660
     */
661
    public function any()
662
    {
663
        // Make sure that we don't mess with the user's options
664
        $query = clone $this;
665
666
        $query->limit(1);
667
668
        return $query->count() > 0;
669
    }
670
671
    /**
672
     * Get the amount of results that are returned per page
673
     * @return int
674
     */
675
    public function getResultsPerPage()
676
    {
677
        return $this->resultsPerPage;
678
    }
679
680
    /**
681
     * Select a column to perform opeations on
682
     *
683
     * This is identical to the `where()` method, except that the column is
684
     * specified as a MySQL column and not as a column name given by the model
685
     *
686
     * @param  string $column The column to select
687
     * @param  string $mode   Whether this column is static or is a column from an aggregate function; Either 'having' or 'where'
688
     *
689
     * @return static
690
     */
691
    protected function column($column, $mode = self::COL_WHERE)
692
    {
693
        $this->currentColumnMode = $mode;
694
695
        if (strpos($column, '.') === false) {
696
            // Add the table name to the column if it isn't there already so that
697
            // MySQL knows what to do when handling multiple tables
698
            $this->currentColumn = ($this->currentColumnMode == self::COL_HAVING) ? "$column" : "`{$this->getFromAlias()}`.`$column`";
699
        } else {
700
            $this->currentColumn = $column;
701
        }
702
703
        $this->currentColumnRaw = $column;
704
705
        return $this;
706
    }
707
708
    /**
709
     * Add a condition for the column
710
     * @param  string $condition The MySQL condition
711
     * @param  mixed  $value     Value(s) to pass to MySQL
712
     * @return void
713
     */
714
    protected function addColumnCondition($condition, $value)
715
    {
716
        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...
717
            throw new Exception("You haven't selected a column!");
718
        }
719
720
        if (!is_array($value) && $value !== null) {
721
            $value = array($value);
722
        }
723
724
        $array = $this->currentColumnMode . 'Conditions';
725
        $this->{$array}[] = "{$this->currentColumn} $condition";
726
727
        if ($value !== null) {
728
            $this->parameters = array_merge($this->parameters, $value);
729
        }
730
731
        $this->currentColumn = null;
732
        $this->currentColumnRaw = null;
733
    }
734
735
    /**
736
     * Get the MySQL extra parameters
737
     *
738
     * @param  bool $respectPagination Whether to respect pagination or not; useful for when pagination should be ignored such as count
739
     * @return string
740
     */
741
    protected function createQueryParams($respectPagination = true)
742
    {
743
        $extras     = $this->extras;
744
        $conditions = $this->createQueryConditions('where');
745
        $groupQuery = $this->groupQuery;
746
        $havingClause = $this->createQueryConditions('having');
747
        $order      = $this->createQueryOrder();
748
        $pagination = "";
749
750
        if ($respectPagination) {
751
            $pagination = $this->createQueryPagination();
752
        }
753
754
        return "$extras $conditions $groupQuery $havingClause $order $pagination";
755
    }
756
757
    /**
758
     * Get the query parameters
759
     *
760
     * @return array
761
     */
762
    protected function getParameters()
763
    {
764
        return array_merge($this->parameters, $this->paginationParameters);
765
    }
766
767
    /**
768
     * Get the alias used for the table in the FROM clause
769
     *
770
     * @return null|string
771
     */
772
    protected function getFromAlias()
773
    {
774
        return $this->getTable();
775
    }
776
777
    /**
778
     * Get the table of the model
779
     *
780
     * @return string
781
     */
782
    protected function getTable()
783
    {
784
        $type = $this->type;
785
786
        return $type::TABLE;
787
    }
788
789
    /**
790
     * Get a MySQL query string in the requested format
791
     * @param  string|string[] $columns The columns that should be included
792
     *                                  (without the ID, if an array is provided)
793
     * @return string The query
794
     */
795
    protected function createQuery($columns = array())
796
    {
797
        $type     = $this->type;
798
        $table    = $type::TABLE;
799
        $params   = $this->createQueryParams();
800
801
        if (is_array($columns)) {
802
            $columns = $this->createQueryColumns($columns);
803
        } elseif (empty($columns)) {
804
            $columns = $this->createQueryColumns();
805
        }
806
807
        return "SELECT $columns FROM $table $params";
808
    }
809
810
    /**
811
     * Generate the columns for the query
812
     * @param  string[] $columns The columns that should be included (without the ID)
813
     * @return string
814
     */
815
    private function createQueryColumns($columns = array())
816
    {
817
        $type = $this->type;
818
        $table = $type::TABLE;
819
        $columnStrings = array("`$table`.id");
820
821
        foreach ($columns as $returnName) {
822
            if (strpos($returnName, ' ') === false) {
823
                $dbName = $this->columns[$returnName];
824
                $columnStrings[] = "`$table`.`$dbName` as `$returnName`";
825
            } else {
826
                // "Column" contains a space, pass it as is
827
                $columnStrings[] = $returnName;
828
            }
829
        }
830
831
        return implode(',', $columnStrings);
832
    }
833
834
    /**
835
     * Generates all the WHERE conditions for the query
836
     * @return string
837
     */
838
    private function createQueryConditions($mode)
839
    {
840
        $array = $mode . 'Conditions';
841
842
        if ($this->{$array}) {
843
            // Add parentheses around the conditions to prevent conflicts due
844
            // to the order of operations
845
            $conditions = array_map(function ($value) { return "($value)"; }, $this->{$array});
846
847
            return strtoupper($mode) . ' ' . implode(' AND ', $conditions);
848
        }
849
850
        return '';
851
    }
852
853
    /**
854
     * Generates the sorting instructions for the query
855
     * @return string
856
     */
857
    private function createQueryOrder()
858
    {
859
        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...
860
            $order = 'ORDER BY ' . $this->sortBy;
861
862
            // Sort by ID if the sorting columns are equal
863
            $id = "`{$this->getFromAlias()}`.`id`";
864
            if ($this->reverseSort) {
865
                $order .= " DESC, $id DESC";
866
            } else {
867
                $order .= ", $id";
868
            }
869
        } else {
870
            $order = '';
871
        }
872
873
        return $order;
874
    }
875
876
    /**
877
     * Generates the pagination instructions for the query
878
     * @return string
879
     */
880
    private function createQueryPagination()
881
    {
882
        // Reset mysqli params just in case createQueryParagination()
883
        // had been called earlier
884
        $this->paginationParameters = array();
885
886
        if ($this->page === null && !$this->limited) {
887
            return '';
888
        }
889
890
        $offset = '';
891
        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...
892
            $firstElement = ($this->page - 1) * $this->resultsPerPage;
893
            $this->paginationParameters[] = $firstElement;
894
895
            $offset = '?,';
896
        }
897
898
        $this->paginationParameters[] = $this->resultsPerPage;
899
900
        return "LIMIT $offset ?";
901
    }
902
903
    /**
904
     * Set the current column we're working on
905
     *
906
     * @param string $column The column we're selecting
907
     * @param string $mode   Either 'where' or 'having'
908
     *
909
     * @return $this
910
     */
911
    private function grabColumn($column, $mode)
912
    {
913
        if (!isset($this->columns[$column])) {
914
            throw new InvalidArgumentException("Unknown column '$column'");
915
        }
916
917
        $this->column($this->columns[$column], $mode);
918
919
        return $this;
920
    }
921
}
922