Completed
Push — fm-matches ( f867e2...7fc64f )
by Vladimir
14:12
created

QueryBuilder   C

Complexity

Total Complexity 78

Size/Duplication

Total Lines 817
Duplicated Lines 3.06 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 79.55%

Importance

Changes 8
Bugs 0 Features 4
Metric Value
wmc 78
c 8
b 0
f 4
lcom 1
cbo 4
dl 25
loc 817
ccs 175
cts 220
cp 0.7955
rs 5

39 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 3
A where() 0 10 2
A equals() 0 6 1
A notEquals() 0 6 1
A greaterThan() 0 6 1
A lessThan() 0 6 1
A isBefore() 0 4 1
A isAfter() 0 13 4
A is() 14 14 3
A isOneOf() 0 13 1
A startsWith() 0 6 1
A except() 11 11 2
A sortBy() 0 10 2
A reverse() 0 6 1
A limit() 0 6 1
A fromPage() 0 6 1
A endAt() 0 4 1
C startAt() 0 28 7
A active() 0 10 2
B visibleTo() 0 18 5
A getNames() 0 10 2
A getArray() 0 10 2
A addToCache() 0 4 1
A getModels() 0 15 3
A count() 0 13 1
A countPages() 0 4 1
A any() 0 9 1
A getResultsPerPage() 0 4 1
A column() 0 15 2
A addColumnCondition() 0 17 3
A createQueryParams() 0 14 2
A getParameters() 0 4 1
A getTypes() 0 4 1
A getTable() 0 6 1
A createQuery() 0 14 3
A createQueryColumns() 0 18 3
A createQueryConditions() 0 12 2
A createQueryOrder() 0 18 3
B createQueryPagination() 0 25 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like QueryBuilder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use QueryBuilder, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * This file contains a class to quickly generate database queries for models
4
 *
5
 * @package    BZiON\Models\QueryBuilder
6
 * @license    https://github.com/allejo/bzion/blob/master/LICENSE.md GNU General Public License Version 3
7
 */
8
9
/**
10
 * This class can be used to search for models with specific characteristics in
11
 * the database.
12
 *
13
 * Note that most methods of this class return itself, so that you can easily
14
 * add a number of different filters.
15
 *
16
 * <code>
17
 *     return Team::getQueryBuilder()
18
 *     ->active()
19
 *     ->where('name')->startsWith('a')
20
 *     ->sortBy('name')->reverse()
21
 *     ->getModels();
22
 * </code>
23
 *
24
 * @package    BZiON\Models\QueryBuilder
25
 */
26
class QueryBuilder implements Countable
27
{
28
    /**
29
     * The type of the model we're building a query for
30
     * @var string
31
     */
32
    protected $type;
33
34
    /**
35
     * The columns that the model provided us
36
     * @var array
37
     */
38
    protected $columns = array('id' => 'id');
39
40
    /**
41
     * The conditions to include in WHERE
42
     * @var string[]
43
     */
44
    protected $conditions = array();
45
46
    /**
47
     * The MySQL value parameters
48
     * @var array
49
     */
50
    protected $parameters = array();
51
52
    /**
53
     * The MySQL parameter types
54
     * @var string
55
     */
56
    protected $types = '';
57
58
    /**
59
     * The MySQL value parameters for pagination
60
     * @var array
61
     */
62
    protected $paginationParameters = array();
63
64
    /**
65
     * The MySQL parameter types for pagination
66
     * @var string
67
     */
68
    protected $paginationTypes = '';
69
70
    /**
71
     * Extra MySQL query string to pass
72
     * @var string
73
     */
74
    protected $extras = '';
75
76
    /**
77
     * Extra MySQL query groupby string to pass
78
     * @var string
79
     */
80
    protected $groupQuery = '';
81
82
    /**
83
     * A column based on which we should sort the results
84
     * @var string|null
85
     */
86
    private $sortBy = null;
87
88
    /**
89
     * Whether to reverse the results
90
     * @var bool
91
     */
92
    private $reverseSort = false;
93
94
    /**
95
     * The currently selected column
96
     * @var string|null
97
     */
98
    private $currentColumn = null;
99
100
    /**
101
     * The currently selected column without the table name (unless it was
102
     * explicitly provided)
103
     * @var string|null
104
     */
105
    protected $currentColumnRaw = null;
106
107
    /**
108
     * A column to consider the name of the model
109
     * @var string|null
110
     */
111
    private $nameColumn = null;
112
113
    /**
114
     * Whether to return the results as arrays instead of models
115
     * @var bool
116
     */
117
    private $returnArray = false;
0 ignored issues
show
Unused Code introduced by
The property $returnArray is not used and could be removed.

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

Loading history...
118
119
    /**
120
     * The page to return
121
     * @var int|null
122
     */
123
    private $page = null;
124
125
    /**
126
     * Whether the ID of the first/last element has been provided
127
     * @var bool
128
     */
129
    private $limited = false;
130
131
    /**
132
     * The number of elements on every page
133
     * @var int
134
     */
135
    protected $resultsPerPage = 30;
136
137
    /**
138
     * Create a new QueryBuilder
139
     *
140
     * A new query builder should be created on a static getQueryBuilder()
141
     * method on each model. The options array can contain the following
142
     * properties:
143
     *
144
     * - `columns`: An associative array - the key of each entry is the column
145
     *   name that will be used by other methods, while the value is
146
     *   is the column name that is used in the database structure
147
     *
148
     * - `activeStatuses`: If the model has a status column, this should be
149
     *                     a list of values that make the entry be considered
150
     *                     "active"
151
     *
152
     * - `name`: The name of the column which represents the name of the object
153
     *
154
     * @param string $type    The type of the Model (e.g. "Player" or "Match")
155
     * @param array  $options The options to pass to the builder (see above)
156
     */
157 4
    public function __construct($type, $options = array())
158
    {
159 4
        $this->type = $type;
160
161 4
        if (isset($options['columns'])) {
162 4
            $this->columns += $options['columns'];
163
        }
164
165 4
        if (isset($options['name'])) {
166 2
            $this->nameColumn = $options['name'];
167
        }
168 4
    }
169
170
    /**
171
     * Select a column
172
     *
173
     * `$queryBuilder->where('username')->equals('administrator');`
174
     *
175
     * @param  string $column The column to select
176
     * @return self
177
     */
178 4
    public function where($column)
179
    {
180 4
        if (!isset($this->columns[$column])) {
181
            throw new InvalidArgumentException("Unknown column '$column'");
182
        }
183
184 4
        $this->column($this->columns[$column]);
185
186 4
        return $this;
187
    }
188
189
    /**
190
     * Request that a column equals a string (case-insensitive)
191
     *
192
     * @param  string $string The string that the column's value should equal to
193
     * @return self
194
     */
195 2
    public function equals($string)
196
    {
197 2
        $this->addColumnCondition("= ?", $string, 's');
198
199 2
        return $this;
200
    }
201
202
    /**
203
     * Request that a column doesNOT equals a string (case-insensitive)
204
     *
205
     * @param  string $string The string that the column's value should equal to
206
     * @return self
207
     */
208 1
    public function notEquals($string)
209
    {
210 1
        $this->addColumnCondition("!= ?", $string, 's');
211
212 1
        return $this;
213
    }
214
215
    /**
216
     * Request that a column is greater than a quantity
217
     *
218
     * @param  string $quantity The quantity to test against
219
     * @return self
220
     */
221
    public function greaterThan($quantity)
222
    {
223
        $this->addColumnCondition("> ?", $quantity, 's');
224
225
        return $this;
226
    }
227
228
    /**
229
     * Request that a column is less than a quantity
230
     *
231
     * @param  string $quantity The quantity to test against
232
     * @return self
233
     */
234
    public function lessThan($quantity)
235
    {
236
        $this->addColumnCondition("< ?", $quantity, 's');
237
238
        return $this;
239
    }
240
241
    /**
242
     * Request that a timestamp is before the specified time
243
     *
244
     * @param string|TimeDate $time      The timestamp to compare to
245
     * @param bool            $inclusive Whether to include the given timestamp
246
     * @param bool            $reverse   Whether to reverse the results
247
     */
248
    public function isBefore($time, $inclusive = false, $reverse = false)
249
    {
250
        return $this->isAfter($time, $inclusive, !$reverse);
251
    }
252
253
    /**
254
     * Request that a timestamp is after the specified time
255
     *
256
     * @param string|TimeDate $time      The timestamp to compare to
257
     * @param bool            $inclusive Whether to include the given timestamp
258
     * @param bool            $reverse   Whether to reverse the results
259
     */
260 1
    public function isAfter($time, $inclusive = false, $reverse = false)
261
    {
262 1
        if ($time instanceof TimeDate) {
263 1
            $time = $time->toMysql();
264
        }
265
266 1
        $comparison  = ($reverse)   ? '<' : '>';
267 1
        $comparison .= ($inclusive) ? '=' : '';
268
269 1
        $this->addColumnCondition("$comparison ?",  $time, 's');
270
271 1
        return $this;
272
    }
273
274
    /**
275
     * Request that a column equals a number
276
     *
277
     * @param  int|Model|null $number The number that the column's value should
278
     *                                equal to. If a Model is provided, use the
279
     *                                model's ID, while null values are ignored.
280
     * @return self
281
     */
282 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...
283
    {
284 1
        if ($number === null) {
285
            return $this;
286
        }
287
288 1
        if ($number instanceof Model) {
289 1
            $number = $number->getId();
290
        }
291
292 1
        $this->addColumnCondition("= ?", $number, 'i');
293
294 1
        return $this;
295
    }
296
297
    /**
298
     * Request that a column equals one of some strings
299
     *
300
     * @param  string[] $strings The list of accepted values for the column
301
     * @return self
302
     */
303 3
    public function isOneOf($strings)
304
    {
305 3
        $count = count($strings);
306 3
        $types = str_repeat('s', $count);
307 3
        $questionMarks = str_repeat(',?', $count);
308
309
        // Remove first comma from questionMarks so that MySQL can read our query
310 3
        $questionMarks = ltrim($questionMarks, ',');
311
312 3
        $this->addColumnCondition("IN ($questionMarks)", $strings, $types);
313
314 3
        return $this;
315
    }
316
317
    /**
318
     * Request that a column value starts with a string (case-insensitive)
319
     *
320
     * @param  string $string The substring that the column's value should start with
321
     * @return self
322
     */
323
    public function startsWith($string)
324
    {
325
        $this->addColumnCondition("LIKE CONCAT(?, '%')", $string, 's');
326
327
        return $this;
328
    }
329
330
    /**
331
     * Request that a specific model is not returned
332
     *
333
     * @param  Model|int $model The ID or model you don't want to get
334
     * @return self
335
     */
336 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...
337
    {
338
        if ($model instanceof Model) {
339
            $model = $model->getId();
340
        }
341
342
        $this->where('id');
343
        $this->addColumnCondition("!= ?", $model, 'i');
344
345
        return $this;
346
    }
347
348
    /**
349
     * Return the results sorted by the value of a column
350
     *
351
     * @param  string $column The column based on which the results should be ordered
352
     * @return self
353
     */
354 2
    public function sortBy($column)
355
    {
356 2
        if (!isset($this->columns[$column])) {
357
            throw new Exception("Unknown column");
358
        }
359
360 2
        $this->sortBy = $this->columns[$column];
361
362 2
        return $this;
363
    }
364
365
    /**
366
     * Reverse the order
367
     *
368
     * Note: This only works if you have specified a column in the sortBy() method
369
     *
370
     * @return self
371
     */
372 2
    public function reverse()
373
    {
374 2
        $this->reverseSort = !$this->reverseSort;
375
376 2
        return $this;
377
    }
378
379
    /**
380
     * Specify the number of results per page
381
     *
382
     * @param  int  $count The number of results
383
     * @return self
384
     */
385 2
    public function limit($count)
386
    {
387 2
        $this->resultsPerPage = $count;
388
389 2
        return $this;
390
    }
391
392
    /**
393
     * Only show results from a specific page
394
     *
395
     * @param  int|null $page The page number (or null to show all pages - counting starts from 0)
396
     * @return self
397
     */
398 2
    public function fromPage($page)
399
    {
400 2
        $this->page = $page;
401
402 2
        return $this;
403
    }
404
405
    /**
406
     * End with a specific result
407
     *
408
     * @param  int|Model $model     The model (or database ID) after the first result
409
     * @param  bool   $inclusive Whether to include the provided model
410
     * @param  bool   $reverse   Whether to reverse the results
411
     * @return self
412
     */
413 1
    public function endAt($model, $inclusive = false, $reverse = false)
414
    {
415 1
        return $this->startAt($model, $inclusive, !$reverse);
416
    }
417
418
    /**
419
     * Start with a specific result
420
     *
421
     * @param  int|Model $model     The model (or database ID) before the first result
422
     * @param  bool   $inclusive Whether to include the provided model
423
     * @param  bool   $reverse   Whether to reverse the results
424
     * @return self
425
     */
426 1
    public function startAt($model, $inclusive = false, $reverse = false)
427
    {
428 1
        if (!$model) {
429 1
            return $this;
430
        } elseif ($model instanceof Model && !$model->isValid()) {
431
            return $this;
432
        }
433
434
        $this->column($this->sortBy);
435
        $this->limited = true;
436
        $column = $this->currentColumn;
437
        $table  = $this->getTable();
438
439
        $comparison  = $this->reverseSort ^ $reverse;
440
        $comparison  = ($comparison) ? '>' : '<';
441
        $comparison .= ($inclusive)  ? '=' : '';
442
        $id = ($model instanceof Model) ? $model->getId() : $model;
443
444
        // Compare an element's timestamp to the timestamp of $model; if it's the
445
        // same, perform the comparison using IDs
446
        $this->addColumnCondition(
447
            "$comparison (SELECT $column FROM $table WHERE id = ?) OR ($column = (SELECT $column FROM $table WHERE id = ?) AND id $comparison ?)",
448
            array($id, $id, $id),
449
            'iii'
450
        );
451
452
        return $this;
453
    }
454
455
    /**
456
     * Request that only "active" Models should be returned
457
     *
458
     * @return self
459
     */
460 3
    public function active()
461
    {
462 3
        if (!isset($this->columns['status'])) {
463
            return $this;
464
        }
465
466 3
        $type = $this->type;
467
468 3
        return $this->where('status')->isOneOf($type::getActiveStatuses());
469
    }
470
471
    /**
472
     * Make sure that Models invisible to a player are not returned
473
     *
474
     * Note that this method does not take PermissionModel::canBeSeenBy() into
475
     * consideration for performance purposes, so you will have to override this
476
     * in your query builder if necessary.
477
     *
478
     * @param  Player  $player      The player in question
479
     * @param  bool $showDeleted false to hide deleted models even from admins
480
     * @return self
481
     */
482 1
    public function visibleTo($player, $showDeleted = false)
483
    {
484 1
        $type = $this->type;
485
486 1
        if (is_subclass_of($type, "PermissionModel")
487 1
         && $player->hasPermission($type::EDIT_PERMISSION)) {
488
            // The player is an admin who can see hidden models
489 1
            if (!$showDeleted) {
490 1
                if (isset($this->columns['status'])) {
491 1
                    return $this->where('status')->notEquals('deleted');
492
                }
493
            }
494
        } else {
495 1
            return $this->active();
496
        }
497
498
        return $this;
499
    }
500
501
    /**
502
     * Perform the query and get back the results in an array of names
503
     *
504
     * @return string[] An array of the type $id => $name
505
     */
506 1
    public function getNames()
507
    {
508 1
        if (!$this->nameColumn) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->nameColumn of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
509
            throw new Exception("You haven't specified a name column");
510
        }
511
512 1
        $results = $this->getArray($this->nameColumn);
513
514 1
        return array_column($results, $this->nameColumn, 'id');
515
    }
516
517
    /**
518
     * Perform the query and get back the results in a list of arrays
519
     *
520
     * @todo   Play with partial models?
521
     * @param  string|string[] $columns The column(s) that should be returned
522
     * @return array[]
523
     */
524 1
    public function getArray($columns)
525
    {
526 1
        if (!is_array($columns)) {
527 1
            $columns = array($columns);
528
        }
529
530 1
        $db = Database::getInstance();
531
532 1
        return $db->query($this->createQuery($columns), $this->getTypes(), $this->getParameters());
533
    }
534
535
    /**
536
     * An alias for QueryBuilder::getModels(), with fast fetching on by default
537
     * and no return of results
538
     *
539
     * @param  bool $fastFetch Whether to perform one query to load all
540
     *                            the model data instead of fetching them
541
     *                            one by one
542
     * @return void
543
     */
544
    public function addToCache($fastFetch = true)
545
    {
546
        $this->getModels($fastFetch);
547
    }
548
549
    /**
550
     * Perform the query and get the results as Models
551
     *
552
     * @todo Fix fast fetch for queries with multiple tables
553
     * @param  bool $fastFetch Whether to perform one query to load all
554
     *                            the model data instead of fetching them
555
     *                            one by one (ignores cache)
556
     * @return array
557
     */
558 4
    public function getModels($fastFetch = false)
559
    {
560 4
        $db   = Database::getInstance();
561 4
        $type = $this->type;
562
563 4
        $columns = ($fastFetch) ? $type::getEagerColumns() : array();
564
565 4
        $results = $db->query($this->createQuery($columns), $this->getTypes(), $this->getParameters());
566
567 4
        if ($fastFetch) {
568 2
            return $type::createFromDatabaseResults($results);
569
        } else {
570 3
            return $type::arrayIdToModel(array_column($results, 'id'));
571
        }
572
    }
573
574
    /**
575
     * Count the results
576
     *
577
     * @return int
578
     */
579 1
    public function count()
580
    {
581 1
        $table  = $this->getTable();
582 1
        $params = $this->createQueryParams(false);
583 1
        $db     = Database::getInstance();
584 1
        $query  = "SELECT COUNT(*) FROM $table $params";
585
586
        // We don't want pagination to affect our results so don't use the functions that combine
587
        // pagination results
588 1
        $results = $db->query($query, $this->types, $this->parameters);
589
590 1
        return $results[0]['COUNT(*)'];
591
    }
592
593
    /**
594
     * Count the number of pages that all the models could be separated into
595
     */
596 1
    public function countPages()
597
    {
598 1
        return ceil($this->count() / $this->getResultsPerPage());
599
    }
600
601
    /**
602
     * Find if there is any result
603
     *
604
     * @return bool
605
     */
606 1
    public function any()
607
    {
608
        // Make sure that we don't mess with the user's options
609 1
        $query = clone $this;
610
611 1
        $query->limit(1);
612
613 1
        return $query->count() > 0;
614
    }
615
616
    /**
617
     * Get the amount of results that are returned per page
618
     * @return int
619
     */
620 1
    public function getResultsPerPage()
621
    {
622 1
        return $this->resultsPerPage;
623
    }
624
625
    /**
626
     * Select a column to perform opeations on
627
     *
628
     * This is identical to the `where()` method, except that the column is
629
     * specified as a MySQL column and not as a column name given by the model
630
     *
631
     * @param  string $column The column to select
632
     * @return self
633
     */
634 4
    protected function column($column)
635
    {
636 4
        if (strpos($column, '.') === false) {
637
            // Add the table name to the column if it isn't there already so that
638
            // MySQL knows what to do when handling multiple tables
639 4
            $table = $this->getTable();
640 4
            $this->currentColumn = "`$table`.`$column`";
641
        } else {
642 1
            $this->currentColumn = $column;
643
        }
644
645 4
        $this->currentColumnRaw = $column;
646
647 4
        return $this;
648
    }
649
650
    /**
651
     * Add a condition for the column
652
     * @param  string $condition The MySQL condition
653
     * @param  mixed  $value     Value(s) to pass to MySQL
654
     * @param  string $type      The type of the values
655
     * @return void
656
     */
657 4
    protected function addColumnCondition($condition, $value, $type)
658
    {
659 4
        if (!$this->currentColumn) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->currentColumn of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
660
            throw new Exception("You haven't selected a column!");
661
        }
662
663 4
        if (!is_array($value)) {
664 2
            $value = array($value);
665
        }
666
667 4
        $this->conditions[] = "{$this->currentColumn} $condition";
668 4
        $this->parameters   = array_merge($this->parameters, $value);
669 4
        $this->types       .= $type;
670
671 4
        $this->currentColumn = null;
672 4
        $this->currentColumnRaw = null;
673 4
    }
674
675
    /**
676
     * Get the MySQL extra parameters
677
     *
678
     * @param  bool $respectPagination Whether to respect pagination or not; useful for when pagination should be ignored such as count
679
     * @return string
680
     */
681 4
    protected function createQueryParams($respectPagination = true)
682
    {
683 4
        $extras     = $this->extras;
684 4
        $conditions = $this->createQueryConditions();
685 4
        $groupQuery = $this->groupQuery;
686 4
        $order      = $this->createQueryOrder();
687 4
        $pagination = "";
688
689 4
        if ($respectPagination) {
690 4
            $pagination = $this->createQueryPagination();
691
        }
692
693 4
        return "$extras $conditions $groupQuery $order $pagination";
694
    }
695
696
    /**
697
     * Get the query parameters
698
     *
699
     * @return array
700
     */
701 4
    protected function getParameters()
702
    {
703 4
        return array_merge($this->parameters, $this->paginationParameters);
704
    }
705
706
    /**
707
     * Get the query types
708
     *
709
     * @return string
710
     */
711 4
    protected function getTypes()
712
    {
713 4
        return $this->types . $this->paginationTypes;
714
    }
715
716
    /**
717
     * Get the table of the model
718
     *
719
     * @return string
720
     */
721 4
    protected function getTable()
722
    {
723 4
        $type = $this->type;
724
725 4
        return $type::TABLE;
726
    }
727
728
    /**
729
     * Get a MySQL query string in the requested format
730
     * @param  string|string[] $columns The columns that should be included
731
     *                                  (without the ID, if an array is provided)
732
     * @return string The query
733
     */
734 4
    protected function createQuery($columns = array())
735
    {
736 4
        $type     = $this->type;
737 4
        $table    = $type::TABLE;
738 4
        $params   = $this->createQueryParams();
739
740 4
        if (is_array($columns)) {
741 3
            $columns = $this->createQueryColumns($columns);
742
        } elseif (empty($columns)) {
743
            $columns = $this->createQueryColumns();
744
        }
745
746 4
        return "SELECT $columns FROM $table $params";
747
    }
748
749
    /**
750
     * Generate the columns for the query
751
     * @param  string[] $columns The columns that should be included (without the ID)
752
     * @return string
753
     */
754 3
    private function createQueryColumns($columns = array())
755
    {
756 3
        $type = $this->type;
757 3
        $table = $type::TABLE;
758 3
        $columnStrings = array("`$table`.id");
759
760 3
        foreach ($columns as $returnName) {
761 1
            if (strpos($returnName, ' ') === false) {
762 1
                $dbName = $this->columns[$returnName];
763 1
                $columnStrings[] = "`$table`.`$dbName` as `$returnName`";
764
            } else {
765
                // "Column" contains a space, pass it as is
766 1
                $columnStrings[] = $returnName;
767
            }
768
        }
769
770 3
        return implode(',', $columnStrings);
771
    }
772
773
    /**
774
     * Generates all the WHERE conditions for the query
775
     * @return string
776
     */
777 4
    private function createQueryConditions()
778
    {
779 4
        if ($this->conditions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->conditions of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
780
            // Add parentheses around the conditions to prevent conflicts due
781
            // to the order of operations
782
            $conditions = array_map(function ($value) { return "($value)"; }, $this->conditions);
783
784 4
            return 'WHERE ' . implode(' AND ', $conditions);
785
        }
786
787
        return '';
788
    }
789
790
    /**
791
     * Generates the sorting instructions for the query
792
     * @return string
793
     */
794 4
    private function createQueryOrder()
795
    {
796 4
        if ($this->sortBy) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->sortBy of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
797 2
            $order = 'ORDER BY ' . $this->sortBy;
798
799
            // Sort by ID if the sorting columns are equal
800 2
            $id = '`' . $this->getTable() . '`.`id`';
801 2
            if ($this->reverseSort) {
802 2
                $order .= " DESC, $id DESC";
803
            } else {
804 2
                $order .= ", $id";
805
            }
806
        } else {
807 3
            $order = '';
808
        }
809
810 4
        return $order;
811
    }
812
813
    /**
814
     * Generates the pagination instructions for the query
815
     * @return string
816
     */
817 4
    private function createQueryPagination()
818
    {
819
        // Reset mysqli params and types just in case createQueryParagination()
820
        // had been called earlier
821 4
        $this->paginationParameters = array();
822 4
        $this->paginationTypes = "";
823
824 4
        if (!$this->page && !$this->limited) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->page of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
825 3
            return '';
826
        }
827
828 2
        $offset = '';
829 2
        if ($this->page) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->page of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
830 2
            $firstElement = ($this->page - 1) * $this->resultsPerPage;
831 2
            $this->paginationParameters[] = $firstElement;
832 2
            $this->paginationTypes       .= 'i';
833
834 2
            $offset = '?,';
835
        }
836
837 2
        $this->paginationParameters[] = $this->resultsPerPage;
838 2
        $this->paginationTypes       .= 'i';
839
840 2
        return "LIMIT $offset ?";
841
    }
842
}
843