QueryBuilder   D
last analyzed

Complexity

Total Complexity 93

Size/Duplication

Total Lines 922
Duplicated Lines 2.71 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 77.45%

Importance

Changes 0
Metric Value
wmc 93
lcom 1
cbo 4
dl 25
loc 922
ccs 182
cts 235
cp 0.7745
rs 4.4444
c 0
b 0
f 0

45 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 3
A selectColumn() 0 10 2
A where() 0 4 1
A having() 0 4 1
A equals() 0 6 1
A notEquals() 0 6 1
A isNotNull() 0 6 1
A isNull() 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 isLike() 0 10 2
A isOneOf() 0 12 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 14 3
A endAt() 0 4 1
C startAt() 0 27 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
B getModels() 0 21 5
A count() 0 13 1
A countPages() 0 4 1
A any() 0 9 1
A getResultsPerPage() 0 4 1
A column() 0 16 3
B addColumnCondition() 0 20 5
A createQueryParams() 0 15 2
A getParameters() 0 4 1
A getFromAlias() 0 4 1
A getTable() 0 6 1
A createQuery() 0 14 3
A createQueryColumns() 0 18 3
A createQueryConditions() 0 14 2
A createQueryOrder() 0 18 3
B createQueryPagination() 0 22 4
A grabColumn() 0 10 2

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