Passed
Pull Request — master (#235)
by Wilmer
15:32 queued 13:12
created

Query::typecastToIntegerOrString()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 3
nop 1
dl 0
loc 9
ccs 6
cts 6
cp 1
crap 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Query;
6
7
use Closure;
8
use Iterator;
9
use Throwable;
10
use Yiisoft\Db\Command\Command;
11
use Yiisoft\Db\Connection\ConnectionInterface;
12
use Yiisoft\Db\Exception\Exception;
13
use Yiisoft\Db\Exception\InvalidArgumentException;
14
use Yiisoft\Db\Exception\InvalidConfigException;
15
use Yiisoft\Db\Exception\NotSupportedException;
16
use Yiisoft\Db\Expression\Expression;
17
use Yiisoft\Db\Expression\ExpressionInterface;
18
use Yiisoft\Arrays\ArrayHelper;
19
use Yiisoft\Cache\Dependency\Dependency;
20
21
use function array_merge;
22
use function count;
23
use function gettype;
24
use function is_array;
25
use function is_int;
26
use function is_string;
27
use function key;
28
use function preg_match;
29
use function preg_split;
30
use function reset;
31
use function str_replace;
32
use function strcasecmp;
33
use function strlen;
34
use function strpos;
35
use function substr;
36
use function trim;
37
38
/**
39
 * Query represents a SELECT SQL statement in a way that is independent of DBMS.
40
 *
41
 * Query provides a set of methods to facilitate the specification of different clauses in a SELECT statement. These
42
 * methods can be chained together.
43
 *
44
 * By calling {@see createCommand()}, we can get a {@see Command} instance which can be further used to perform/execute
45
 * the DB query against a database.
46
 *
47
 * For example,
48
 *
49
 * ```php
50
 * $query = new Query;
51
 * // compose the query
52
 * $query->select('id, name')
53
 *     ->from('user')
54
 *     ->limit(10);
55
 * // build and execute the query
56
 * $rows = $query->all();
57
 * // alternatively, you can create DB command and execute it
58
 * $command = $query->createCommand();
59
 * // $command->sql returns the actual SQL
60
 * $rows = $command->queryAll();
61
 * ```
62
 *
63
 * Query internally uses the {@see QueryBuilder} class to generate the SQL statement.
64
 *
65
 * A more detailed usage guide on how to work with Query can be found in the
66
 * [guide article on Query Builder](guide:db-query-builder).
67
 *
68
 * @property string[] $tablesUsedInFrom Table names indexed by aliases. This property is read-only.
69
 */
70
class Query implements QueryInterface, ExpressionInterface
71
{
72
    use QueryTrait;
73
74
    protected array $select = [];
75
    protected ?string $selectOption = null;
76
    protected ?bool $distinct = null;
77
    protected $from;
78
    protected array $groupBy = [];
79
    protected array $join = [];
80
    protected $having;
81
    protected array $union = [];
82
    protected array $withQueries = [];
83
    protected array $params = [];
84
    private ?Dependency $queryCacheDependency = null;
85
    private ConnectionInterface $db;
86
    /** @var $queryCacheDuration bool|int|null */
0 ignored issues
show
Documentation Bug introduced by
The doc comment $queryCacheDuration at position 0 could not be parsed: Unknown type name '$queryCacheDuration' at position 0 in $queryCacheDuration.
Loading history...
87
    private $queryCacheDuration;
88
89 1883
    public function __construct(ConnectionInterface $db)
90
    {
91 1883
        $this->db = $db;
92 1883
    }
93
94
    /**
95
     * Creates a DB command that can be used to execute this query.
96
     *
97
     * If this parameter is not given, the `db` application component will be used.
98
     *
99
     * @throws Exception|InvalidConfigException
100
     *
101
     * @return Command the created DB command instance.
102
     */
103 120
    public function createCommand(): Command
104
    {
105 120
        [$sql, $params] = $this->db->getQueryBuilder()->build($this);
0 ignored issues
show
Bug introduced by
The method getQueryBuilder() does not exist on Yiisoft\Db\Connection\ConnectionInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Yiisoft\Db\Connection\ConnectionInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

105
        [$sql, $params] = $this->db->/** @scrutinizer ignore-call */ getQueryBuilder()->build($this);
Loading history...
106
107 120
        $command = $this->db->createCommand($sql, $params);
108
109 120
        $this->setCommandCache($command);
110
111 120
        return $command;
112
    }
113
114
    /**
115
     * Prepares for building SQL.
116
     *
117
     * This method is called by {@see QueryBuilder} when it starts to build SQL from a query object. You may override
118
     * this method to do some final preparation work when converting a query into a SQL statement.
119
     *
120
     * @param QueryBuilder $builder
121
     *
122
     * @return $this a prepared query instance which will be used by {@see QueryBuilder} to build the SQL.
123
     */
124 908
    public function prepare(QueryBuilder $builder): self
0 ignored issues
show
Unused Code introduced by
The parameter $builder is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

124
    public function prepare(/** @scrutinizer ignore-unused */ QueryBuilder $builder): self

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
125
    {
126 908
        return $this;
127
    }
128
129
    /**
130
     * Starts a batch query.
131
     *
132
     * A batch query supports fetching data in batches, which can keep the memory usage under a limit.
133
     *
134
     * This method will return a {@see BatchQueryResult} object which implements the {@see Iterator} interface and can
135
     * be traversed to retrieve the data in batches.
136
     *
137
     * For example,
138
     *
139
     * ```php
140
     * $query = (new Query)->from('user');
141
     * foreach ($query->batch() as $rows) {
142
     *     // $rows is an array of 100 or fewer rows from user table
143
     * }
144
     * ```
145
     *
146
     * @param int $batchSize the number of records to be fetched in each batch.
147
     *
148
     * @return BatchQueryResult the batch query result. It implements the {@see Iterator} interface and can be
149
     * traversed to retrieve the data in batches.
150
     */
151 30
    public function batch(int $batchSize = 100): BatchQueryResult
152
    {
153 30
        return (new BatchQueryResult())
154 30
            ->query($this)
155 30
            ->batchSize($batchSize)
156 30
            ->db($this->db)
157 30
            ->each(false);
158
    }
159
160
    /**
161
     * Starts a batch query and retrieves data row by row.
162
     *
163
     * This method is similar to {@see batch()} except that in each iteration of the result, only one row of data is
164
     * returned. For example,
165
     *
166
     * ```php
167
     * $query = (new Query)->from('user');
168
     * foreach ($query->each() as $row) {
169
     * }
170
     * ```
171
     *
172
     * @param int $batchSize the number of records to be fetched in each batch.
173
     *
174
     * @return BatchQueryResult the batch query result. It implements the {@see Iterator} interface and can be
175
     * traversed to retrieve the data in batches.
176
     */
177 10
    public function each(int $batchSize = 100): BatchQueryResult
178
    {
179 10
        return (new BatchQueryResult())
180 10
            ->query($this)
181 10
            ->batchSize($batchSize)
182 10
            ->db($this->db)
183 10
            ->each(true);
184
    }
185
186
    /**
187
     * Executes the query and returns all results as an array.
188
     *
189
     * If this parameter is not given, the `db` application component will be used.
190
     *
191
     * @throws Exception|InvalidConfigException|Throwable
192
     *
193
     * @return array the query results. If the query results in nothing, an empty array will be returned.
194
     */
195 311
    public function all(): array
196
    {
197 311
        if ($this->emulateExecution) {
198 15
            return [];
199
        }
200
201 301
        $rows = $this->createCommand()->queryAll();
202
203 301
        return $this->populate($rows);
204
    }
205
206
    /**
207
     * Converts the raw query results into the format as specified by this query.
208
     *
209
     * This method is internally used to convert the data fetched from database into the format as required by this
210
     * query.
211
     *
212
     * @param array $rows the raw query result from database.
213
     *
214
     * @return array the converted query result.
215
     */
216 520
    public function populate(array $rows): array
217
    {
218 520
        if ($this->indexBy === null) {
219 500
            return $rows;
220
        }
221
222 55
        $result = [];
223
224 55
        foreach ($rows as $row) {
225 55
            $result[ArrayHelper::getValueByPath($row, $this->indexBy)] = $row;
226
        }
227
228 55
        return $result;
229
    }
230
231
    /**
232
     * Executes the query and returns a single row of result.
233
     *
234
     * If this parameter is not given, the `db` application component will be used.
235
     *
236
     * @throws Exception|InvalidConfigException|Throwable
237
     *
238
     * @return array|bool the first row (in terms of an array) of the query result. False is returned if the query
239
     * results in nothing.
240
     */
241 432
    public function one()
242
    {
243 432
        if ($this->emulateExecution) {
244 10
            return false;
245
        }
246
247 422
        return $this->createCommand()->queryOne();
248
    }
249
250
    /**
251
     * Returns the query result as a scalar value.
252
     *
253
     * The value returned will be the first column in the first row of the query results.
254
     *
255
     * @throws Exception|InvalidConfigException|Throwable
256
     *
257
     * @return false|string|null the value of the first column in the first row of the query result. False is returned
258
     * if the query result is empty.
259
     */
260 25
    public function scalar()
261
    {
262 25
        if ($this->emulateExecution) {
263 10
            return null;
264
        }
265
266 15
        return $this->createCommand()->queryScalar();
267
    }
268
269
    /**
270
     * Executes the query and returns the first column of the result.
271
     *
272
     * If this parameter is not given, the `db` application component will be used.
273
     *
274
     * @throws Exception|InvalidConfigException|Throwable
275
     *
276
     * @return array the first column of the query result. An empty array is returned if the query results in nothing.
277
     */
278 25
    public function column(): array
279
    {
280 25
        if ($this->emulateExecution) {
281 10
            return [];
282
        }
283
284 15
        if ($this->indexBy === null) {
285 15
            return $this->createCommand()->queryColumn();
286
        }
287
288 5
        if (is_string($this->indexBy) && is_array($this->select) && count($this->select) === 1) {
289 5
            if (strpos($this->indexBy, '.') === false && count($tables = $this->getTablesUsedInFrom()) > 0) {
290 5
                $this->select[] = key($tables) . '.' . $this->indexBy;
291
            } else {
292
                $this->select[] = $this->indexBy;
293
            }
294
        }
295
296 5
        $rows = $this->createCommand()->queryAll();
297 5
        $results = [];
298 5
        foreach ($rows as $row) {
299 5
            $value = reset($row);
300
301 5
            if ($this->indexBy instanceof Closure) {
302 5
                $results[($this->indexBy)($row)] = $value;
303
            } else {
304 5
                $results[$row[$this->indexBy]] = $value;
305
            }
306
        }
307
308 5
        return $results;
309
    }
310
311
    /**
312
     * Returns the number of records.
313
     *
314
     * @param string $q the COUNT expression. Defaults to '*'.
315
     * Make sure you properly [quote](guide:db-dao#quoting-table-and-column-names) column names in the expression.
316
     *
317
     * @throws Exception|InvalidConfigException|Throwable
318
     *
319
     * @return int|string The number of records. The result may be a string depending on to support integer values
320
     * higher than a 32bit PHP integer can handle.
321
     */
322 95
    public function count(string $q = '*')
323
    {
324 95
        if ($this->emulateExecution) {
325 10
            return 0;
326
        }
327
328 95
        return $this->typecastToIntegerOrString(
329 95
            $this->queryScalar("COUNT($q)")
330
        );
331
    }
332
333
    /**
334
     * Returns the sum of the specified column values.
335
     *
336
     * @param string $q the column name or expression.
337
     * Make sure you properly [quote](guide:db-dao#quoting-table-and-column-names) column names in the expression.
338
     *
339
     * @throws Exception|InvalidConfigException|Throwable
340
     *
341
     * @return mixed the sum of the specified column values.
342
     */
343 15
    public function sum(string $q)
344
    {
345 15
        if ($this->emulateExecution) {
346 10
            return 0;
347
        }
348
349 5
        return $this->queryScalar("SUM($q)");
350
    }
351
352
    /**
353
     * Returns the average of the specified column values.
354
     *
355
     * @param string $q the column name or expression.
356
     * Make sure you properly [quote](guide:db-dao#quoting-table-and-column-names) column names in the expression.
357
     *
358
     * @throws Throwable
359
     *
360
     * @return mixed the average of the specified column values.
361
     */
362 15
    public function average(string $q)
363
    {
364 15
        if ($this->emulateExecution) {
365 10
            return 0;
366
        }
367
368 5
        return $this->queryScalar("AVG($q)");
369
    }
370
371
    /**
372
     * Returns the minimum of the specified column values.
373
     *
374
     * @param string $q the column name or expression.
375
     * Make sure you properly [quote](guide:db-dao#quoting-table-and-column-names) column names in the expression.
376
     *
377
     * @throws Exception|InvalidConfigException|Throwable
378
     *
379
     * @return mixed the minimum of the specified column values.
380
     */
381 15
    public function min(string $q)
382
    {
383 15
        return $this->queryScalar("MIN($q)");
384
    }
385
386
    /**
387
     * Returns the maximum of the specified column values.
388
     *
389
     * @param string $q the column name or expression.
390
     * Make sure you properly [quote](guide:db-dao#quoting-table-and-column-names) column names in the expression.
391
     *
392
     * @throws Exception|InvalidConfigException|Throwable
393
     *
394
     * @return mixed the maximum of the specified column values.
395
     */
396 15
    public function max(string $q)
397
    {
398 15
        return $this->queryScalar("MAX($q)");
399
    }
400
401
    /**
402
     * Returns a value indicating whether the query result contains any row of data.
403
     *
404
     * @throws Exception|InvalidConfigException|Throwable
405
     *
406
     * @return bool whether the query result contains any row of data.
407
     */
408 25
    public function exists(): bool
409
    {
410 25
        if ($this->emulateExecution) {
411 10
            return false;
412
        }
413
414 15
        $command = $this->createCommand();
415 15
        $params = $command->getParams();
416 15
        $command->setSql($this->db->getQueryBuilder()->selectExists($command->getSql()));
417 15
        $command->bindValues($params);
418
419 15
        return (bool) $command->queryScalar();
420
    }
421
422
    /**
423
     * Queries a scalar value by setting {@see select} first.
424
     *
425
     * Restores the value of select to make this query reusable.
426
     *
427
     * @param ExpressionInterface|string $selectExpression
428
     *
429
     * @throws Exception|InvalidConfigException|Throwable
430
     *
431
     * @return bool|string
432
     */
433 95
    protected function queryScalar($selectExpression)
434
    {
435 95
        if ($this->emulateExecution) {
436 10
            return null;
437
        }
438
439
        if (
440 95
            !$this->distinct
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->distinct of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
441 95
            && empty($this->groupBy)
442 95
            && empty($this->having)
443 95
            && empty($this->union)
444 95
            && empty($this->with)
445
        ) {
446 95
            $select = $this->select;
447 95
            $order = $this->orderBy;
448 95
            $limit = $this->limit;
449 95
            $offset = $this->offset;
450
451 95
            $this->select = [$selectExpression];
452 95
            $this->orderBy = [];
453 95
            $this->limit = null;
454 95
            $this->offset = null;
455
456
            try {
457 95
                $command = $this->createCommand();
458
            } catch (Throwable $e) {
459
                /** throw it later */
460
            }
461
462 95
            $this->select = $select;
463 95
            $this->orderBy = $order;
464 95
            $this->limit = $limit;
465 95
            $this->offset = $offset;
466
467 95
            if (isset($e)) {
468
                throw $e;
469
            }
470
471 95
            return $command->queryScalar();
472
        }
473
474 10
        $command = static::createInstance($this->db)
475 10
            ->select([$selectExpression])
476 10
            ->from(['c' => $this])
477 10
            ->createCommand();
478
479 10
        $this->setCommandCache($command);
480
481 10
        return $command->queryScalar();
482
    }
483
484
    /**
485
     * Returns table names used in {@see from} indexed by aliases.
486
     *
487
     * Both aliases and names are enclosed into {{ and }}.
488
     *
489
     * @throws InvalidArgumentException
490
     * @throws InvalidConfigException
491
     *
492
     * @return array table names indexed by aliases
493
     */
494 110
    public function getTablesUsedInFrom(): array
495
    {
496 110
        if (empty($this->from)) {
497
            return [];
498
        }
499
500 110
        if (is_array($this->from)) {
501 105
            $tableNames = $this->from;
502 5
        } elseif (is_string($this->from)) {
503
            $tableNames = preg_split('/\s*,\s*/', trim($this->from), -1, PREG_SPLIT_NO_EMPTY);
504 5
        } elseif ($this->from instanceof Expression) {
505
            $tableNames = [$this->from];
506
        } else {
507 5
            throw new InvalidConfigException(gettype($this->from) . ' in $from is not supported.');
508
        }
509
510 105
        return $this->cleanUpTableNames($tableNames);
511
    }
512
513
    /**
514
     * Clean up table names and aliases.
515
     *
516
     * Both aliases and names are enclosed into {{ and }}.
517
     *
518
     * @param array $tableNames non-empty array
519
     *
520
     * @throws InvalidArgumentException
521
     *
522
     * @return string[] table names indexed by aliases
523
     */
524 260
    protected function cleanUpTableNames(array $tableNames): array
525
    {
526 260
        $cleanedUpTableNames = [];
527 260
        foreach ($tableNames as $alias => $tableName) {
528 260
            if (is_string($tableName) && !is_string($alias)) {
529
                $pattern = <<<PATTERN
530 225
~
531
^
532
\s*
533
(
534
(?:['"`\[]|{{)
535
.*?
536
(?:['"`\]]|}})
537
|
538
\(.*?\)
539
|
540
.*?
541
)
542
(?:
543
(?:
544
    \s+
545
    (?:as)?
546
    \s*
547
)
548
(
549
   (?:['"`\[]|{{)
550
    .*?
551
    (?:['"`\]]|}})
552
    |
553
    .*?
554
)
555
)?
556
\s*
557
$
558
~iux
559
PATTERN;
560 225
                if (preg_match($pattern, $tableName, $matches)) {
561 225
                    if (isset($matches[2])) {
562 15
                        [, $tableName, $alias] = $matches;
563
                    } else {
564 220
                        $tableName = $alias = $matches[1];
565
                    }
566
                }
567
            }
568
569 260
            if ($tableName instanceof Expression) {
570 10
                if (!is_string($alias)) {
571 5
                    throw new InvalidArgumentException(
572 5
                        'To use Expression in from() method, pass it in array format with alias.'
573
                    );
574
                }
575 5
                $cleanedUpTableNames[$this->ensureNameQuoted($alias)] = $tableName;
576 250
            } elseif ($tableName instanceof self) {
577 5
                $cleanedUpTableNames[$this->ensureNameQuoted($alias)] = $tableName;
578
            } else {
579 245
                $cleanedUpTableNames[$this->ensureNameQuoted($alias)] = $this->ensureNameQuoted($tableName);
580
            }
581
        }
582
583 255
        return $cleanedUpTableNames;
584
    }
585
586
    /**
587
     * Ensures name is wrapped with {{ and }}.
588
     *
589
     * @param string $name
590
     *
591
     * @return string
592
     */
593 255
    private function ensureNameQuoted(string $name): string
594
    {
595 255
        $name = str_replace(["'", '"', '`', '[', ']'], '', $name);
596 255
        if ($name && !preg_match('/^{{.*}}$/', $name)) {
597 250
            return '{{' . $name . '}}';
598
        }
599
600 20
        return $name;
601
    }
602
603
    /**
604
     * Sets the SELECT part of the query.
605
     *
606
     * @param array|ExpressionInterface|string $columns the columns to be selected.
607
     * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. ['id', 'name']).
608
     * Columns can be prefixed with table names (e.g. "user.id") and/or contain column aliases
609
     * (e.g. "user.id AS user_id").
610
     *
611
     * The method will automatically quote the column names unless a column contains some parenthesis (which means the
612
     * column contains a DB expression). A DB expression may also be passed in form of an {@see ExpressionInterface}
613
     * object.
614
     *
615
     * Note that if you are selecting an expression like `CONCAT(first_name, ' ', last_name)`, you should use an array
616
     * to specify the columns. Otherwise, the expression may be incorrectly split into several parts.
617
     *
618
     * When the columns are specified as an array, you may also use array keys as the column aliases (if a column does
619
     * not need alias, do not use a string key).
620
     * @param string|null $option additional option that should be appended to the 'SELECT' keyword. For example,
621
     * in MySQL, the option 'SQL_CALC_FOUND_ROWS' can be used.
622
     *
623
     * @return $this the query object itself.
624
     */
625 789
    public function select($columns, ?string $option = null): self
626
    {
627 789
        $this->select = $this->normalizeSelect($columns);
628 789
        $this->selectOption = $option;
629
630 789
        return $this;
631
    }
632
633
    /**
634
     * Add more columns to the SELECT part of the query.
635
     *
636
     * Note, that if {@see select} has not been specified before, you should include `*` explicitly if you want to
637
     * select all remaining columns too:
638
     *
639
     * ```php
640
     * $query->addSelect(["*", "CONCAT(first_name, ' ', last_name) AS full_name"])->one();
641
     * ```
642
     *
643
     * @param array|ExpressionInterface|string $columns the columns to add to the select. See {@see select()} for more
644
     * details about the format of this parameter.
645
     *
646
     * @return $this the query object itself.
647
     *
648
     * {@see select()}
649
     */
650 15
    public function addSelect($columns): self
651
    {
652 15
        if ($this->select === null) {
653
            return $this->select($columns);
654
        }
655
656 15
        if (!is_array($this->select)) {
0 ignored issues
show
introduced by
The condition is_array($this->select) is always true.
Loading history...
657
            $this->select = $this->normalizeSelect($this->select);
658
        }
659
660 15
        $this->select = array_merge($this->select, $this->normalizeSelect($columns));
661
662 15
        return $this;
663
    }
664
665
    /**
666
     * Normalizes the SELECT columns passed to {@see select()} or {@see addSelect()}.
667
     *
668
     * @param array|ExpressionInterface|string $columns
669
     *
670
     * @return array
671
     */
672 789
    protected function normalizeSelect($columns): array
673
    {
674 789
        if ($columns instanceof ExpressionInterface) {
675 5
            $columns = [$columns];
676 789
        } elseif (!is_array($columns)) {
677 113
            $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
678
        }
679
680 789
        $select = [];
681 789
        foreach ($columns as $columnAlias => $columnDefinition) {
682 348
            if (is_string($columnAlias)) {
683
                /** Already in the normalized format, good for them */
684 182
                $select[$columnAlias] = $columnDefinition;
685 182
                continue;
686
            }
687 341
            if (is_string($columnDefinition)) {
688
                if (
689 336
                    preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-_.]+)$/', $columnDefinition, $matches) &&
690 336
                    !preg_match('/^\d+$/', $matches[2]) &&
691 336
                    strpos($matches[2], '.') === false
692
                ) {
693
                    /** Using "columnName as alias" or "columnName alias" syntax */
694 25
                    $select[$matches[2]] = $matches[1];
695 25
                    continue;
696
                }
697 336
                if (strpos($columnDefinition, '(') === false) {
698
                    /** Normal column name, just alias it to itself to ensure it's not selected twice */
699 280
                    $select[$columnDefinition] = $columnDefinition;
700 280
                    continue;
701
                }
702
            }
703
            /** Either a string calling a function, DB expression, or sub-query */
704 106
            $select[] = $columnDefinition;
705
        }
706
707 789
        return $select;
708
    }
709
710
    /**
711
     * Sets the value indicating whether to SELECT DISTINCT or not.
712
     *
713
     * @param bool $value whether to SELECT DISTINCT or not.
714
     *
715
     * @return $this the query object itself
716
     */
717 627
    public function distinct(?bool $value = true): self
718
    {
719 627
        $this->distinct = $value;
720
721 627
        return $this;
722
    }
723
724
    /**
725
     * Sets the FROM part of the query.
726
     *
727
     * @param array|ExpressionInterface|string $tables the table(s) to be selected from. This can be either a string
728
     * (e.g. `'user'`) or an array (e.g. `['user', 'profile']`) specifying one or several table names.
729
     *
730
     * Table names can contain schema prefixes (e.g. `'public.user'`) and/or table aliases (e.g. `'user u'`).
731
     *
732
     * The method will automatically quote the table names unless it contains some parenthesis (which means the table is
733
     * given as a sub-query or DB expression).
734
     *
735
     * When the tables are specified as an array, you may also use the array keys as the table aliases (if a table does
736
     * not need alias, do not use a string key).
737
     *
738
     * Use a Query object to represent a sub-query. In this case, the corresponding array key will be used as the alias
739
     * for the sub-query.
740
     *
741
     * To specify the `FROM` part in plain SQL, you may pass an instance of {@see ExpressionInterface}.
742
     *
743
     * Here are some examples:
744
     *
745
     * ```php
746
     * // SELECT * FROM  `user` `u`, `profile`;
747
     * $query = (new \Yiisoft\Db\Query\Query)->from(['u' => 'user', 'profile']);
748
     *
749
     * // SELECT * FROM (SELECT * FROM `user` WHERE `active` = 1) `activeusers`;
750
     * $subquery = (new \Yiisoft\Db\Query\Query)->from('user')->where(['active' => true])
751
     * $query = (new \Yiisoft\Db\Query\Query)->from(['activeusers' => $subquery]);
752
     *
753
     * // subquery can also be a string with plain SQL wrapped in parenthesis
754
     * // SELECT * FROM (SELECT * FROM `user` WHERE `active` = 1) `activeusers`;
755
     * $subquery = "(SELECT * FROM `user` WHERE `active` = 1)";
756
     * $query = (new \Yiisoft\Db\Query\Query)->from(['activeusers' => $subquery]);
757
     * ```
758
     *
759
     * @return $this the query object itself
760
     */
761 963
    public function from($tables): self
762
    {
763 963
        if ($tables instanceof ExpressionInterface) {
764 16
            $tables = [$tables];
765
        }
766 963
        if (is_string($tables)) {
767 256
            $tables = preg_split('/\s*,\s*/', trim($tables), -1, PREG_SPLIT_NO_EMPTY);
768
        }
769 963
        $this->from = $tables;
770
771 963
        return $this;
772
    }
773
774
    /**
775
     * Sets the WHERE part of the query.
776
     *
777
     * The method requires a `$condition` parameter, and optionally a `$params` parameter specifying the values to be
778
     * bound to the query.
779
     *
780
     * The `$condition` parameter should be either a string (e.g. `'id=1'`) or an array.
781
     *
782
     * {@inheritdoc}
783
     *
784
     * @param array|ExpressionInterface|string $condition the conditions that should be put in the WHERE part.
785
     * @param array $params the parameters (name => value) to be bound to the query.
786
     *
787
     * @return $this the query object itself.
788
     *
789
     * {@see andWhere()}
790
     * {@see orWhere()}
791
     * {@see QueryInterface::where()}
792
     */
793 1368
    public function where($condition, array $params = []): self
794
    {
795 1368
        $this->where = $condition;
0 ignored issues
show
Documentation Bug introduced by
It seems like $condition can also be of type Yiisoft\Db\Expression\ExpressionInterface. However, the property $where is declared as type array|null|string. 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...
796 1368
        $this->addParams($params);
797
798 1368
        return $this;
799
    }
800
801
    /**
802
     * Adds an additional WHERE condition to the existing one.
803
     *
804
     * The new condition and the existing one will be joined using the `AND` operator.
805
     *
806
     * @param array|ExpressionInterface|string $condition the new WHERE condition. Please refer to {@see where()} on how
807
     * to specify this parameter.
808
     * @param array $params the parameters (name => value) to be bound to the query.
809
     *
810
     * @return $this the query object itself.
811
     *
812
     * {@see where()}
813
     * {@see orWhere()}
814
     */
815 348
    public function andWhere($condition, array $params = []): self
816
    {
817 348
        if ($this->where === null) {
818 323
            $this->where = $condition;
0 ignored issues
show
Documentation Bug introduced by
It seems like $condition can also be of type Yiisoft\Db\Expression\ExpressionInterface. However, the property $where is declared as type array|null|string. 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...
819 131
        } elseif (is_array($this->where) && isset($this->where[0]) && strcasecmp($this->where[0], 'and') === 0) {
820 6
            $this->where[] = $condition;
821
        } else {
822 131
            $this->where = ['and', $this->where, $condition];
823
        }
824
825 348
        $this->addParams($params);
826
827 348
        return $this;
828
    }
829
830
    /**
831
     * Adds an additional WHERE condition to the existing one.
832
     *
833
     * The new condition and the existing one will be joined using the `OR` operator.
834
     *
835
     * @param array|ExpressionInterface|string $condition the new WHERE condition. Please refer to {@see where()} on how
836
     * to specify this parameter.
837
     * @param array $params the parameters (name => value) to be bound to the query.
838
     *
839
     * @return $this the query object itself.
840
     *
841
     * {@see where()}
842
     * {@see andWhere()}
843
     */
844 6
    public function orWhere($condition, array $params = []): self
845
    {
846 6
        if ($this->where === null) {
847
            $this->where = $condition;
0 ignored issues
show
Documentation Bug introduced by
It seems like $condition can also be of type Yiisoft\Db\Expression\ExpressionInterface. However, the property $where is declared as type array|null|string. 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...
848
        } else {
849 6
            $this->where = ['or', $this->where, $condition];
850
        }
851
852 6
        $this->addParams($params);
853
854 6
        return $this;
855
    }
856
857
    /**
858
     * Adds a filtering condition for a specific column and allow the user to choose a filter operator.
859
     *
860
     * It adds an additional WHERE condition for the given field and determines the comparison operator based on the
861
     * first few characters of the given value.
862
     *
863
     * The condition is added in the same way as in {@see andFilterWhere} so {@see isEmpty()|empty values} are ignored.
864
     * The new condition and the existing one will be joined using the `AND` operator.
865
     *
866
     * The comparison operator is intelligently determined based on the first few characters in the given value.
867
     * In particular, it recognizes the following operators if they appear as the leading characters in the given value:
868
     *
869
     * - `<`: the column must be less than the given value.
870
     * - `>`: the column must be greater than the given value.
871
     * - `<=`: the column must be less than or equal to the given value.
872
     * - `>=`: the column must be greater than or equal to the given value.
873
     * - `<>`: the column must not be the same as the given value.
874
     * - `=`: the column must be equal to the given value.
875
     * - If none of the above operators is detected, the `$defaultOperator` will be used.
876
     *
877
     * @param string $name the column name.
878
     * @param string|null $value the column value optionally prepended with the comparison operator.
879
     * @param string $defaultOperator The operator to use, when no operator is given in `$value`.
880
     * Defaults to `=`, performing an exact match.
881
     *
882
     * @throws NotSupportedException
883
     *
884
     * @return $this the query object itself.
885
     */
886 5
    public function andFilterCompare(string $name, ?string $value, string $defaultOperator = '='): self
887
    {
888 5
        if (preg_match('/^(<>|>=|>|<=|<|=)/', (string) $value, $matches)) {
889 5
            $operator = $matches[1];
890 5
            $value = substr($value, strlen($operator));
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type null; however, parameter $string of substr() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

890
            $value = substr(/** @scrutinizer ignore-type */ $value, strlen($operator));
Loading history...
891
        } else {
892 5
            $operator = $defaultOperator;
893
        }
894
895 5
        return $this->andFilterWhere([$operator, $name, $value]);
896
    }
897
898
    /**
899
     * Appends a JOIN part to the query.
900
     *
901
     * The first parameter specifies what type of join it is.
902
     *
903
     * @param string $type  the type of join, such as INNER JOIN, LEFT JOIN.
904
     * @param array|string $table the table to be joined.
905
     * Use a string to represent the name of the table to be joined.
906
     * The table name can contain a schema prefix (e.g. 'public.user') and/or table alias (e.g. 'user u').
907
     * The method will automatically quote the table name unless it contains some parenthesis (which means the table is
908
     * given as a sub-query or DB expression).
909
     * Use an array to represent joining with a sub-query. The array must contain only one element.
910
     * The value must be a {@see Query} object representing the sub-query while the corresponding key represents the
911
     * alias for the sub-query.
912
     * @param array|string $on the join condition that should appear in the ON part.
913
     * Please refer to {@see where()} on how to specify this parameter.
914
     *
915
     * Note that the array format of {@see where()} is designed to match columns to values instead of columns to
916
     * columns, so the following would **not** work as expected: `['post.author_id' => 'user.id']`, it would match the
917
     * `post.author_id` column value against the string `'user.id'`.
918
     *
919
     * It is recommended to use the string syntax here which is more suited for a join:
920
     *
921
     * ```php
922
     * 'post.author_id = user.id'
923
     * ```
924
     * @param array $params the parameters (name => value) to be bound to the query.
925
     *
926
     * @return $this the query object itself.
927
     */
928 110
    public function join(string $type, $table, $on = '', array $params = []): self
929
    {
930 110
        $this->join[] = [$type, $table, $on];
931
932 110
        return $this->addParams($params);
933
    }
934
935
    /**
936
     * Appends an INNER JOIN part to the query.
937
     *
938
     * @param array|string $table the table to be joined.
939
     * Use a string to represent the name of the table to be joined.
940
     * The table name can contain a schema prefix (e.g. 'public.user') and/or table alias (e.g. 'user u').
941
     * The method will automatically quote the table name unless it contains some parenthesis (which means the table is
942
     * given as a sub-query or DB expression).
943
     * Use an array to represent joining with a sub-query. The array must contain only one element.
944
     * The value must be a {@see Query} object representing the sub-query while the corresponding key represents the
945
     * alias for the sub-query.
946
     * @param array|string $on the join condition that should appear in the ON part.
947
     * Please refer to {@see join()} on how to specify this parameter.
948
     * @param array $params the parameters (name => value) to be bound to the query.
949
     *
950
     * @return $this the query object itself.
951
     */
952 5
    public function innerJoin($table, $on = '', array $params = []): self
953
    {
954 5
        $this->join[] = ['INNER JOIN', $table, $on];
955
956 5
        return $this->addParams($params);
957
    }
958
959
    /**
960
     * Appends a LEFT OUTER JOIN part to the query.
961
     *
962
     * @param array|string $table the table to be joined.
963
     * Use a string to represent the name of the table to be joined.
964
     * The table name can contain a schema prefix (e.g. 'public.user') and/or table alias (e.g. 'user u').
965
     * The method will automatically quote the table name unless it contains some parenthesis (which means the table is
966
     * given as a sub-query or DB expression).
967
     * Use an array to represent joining with a sub-query. The array must contain only one element.
968
     * The value must be a {@see Query} object representing the sub-query while the corresponding key represents the
969
     * alias for the sub-query.
970
     * @param array|string $on the join condition that should appear in the ON part.
971
     * Please refer to {@see join()} on how to specify this parameter.
972
     * @param array $params the parameters (name => value) to be bound to the query.
973
     *
974
     * @return $this the query object itself.
975
     */
976 5
    public function leftJoin($table, $on = '', array $params = []): self
977
    {
978 5
        $this->join[] = ['LEFT JOIN', $table, $on];
979
980 5
        return $this->addParams($params);
981
    }
982
983
    /**
984
     * Appends a RIGHT OUTER JOIN part to the query.
985
     *
986
     * @param array|string $table the table to be joined.
987
     * Use a string to represent the name of the table to be joined.
988
     * The table name can contain a schema prefix (e.g. 'public.user') and/or table alias (e.g. 'user u').
989
     * The method will automatically quote the table name unless it contains some parenthesis (which means the table is
990
     * given as a sub-query or DB expression).
991
     * Use an array to represent joining with a sub-query. The array must contain only one element.
992
     * The value must be a {@see Query} object representing the sub-query while the corresponding key represents the
993
     * alias for the sub-query.
994
     * @param array|string $on the join condition that should appear in the ON part.
995
     * Please refer to {@see join()} on how to specify this parameter.
996
     * @param array $params the parameters (name => value) to be bound to the query.
997
     *
998
     * @return $this the query object itself.
999
     */
1000
    public function rightJoin($table, $on = '', array $params = []): self
1001
    {
1002
        $this->join[] = ['RIGHT JOIN', $table, $on];
1003
1004
        return $this->addParams($params);
1005
    }
1006
1007
    /**
1008
     * Sets the GROUP BY part of the query.
1009
     *
1010
     * @param array|ExpressionInterface|string $columns the columns to be grouped by.
1011
     * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. ['id', 'name']).
1012
     * The method will automatically quote the column names unless a column contains some parenthesis (which means the
1013
     * column contains a DB expression).
1014
     *
1015
     * Note that if your group-by is an expression containing commas, you should always use an array to represent the
1016
     * group-by information. Otherwise, the method will not be able to correctly determine the group-by columns.
1017
     *
1018
     * {@see ExpressionInterface} object can be passed to specify the GROUP BY part explicitly in plain SQL.
1019
     * {@see ExpressionInterface} object can be passed as well.
1020
     *
1021
     * @return $this the query object itself.
1022
     *
1023
     * {@see addGroupBy()}
1024
     */
1025 637
    public function groupBy($columns): self
1026
    {
1027 637
        if ($columns instanceof ExpressionInterface) {
1028 5
            $columns = [$columns];
1029 637
        } elseif (!is_array($columns)) {
1030 25
            $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
1031
        }
1032 637
        $this->groupBy = $columns;
1033
1034 637
        return $this;
1035
    }
1036
1037
    /**
1038
     * Adds additional group-by columns to the existing ones.
1039
     *
1040
     * @param array|string $columns additional columns to be grouped by.
1041
     * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. ['id', 'name']).
1042
     * The method will automatically quote the column names unless a column contains some parenthesis (which means the
1043
     * column contains a DB expression).
1044
     *
1045
     * Note that if your group-by is an expression containing commas, you should always use an array to represent the
1046
     * group-by information. Otherwise, the method will not be able to correctly determine the group-by columns.
1047
     *
1048
     * {@see Expression} object can be passed to specify the GROUP BY part explicitly in plain SQL.
1049
     * {@see ExpressionInterface} object can be passed as well.
1050
     *
1051
     * @return $this the query object itself
1052
     *
1053
     * {@see groupBy()}
1054
     */
1055 5
    public function addGroupBy($columns): self
1056
    {
1057 5
        if ($columns instanceof ExpressionInterface) {
0 ignored issues
show
introduced by
$columns is never a sub-type of Yiisoft\Db\Expression\ExpressionInterface.
Loading history...
1058
            $columns = [$columns];
1059 5
        } elseif (!is_array($columns)) {
1060 5
            $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
1061
        }
1062 5
        if ($this->groupBy === null) {
1063
            $this->groupBy = $columns;
1064
        } else {
1065 5
            $this->groupBy = array_merge($this->groupBy, $columns);
1066
        }
1067
1068 5
        return $this;
1069
    }
1070
1071
    /**
1072
     * Sets the HAVING part of the query.
1073
     *
1074
     * @param array|ExpressionInterface|string $condition the conditions to be put after HAVING.
1075
     * Please refer to {@see where()} on how to specify this parameter.
1076
     * @param array $params the parameters (name => value) to be bound to the query.
1077
     *
1078
     * @return $this the query object itself.
1079
     *
1080
     * {@see andHaving()}
1081
     * {@see orHaving()}
1082
     */
1083 637
    public function having($condition, array $params = []): self
1084
    {
1085 637
        $this->having = $condition;
1086 637
        $this->addParams($params);
1087
1088 637
        return $this;
1089
    }
1090
1091
    /**
1092
     * Adds an additional HAVING condition to the existing one.
1093
     * The new condition and the existing one will be joined using the `AND` operator.
1094
     *
1095
     * @param array|ExpressionInterface|string $condition the new HAVING condition. Please refer to {@see where()}
1096
     * on how to specify this parameter.
1097
     * @param array $params the parameters (name => value) to be bound to the query.
1098
     *
1099
     * @return $this the query object itself.
1100
     *
1101
     * {@see having()}
1102
     * {@see orHaving()}
1103
     */
1104 5
    public function andHaving($condition, array $params = []): self
1105
    {
1106 5
        if ($this->having === null) {
1107
            $this->having = $condition;
1108
        } else {
1109 5
            $this->having = ['and', $this->having, $condition];
1110
        }
1111
1112 5
        $this->addParams($params);
1113
1114 5
        return $this;
1115
    }
1116
1117
    /**
1118
     * Adds an additional HAVING condition to the existing one.
1119
     *
1120
     * The new condition and the existing one will be joined using the `OR` operator.
1121
     *
1122
     * @param array|ExpressionInterface|string $condition the new HAVING condition. Please refer to {@see where()}
1123
     * on how to specify this parameter.
1124
     * @param array $params the parameters (name => value) to be bound to the query.
1125
     *
1126
     * @return $this the query object itself.
1127
     *
1128
     * {@see having()}
1129
     * {@see andHaving()}
1130
     */
1131 5
    public function orHaving($condition, $params = []): self
1132
    {
1133 5
        if ($this->having === null) {
1134
            $this->having = $condition;
1135
        } else {
1136 5
            $this->having = ['or', $this->having, $condition];
1137
        }
1138
1139 5
        $this->addParams($params);
1140
1141 5
        return $this;
1142
    }
1143
1144
    /**
1145
     * Sets the HAVING part of the query but ignores {@see isEmpty()|empty operands}.
1146
     *
1147
     * This method is similar to {@see having()}. The main difference is that this method will remove
1148
     * {@see isEmpty()|empty query operands}. As a result, this method is best suited for building query conditions
1149
     * based on filter values entered by users.
1150
     *
1151
     * The following code shows the difference between this method and {@see having()}:
1152
     *
1153
     * ```php
1154
     * // HAVING `age`=:age
1155
     * $query->filterHaving(['name' => null, 'age' => 20]);
1156
     * // HAVING `age`=:age
1157
     * $query->having(['age' => 20]);
1158
     * // HAVING `name` IS NULL AND `age`=:age
1159
     * $query->having(['name' => null, 'age' => 20]);
1160
     * ```
1161
     *
1162
     * Note that unlike {@see having()}, you cannot pass binding parameters to this method.
1163
     *
1164
     * @param array $condition the conditions that should be put in the HAVING part.
1165
     * See {@see having()} on how to specify this parameter.
1166
     *
1167
     * @throws NotSupportedException
1168
     *
1169
     * @return $this the query object itself.
1170
     *
1171
     * {@see having()}
1172
     * {@see andFilterHaving()}
1173
     * {@see orFilterHaving()}
1174
     */
1175 10
    public function filterHaving(array $condition): self
1176
    {
1177 10
        $condition = $this->filterCondition($condition);
1178
1179 10
        if ($condition !== []) {
1180 10
            $this->having($condition);
1181
        }
1182
1183 10
        return $this;
1184
    }
1185
1186
    /**
1187
     * Adds an additional HAVING condition to the existing one but ignores {@see isEmpty()|empty operands}.
1188
     *
1189
     * The new condition and the existing one will be joined using the `AND` operator.
1190
     *
1191
     * This method is similar to {@see andHaving()}. The main difference is that this method will remove
1192
     * {@see isEmpty()|empty query operands}. As a result, this method is best suited for building query conditions
1193
     * based on filter values entered by users.
1194
     *
1195
     * @param array $condition the new HAVING condition. Please refer to {@see having()} on how to specify this
1196
     * parameter.
1197
     *
1198
     * @throws NotSupportedException
1199
     *
1200
     * @return $this the query object itself.
1201
     *
1202
     * {@see filterHaving()}
1203
     * {@see orFilterHaving()}
1204
     */
1205 10
    public function andFilterHaving(array $condition): self
1206
    {
1207 10
        $condition = $this->filterCondition($condition);
1208
1209 10
        if ($condition !== []) {
1210
            $this->andHaving($condition);
1211
        }
1212
1213 10
        return $this;
1214
    }
1215
1216
    /**
1217
     * Adds an additional HAVING condition to the existing one but ignores {@see isEmpty()|empty operands}.
1218
     *
1219
     * The new condition and the existing one will be joined using the `OR` operator.
1220
     *
1221
     * This method is similar to {@see orHaving()}. The main difference is that this method will remove
1222
     * {@see isEmpty()|empty query operands}. As a result, this method is best suited for building query conditions
1223
     * based on filter values entered by users.
1224
     *
1225
     * @param array $condition the new HAVING condition. Please refer to {@see having()} on how to specify this
1226
     * parameter.
1227
     *
1228
     * @throws NotSupportedException
1229
     *
1230
     * @return $this the query object itself.
1231
     *
1232
     * {@see filterHaving()}
1233
     * {@see andFilterHaving()}
1234
     */
1235 10
    public function orFilterHaving(array $condition): self
1236
    {
1237 10
        $condition = $this->filterCondition($condition);
1238
1239 10
        if ($condition !== []) {
1240
            $this->orHaving($condition);
1241
        }
1242
1243 10
        return $this;
1244
    }
1245
1246
    /**
1247
     * Appends a SQL statement using UNION operator.
1248
     *
1249
     * @param Query|string $sql the SQL statement to be appended using UNION.
1250
     * @param bool $all TRUE if using UNION ALL and FALSE if using UNION.
1251
     *
1252
     * @return $this the query object itself.
1253
     */
1254 17
    public function union($sql, $all = false): self
1255
    {
1256 17
        $this->union[] = ['query' => $sql, 'all' => $all];
1257
1258 17
        return $this;
1259
    }
1260
1261
    /**
1262
     * Sets the parameters to be bound to the query.
1263
     *
1264
     * @param array $params list of query parameter values indexed by parameter placeholders.
1265
     * For example, `[':name' => 'Dan', ':age' => 31]`.
1266
     *
1267
     * @return $this the query object itself.
1268
     *
1269
     * {@see addParams()}
1270
     */
1271 640
    public function params(array $params): self
1272
    {
1273 640
        $this->params = $params;
1274
1275 640
        return $this;
1276
    }
1277
1278
    /**
1279
     * Adds additional parameters to be bound to the query.
1280
     *
1281
     * @param array $params list of query parameter values indexed by parameter placeholders.
1282
     * For example, `[':name' => 'Dan', ':age' => 31]`.
1283
     *
1284
     * @return $this the query object itself.
1285
     *
1286
     * {@see params()}
1287
     */
1288 1459
    public function addParams(array $params): self
1289
    {
1290 1459
        if (!empty($params)) {
1291 62
            if (empty($this->params)) {
1292 62
                $this->params = $params;
1293
            } else {
1294 11
                foreach ($params as $name => $value) {
1295 11
                    if (is_int($name)) {
1296
                        $this->params[] = $value;
1297
                    } else {
1298 11
                        $this->params[$name] = $value;
1299
                    }
1300
                }
1301
            }
1302
        }
1303
1304 1459
        return $this;
1305
    }
1306
1307
    /**
1308
     * Enables query cache for this Query.
1309
     *
1310
     * @param int|true $duration the number of seconds that query results can remain valid in cache.
1311
     * Use 0 to indicate that the cached data will never expire.
1312
     * Use a negative number to indicate that query cache should not be used.
1313
     * Use boolean `true` to indicate that {@see ConnectionInterface::queryCacheDuration} should be used.
1314
     * Defaults to `true`.
1315
     * @param Dependency|null $dependency the cache dependency associated with the cached result.
1316
     *
1317
     * @return $this the Query object itself.
1318
     */
1319 5
    public function cache($duration = true, ?Dependency $dependency = null): self
1320
    {
1321 5
        $this->queryCacheDuration = $duration;
1322 5
        $this->queryCacheDependency = $dependency;
1323
1324 5
        return $this;
1325
    }
1326
1327
    /**
1328
     * Disables query cache for this Query.
1329
     *
1330
     * @return $this the Query object itself.
1331
     */
1332 5
    public function noCache(): self
1333
    {
1334 5
        $this->queryCacheDuration = -1;
1335
1336 5
        return $this;
1337
    }
1338
1339
    /**
1340
     * Sets $command cache, if this query has enabled caching.
1341
     *
1342
     * @param Command $command
1343
     *
1344
     * @return Command
1345
     */
1346 672
    protected function setCommandCache(Command $command): Command
1347
    {
1348 672
        if ($this->queryCacheDuration !== null || $this->queryCacheDependency !== null) {
1349 5
            $duration = $this->queryCacheDuration === true ? null : $this->queryCacheDuration;
1350 5
            $command->cache($duration, $this->queryCacheDependency);
1351
        }
1352
1353 672
        return $command;
1354
    }
1355
1356
    /**
1357
     * Creates a new Query object and copies its property values from an existing one.
1358
     *
1359
     * The properties being copies are the ones to be used by query builders.
1360
     *
1361
     * @param ConnectionInterface $db the source query object.
1362
     * @param Query $from the source query object.
1363
     *
1364
     * @return $this the new Query object.
1365
     */
1366 622
    public static function create(ConnectionInterface $db, self $from): self
1367
    {
1368 622
        return (new self($db))
1369 622
            ->where($from->where)
1370 622
            ->limit($from->limit)
1371 622
            ->offset($from->offset)
1372 622
            ->orderBy($from->orderBy)
1373 622
            ->indexBy($from->indexBy)
1374 622
            ->select($from->select)
1375 622
            ->selectOption($from->selectOption)
1376 622
            ->distinct($from->distinct)
1377 622
            ->from($from->from)
1378 622
            ->groupBy($from->groupBy)
1379 622
            ->setJoin($from->join)
1380 622
            ->having($from->having)
1381 622
            ->setUnion($from->union)
1382 622
            ->params($from->params)
1383 622
            ->withQueries($from->withQueries);
1384
    }
1385
1386
    /**
1387
     * Returns the SQL representation of Query.
1388
     *
1389
     * @return string
1390
     */
1391
    public function __toString(): string
1392
    {
1393
        return serialize($this);
1394
    }
1395
1396 1525
    public function getDistinct(): ?bool
1397
    {
1398 1525
        return $this->distinct;
1399
    }
1400
1401 1561
    public function getFrom()
1402
    {
1403 1561
        return $this->from;
1404
    }
1405
1406 1530
    public function getGroupBy(): array
1407
    {
1408 1530
        return $this->groupBy;
1409
    }
1410
1411 1540
    public function getHaving()
1412
    {
1413 1540
        return $this->having;
1414
    }
1415
1416 1530
    public function getJoin(): array
1417
    {
1418 1530
        return $this->join;
1419
    }
1420
1421 1565
    public function getParams(): array
1422
    {
1423 1565
        return $this->params;
1424
    }
1425
1426 1545
    public function getSelect(): array
1427
    {
1428 1545
        return $this->select;
1429
    }
1430
1431 1525
    public function getSelectOption(): ?string
1432
    {
1433 1525
        return $this->selectOption;
1434
    }
1435
1436 1525
    public function getUnion(): array
1437
    {
1438 1525
        return $this->union;
1439
    }
1440
1441 1520
    public function getWithQueries(): array
1442
    {
1443 1520
        return $this->withQueries;
1444
    }
1445
1446
    /**
1447
     * Prepends a SQL statement using WITH syntax.
1448
     *
1449
     * @param Query|string $query the SQL statement to be appended using UNION.
1450
     * @param string $alias query alias in WITH construction.
1451
     * @param bool $recursive TRUE if using WITH RECURSIVE and FALSE if using WITH.
1452
     *
1453
     * @return $this the query object itself.
1454
     */
1455 15
    public function withQuery($query, string $alias, bool $recursive = false): self
1456
    {
1457 15
        $this->withQueries[] = ['query' => $query, 'alias' => $alias, 'recursive' => $recursive];
1458
1459 15
        return $this;
1460
    }
1461
1462
    /**
1463
     * This function can be overridden to customize the returned class.
1464
     *
1465
     * @param ConnectionInterface $value
1466
     *
1467
     * @return $this
1468
     */
1469 10
    protected static function createInstance(ConnectionInterface $value): self
1470
    {
1471 10
        return new self($value);
1472
    }
1473
1474 622
    public function selectOption(?string $value): self
1475
    {
1476 622
        $this->selectOption = $value;
1477
1478 622
        return $this;
1479
    }
1480
1481 622
    public function setJoin(array $value): self
1482
    {
1483 622
        $this->join = $value;
1484
1485 622
        return $this;
1486
    }
1487
1488 622
    public function setUnion(array $value): self
1489
    {
1490 622
        $this->union = $value;
1491
1492 622
        return $this;
1493
    }
1494
1495 622
    public function withQueries(array $value): self
1496
    {
1497 622
        $this->withQueries = $value;
1498
1499 622
        return $this;
1500
    }
1501
1502
    /**
1503
     * @param mixed $value
1504
     *
1505
     * @return int|string
1506
     */
1507 95
    private function typecastToIntegerOrString($value)
1508
    {
1509 95
        if (is_int($value)) {
1510 21
            return $value;
1511
        }
1512
1513 74
        $string = (string)$value;
1514 74
        $integer = (int)$string;
1515 74
        return (string)$integer === $string ? $integer : $string;
1516
    }
1517
}
1518