Completed
Push — master ( 7c2796...90a4f2 )
by Vladimir
02:44
created

QueryBuilder::selectColumn()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.4285
c 0
b 0
f 0
ccs 2
cts 2
cp 1
cc 2
eloc 5
nc 2
nop 2
crap 2
1
<?php
2
/**
3
 * This file contains a class to quickly generate database queries for models
4
 *
5
 * @package    BZiON\Models\QueryBuilder
6
 * @license    https://github.com/allejo/bzion/blob/master/LICENSE.md GNU General Public License Version 3
7
 */
8
9
/**
10
 * This class can be used to search for models with specific characteristics in
11
 * the database.
12
 *
13
 * Note that most methods of this class return itself, so that you can easily
14
 * add a number of different filters.
15
 *
16
 * <code>
17
 *     return Team::getQueryBuilder()
18
 *     ->active()
19
 *     ->where('name')->startsWith('a')
20
 *     ->sortBy('name')->reverse()
21
 *     ->getModels();
22
 * </code>
23
 *
24
 * @package    BZiON\Models\QueryBuilder
25
 */
26
class QueryBuilder implements Countable
27
{
28
    const COL_HAVING = 'having';
29
    const COL_WHERE = 'where';
30
31
    /**
32
     * The type of the model we're building a query for
33
     * @var string
34
     */
35
    protected $type;
36
37
    /**
38
     * The columns that the model provided us
39
     * @var array
40
     */
41
    protected $columns = array('id' => 'id');
42
43
    /**
44
     * Extra columns that are generated from the SQL query (this should be a comma separated string or null)
45
     * @var string|null
46
     */
47
    protected $extraColumns = null;
48
49
    /**
50
     * The conditions to include in WHERE
51
     * @var string[]
52
     */
53
    protected $whereConditions = array();
54
55
    /**
56
     * The conditions to include in HAVING
57
     * @var string[]
58
     */
59
    protected $havingConditions = array();
60
61
    /**
62
     * The MySQL value parameters
63
     * @var array
64
     */
65
    protected $parameters = array();
66
67
    /**
68
     * The MySQL value parameters for pagination
69
     * @var array
70
     */
71
    protected $paginationParameters = array();
72
73
    /**
74
     * Extra MySQL query string to pass
75
     * @var string
76
     */
77
    protected $extras = '';
78
79
    /**
80
     * Extra MySQL query groupby string to pass
81
     * @var string
82
     */
83
    protected $groupQuery = '';
84
85
    /**
86
     * A column based on which we should sort the results
87
     * @var string|null
88
     */
89
    private $sortBy = null;
90
91
    /**
92
     * Whether to reverse the results
93
     * @var bool
94
     */
95
    private $reverseSort = false;
96
97
    /**
98
     * The currently selected column
99
     * @var string|null
100
     */
101
    private $currentColumn = null;
102
103
    /**
104
     * Either 'where' or 'having'
105
     * @var string
106
     */
107
    private $currentColumnMode = '';
108
109
    /**
110
     * The currently selected column without the table name (unless it was
111
     * explicitly provided)
112
     * @var string|null
113
     */
114
    protected $currentColumnRaw = null;
115
116
    /**
117
     * A column to consider the name of the model
118
     * @var string|null
119
     */
120
    private $nameColumn = null;
121
122
    /**
123
     * The page to return
124
     * @var int|null
125
     */
126
    private $page = null;
127
128
    /**
129
     * Whether the ID of the first/last element has been provided
130
     * @var bool
131
     */
132
    private $limited = false;
133
134
    /**
135
     * The number of elements on every page
136
     * @var int
137
     */
138
    protected $resultsPerPage = 30;
139
140
    /**
141
     * Create a new QueryBuilder
142
     *
143
     * A new query builder should be created on a static getQueryBuilder()
144
     * method on each model. The options array can contain the following
145
     * properties:
146
     *
147
     * - `columns`: An associative array - the key of each entry is the column
148
     *   name that will be used by other methods, while the value is
149
     *   is the column name that is used in the database structure
150
     *
151
     * - `activeStatuses`: If the model has a status column, this should be
152
     *                     a list of values that make the entry be considered
153
     *                     "active"
154
     *
155
     * - `name`: The name of the column which represents the name of the object
156
     *
157
     * @param string $type    The type of the Model (e.g. "Player" or "Match")
158
     * @param array  $options The options to pass to the builder (see above)
159
     */
160 27
    public function __construct($type, $options = array())
161
    {
162 27
        $this->type = $type;
163
164 27
        if (isset($options['columns'])) {
165 27
            $this->columns += $options['columns'];
166
        }
167
168 27
        if (isset($options['name'])) {
169 2
            $this->nameColumn = $options['name'];
170
        }
171 27
    }
172
173
    /**
174
     * 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 timestamp is between a date range
333
     *
334 1
     * @param MonthDateRange $range
335
     * @param int|null       $year
336
     */
337
    public function betweenMonths(MonthDateRange $range, $year = null)
338 1
    {
339 1
        $this->isAfter($range->getStartOfRange($year), true);
340
        $this->isBefore($range->getEndOfRange($year), true);
341
    }
342 1
343
    /**
344 1
     * Request that a column equals a number
345
     *
346
     * @param  int|Model|null $number The number that the column's value should
347
     *                                equal to. If a Model is provided, use the
348
     *                                model's ID, while null values are ignored.
349
     * @return static
350
     */
351 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...
352
    {
353
        if ($number === null) {
354 3
            return $this;
355
        }
356 3
357 3
        if ($number instanceof Model) {
358
            $number = $number->getId();
359
        }
360 3
361
        $this->addColumnCondition("= ?", $number);
362 3
363
        return $this;
364 3
    }
365
366
    /**
367
     * Request that a column equals one of some strings
368
     *
369
     * @todo   Improve for PDO
370
     * @param  string[] $strings The list of accepted values for the column
371
     * @return static
372
     */
373
    public function isOneOf($strings)
374
    {
375
        $count = count($strings);
376
        $questionMarks = str_repeat(',?', $count);
377
378
        // Remove first comma from questionMarks so that MySQL can read our query
379
        $questionMarks = ltrim($questionMarks, ',');
380
381
        $this->addColumnCondition("IN ($questionMarks)", $strings);
382
383
        return $this;
384
    }
385
386
    /**
387
     * Request that a column value starts with a string (case-insensitive)
388
     *
389
     * @param  string $string The substring that the column's value should start with
390
     * @return static
391
     */
392
    public function startsWith($string)
393
    {
394
        $this->addColumnCondition("LIKE CONCAT(?, '%')", $string);
395
396
        return $this;
397
    }
398
399
    /**
400
     * Request that a specific model is not returned
401
     *
402
     * @param  Model|int $model The ID or model you don't want to get
403
     * @return static
404 3
     */
405 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...
406 3
    {
407
        if ($model instanceof Model) {
408
            $model = $model->getId();
409
        }
410 3
411
        $this->where('id');
412 3
        $this->addColumnCondition("!= ?", $model);
413
414
        return $this;
415
    }
416
417
    /**
418
     * Return the results sorted by the value of a column
419
     *
420
     * @param  string $column The column based on which the results should be ordered
421
     * @return static
422 2
     */
423
    public function sortBy($column)
424 2
    {
425
        if (!isset($this->columns[$column])) {
426 2
            throw new Exception("Unknown column");
427
        }
428
429
        $this->sortBy = $this->columns[$column];
430
431
        return $this;
432
    }
433
434
    /**
435 2
     * Reverse the order
436
     *
437 2
     * Note: This only works if you have specified a column in the sortBy() method
438
     *
439 2
     * @return static
440
     */
441
    public function reverse()
442
    {
443
        $this->reverseSort = !$this->reverseSort;
444
445
        return $this;
446
    }
447
448 2
    /**
449
     * Specify the number of results per page
450 2
     *
451
     * @param  int  $count The number of results
452 2
     * @return static
453
     */
454
    public function limit($count)
455
    {
456
        $this->resultsPerPage = $count;
457
458
        return $this;
459
    }
460
461
    /**
462
     * Only show results from a specific page. This will
463 1
     *
464
     * @param  int|null $page The page number (or null to show all pages - counting starts from 0)
465 1
     * @return static
466
     */
467
    public function fromPage($page)
468
    {
469
        if ($page === null) {
470
            $this->page = $page;
471
            return $this;
472
        }
473
474
        $page = intval($page);
475
        $page = ($page <= 0) ? 1 : $page;
476 1
477
        $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...
478 1
479 1
        return $this;
480
    }
481
482
    /**
483
     * End with a specific result
484
     *
485
     * @param  int|Model $model     The model (or database ID) after the first result
486
     * @param  bool   $inclusive Whether to include the provided model
487
     * @param  bool   $reverse   Whether to reverse the results
488
     * @return static
489
     */
490
    public function endAt($model, $inclusive = false, $reverse = false)
491
    {
492
        return $this->startAt($model, $inclusive, !$reverse);
493
    }
494
495
    /**
496
     * Start with a specific result
497
     *
498
     * @param  int|Model $model     The model (or database ID) before the first result
499
     * @param  bool   $inclusive Whether to include the provided model
500
     * @param  bool   $reverse   Whether to reverse the results
501
     * @return static
502
     */
503
    public function startAt($model, $inclusive = false, $reverse = false)
504
    {
505
        if (!$model) {
506
            return $this;
507
        } elseif ($model instanceof Model && !$model->isValid()) {
508
            return $this;
509 3
        }
510
511 3
        $this->column($this->sortBy);
512
        $this->limited = true;
513
        $column = $this->currentColumn;
514
        $table  = $this->getTable();
515 3
516
        $comparison  = $this->reverseSort ^ $reverse;
517 3
        $comparison  = ($comparison) ? '>' : '<';
518
        $comparison .= ($inclusive)  ? '=' : '';
519
        $id = ($model instanceof Model) ? $model->getId() : $model;
520
521
        // Compare an element's timestamp to the timestamp of $model; if it's the
522
        // same, perform the comparison using IDs
523
        $this->addColumnCondition(
524
            "$comparison (SELECT $column FROM $table WHERE id = ?) OR ($column = (SELECT $column FROM $table WHERE id = ?) AND id $comparison ?)",
525
            array($id, $id, $id)
526
        );
527
528
        return $this;
529
    }
530
531 1
    /**
532
     * Request that only "active" Models should be returned
533 1
     *
534
     * @return static
535 1
     */
536 1
    public function active()
537
    {
538 1
        if (!isset($this->columns['status'])) {
539 1
            return $this;
540 1
        }
541
542
        $type = $this->type;
543
544 1
        return $this->where('status')->isOneOf($type::getActiveStatuses());
545
    }
546
547
    /**
548
     * Make sure that Models invisible to a player are not returned
549
     *
550
     * Note that this method does not take PermissionModel::canBeSeenBy() into
551
     * consideration for performance purposes, so you will have to override this
552
     * in your query builder if necessary.
553
     *
554
     * @param  Player  $player      The player in question
555 1
     * @param  bool $showDeleted false to hide deleted models even from admins
556
     * @return static
557 1
     */
558
    public function visibleTo($player, $showDeleted = false)
559
    {
560
        $type = $this->type;
561 1
562
        if (is_subclass_of($type, "PermissionModel")
563 1
         && $player->hasPermission($type::EDIT_PERMISSION)) {
564
            // The player is an admin who can see hidden models
565
            if (!$showDeleted) {
566
                if (isset($this->columns['status'])) {
567
                    return $this->where('status')->notEquals('deleted');
568
                }
569
            }
570
        } else {
571
            return $this->active();
572
        }
573 1
574
        return $this;
575 1
    }
576 1
577
    /**
578
     * Perform the query and get back the results in an array of names
579 1
     *
580
     * @return string[] An array of the type $id => $name
581 1
     */
582
    public function getNames()
583
    {
584
        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...
585
            throw new Exception("You haven't specified a name column");
586
        }
587
588
        $results = $this->getArray($this->nameColumn);
589
590
        return array_column($results, $this->nameColumn, 'id');
591
    }
592
593
    /**
594
     * Perform the query and get back the results in a list of arrays
595
     *
596
     * @todo   Play with partial models?
597
     * @param  string|string[] $columns The column(s) that should be returned
598
     * @return array[]
599
     */
600
    public function getArray($columns)
601
    {
602
        if (!is_array($columns)) {
603
            $columns = array($columns);
604
        }
605
606
        $db = Database::getInstance();
607 27
608
        return $db->query($this->createQuery($columns), $this->getParameters());
609 27
    }
610 27
611
    /**
612 27
     * An alias for QueryBuilder::getModels(), with fast fetching on by default
613
     * and no return of results
614 27
     *
615
     * @param  bool $fastFetch Whether to perform one query to load all
616
     *                            the model data instead of fetching them
617
     *                            one by one
618
     * @return void
619 27
     */
620 27
    public function addToCache($fastFetch = true)
621
    {
622 27
        $this->getModels($fastFetch);
623 25
    }
624
625 3
    /**
626
     * Perform the query and get the results as Models
627
     *
628
     * @todo Fix fast fetch for queries with multiple tables
629
     * @param  bool $fastFetch Whether to perform one query to load all
630
     *                            the model data instead of fetching them
631
     *                            one by one (ignores cache)
632
     * @return array
633
     */
634 1
    public function getModels($fastFetch = false)
635
    {
636 1
        $db   = Database::getInstance();
637 1
        $type = $this->type;
638 1
639 1
        $columns = ($fastFetch) ? $type::getEagerColumns($this->getFromAlias()) : array();
640
641
        if (is_string($columns) && !empty($this->extraColumns)) {
642
            $columns .= ',' . $this->extraColumns;
643 1
        }
644
645 1
        // Storing the value in a variable allows for quicker debugging
646
        $query = $this->createQuery($columns);
647
        $results = $db->query($query, $this->getParameters());
648
649
        if ($fastFetch) {
650
            return $type::createFromDatabaseResults($results);
651 1
        } else {
652
            return $type::arrayIdToModel(array_column($results, 'id'));
653 1
        }
654
    }
655
656
    /**
657
     * Count the results
658
     *
659
     * @return int
660
     */
661 1
    public function count()
662
    {
663
        $table  = $this->getTable();
664 1
        $params = $this->createQueryParams(false);
665
        $db     = Database::getInstance();
666 1
        $query  = "SELECT COUNT(*) FROM $table $params";
667
668 1
        // We don't want pagination to affect our results so don't use the functions that combine
669
        // pagination results
670
        $results = $db->query($query, $this->parameters);
671
672
        return $results[0]['COUNT(*)'];
673
    }
674
675 1
    /**
676
     * Count the number of pages that all the models could be separated into
677 1
     */
678
    public function countPages()
679
    {
680
        return ceil($this->count() / $this->getResultsPerPage());
681
    }
682
683
    /**
684
     * Find if there is any result
685
     *
686
     * @return bool
687
     */
688
    public function any()
689
    {
690
        // Make sure that we don't mess with the user's options
691 5
        $query = clone $this;
692
693 5
        $query->limit(1);
694
695 5
        return $query->count() > 0;
696
    }
697
698 5
    /**
699
     * Get the amount of results that are returned per page
700 1
     * @return int
701
     */
702
    public function getResultsPerPage()
703 5
    {
704
        return $this->resultsPerPage;
705 5
    }
706
707
    /**
708
     * Select a column to perform opeations on
709
     *
710
     * This is identical to the `where()` method, except that the column is
711
     * specified as a MySQL column and not as a column name given by the model
712
     *
713
     * @param  string $column The column to select
714 5
     * @param  string $mode   Whether this column is static or is a column from an aggregate function; Either 'having' or 'where'
715
     *
716 5
     * @return static
717
     */
718
    protected function column($column, $mode = self::COL_WHERE)
719
    {
720 5
        $this->currentColumnMode = $mode;
721 3
722
        if (strpos($column, '.') === false) {
723
            // Add the table name to the column if it isn't there already so that
724 5
            // MySQL knows what to do when handling multiple tables
725 5
            $this->currentColumn = ($this->currentColumnMode == self::COL_HAVING) ? "$column" : "`{$this->getFromAlias()}`.`$column`";
726
        } else {
727 5
            $this->currentColumn = $column;
728 5
        }
729
730
        $this->currentColumnRaw = $column;
731 5
732 5
        return $this;
733 5
    }
734
735
    /**
736
     * Add a condition for the column
737
     * @param  string $condition The MySQL condition
738
     * @param  mixed  $value     Value(s) to pass to MySQL
739
     * @return void
740
     */
741 27
    protected function addColumnCondition($condition, $value)
742
    {
743 27
        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...
744 27
            throw new Exception("You haven't selected a column!");
745 27
        }
746 27
747 27
        if (!is_array($value) && $value !== null) {
748 27
            $value = array($value);
749
        }
750 27
751 27
        $array = $this->currentColumnMode . 'Conditions';
752
        $this->{$array}[] = "{$this->currentColumn} $condition";
753
754 27
        if ($value !== null) {
755
            $this->parameters = array_merge($this->parameters, $value);
756
        }
757
758
        $this->currentColumn = null;
759
        $this->currentColumnRaw = null;
760
    }
761
762 27
    /**
763
     * Get the MySQL extra parameters
764 27
     *
765
     * @param  bool $respectPagination Whether to respect pagination or not; useful for when pagination should be ignored such as count
766
     * @return string
767
     */
768
    protected function createQueryParams($respectPagination = true)
769
    {
770
        $extras     = $this->extras;
771
        $conditions = $this->createQueryConditions('where');
772 27
        $groupQuery = $this->groupQuery;
773
        $havingClause = $this->createQueryConditions('having');
774 27
        $order      = $this->createQueryOrder();
775
        $pagination = "";
776
777
        if ($respectPagination) {
778
            $pagination = $this->createQueryPagination();
779
        }
780
781
        return "$extras $conditions $groupQuery $havingClause $order $pagination";
782 27
    }
783
784 27
    /**
785
     * Get the query parameters
786 27
     *
787
     * @return array
788
     */
789
    protected function getParameters()
790
    {
791
        return array_merge($this->parameters, $this->paginationParameters);
792
    }
793
794
    /**
795 27
     * Get the alias used for the table in the FROM clause
796
     *
797 27
     * @return null|string
798 27
     */
799 27
    protected function getFromAlias()
800
    {
801 27
        return $this->getTable();
802 3
    }
803
804
    /**
805
     * Get the table of the model
806
     *
807 27
     * @return string
808
     */
809
    protected function getTable()
810
    {
811
        $type = $this->type;
812
813
        return $type::TABLE;
814
    }
815 3
816
    /**
817 3
     * Get a MySQL query string in the requested format
818 3
     * @param  string|string[] $columns The columns that should be included
819 3
     *                                  (without the ID, if an array is provided)
820
     * @return string The query
821 3
     */
822 1
    protected function createQuery($columns = array())
823 1
    {
824 1
        $type     = $this->type;
825
        $table    = $type::TABLE;
826
        $params   = $this->createQueryParams();
827 1
828
        if (is_array($columns)) {
829
            $columns = $this->createQueryColumns($columns);
830
        } elseif (empty($columns)) {
831 3
            $columns = $this->createQueryColumns();
832
        }
833
834
        return "SELECT $columns FROM $table $params";
835
    }
836
837
    /**
838 27
     * Generate the columns for the query
839
     * @param  string[] $columns The columns that should be included (without the ID)
840 27
     * @return string
841
     */
842 27
    private function createQueryColumns($columns = array())
843
    {
844
        $type = $this->type;
845
        $table = $type::TABLE;
846
        $columnStrings = array("`$table`.id");
847 5
848
        foreach ($columns as $returnName) {
849
            if (strpos($returnName, ' ') === false) {
850 27
                $dbName = $this->columns[$returnName];
851
                $columnStrings[] = "`$table`.`$dbName` as `$returnName`";
852
            } else {
853
                // "Column" contains a space, pass it as is
854
                $columnStrings[] = $returnName;
855
            }
856
        }
857 27
858
        return implode(',', $columnStrings);
859 27
    }
860 3
861
    /**
862
     * Generates all the WHERE conditions for the query
863 3
     * @return string
864 3
     */
865 2
    private function createQueryConditions($mode)
866
    {
867 3
        $array = $mode . 'Conditions';
868
869
        if ($this->{$array}) {
870 25
            // Add parentheses around the conditions to prevent conflicts due
871
            // to the order of operations
872
            $conditions = array_map(function ($value) { return "($value)"; }, $this->{$array});
873 27
874
            return strtoupper($mode) . ' ' . implode(' AND ', $conditions);
875
        }
876
877
        return '';
878
    }
879
880 27
    /**
881
     * Generates the sorting instructions for the query
882
     * @return string
883
     */
884 27
    private function createQueryOrder()
885
    {
886 27
        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...
887 26
            $order = 'ORDER BY ' . $this->sortBy;
888
889
            // Sort by ID if the sorting columns are equal
890 2
            $id = "`{$this->getFromAlias()}`.`id`";
891 2
            if ($this->reverseSort) {
892 2
                $order .= " DESC, $id DESC";
893 2
            } else {
894
                $order .= ", $id";
895 2
            }
896
        } else {
897
            $order = '';
898 2
        }
899
900 2
        return $order;
901
    }
902
903
    /**
904
     * Generates the pagination instructions for the query
905
     * @return string
906
     */
907
    private function createQueryPagination()
908
    {
909
        // Reset mysqli params just in case createQueryParagination()
910
        // had been called earlier
911 5
        $this->paginationParameters = array();
912
913 5
        if ($this->page === null && !$this->limited) {
914
            return '';
915
        }
916
917 5
        $offset = '';
918
        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...
919 5
            $firstElement = ($this->page - 1) * $this->resultsPerPage;
920
            $this->paginationParameters[] = $firstElement;
921
922
            $offset = '?,';
923
        }
924
925
        $this->paginationParameters[] = $this->resultsPerPage;
926
927
        return "LIMIT $offset ?";
928
    }
929
930
    /**
931
     * Set the current column we're working on
932
     *
933
     * @param string $column The column we're selecting
934
     * @param string $mode   Either 'where' or 'having'
935
     *
936
     * @return $this
937
     */
938
    private function grabColumn($column, $mode)
939
    {
940
        if (!isset($this->columns[$column])) {
941
            throw new InvalidArgumentException("Unknown column '$column'");
942
        }
943
944
        $this->column($this->columns[$column], $mode);
945
946
        return $this;
947
    }
948
}
949