Completed
Push — master ( 8a441b...ebc209 )
by Vladimir
04:28
created

QueryBuilder::betweenMonths()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

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