Passed
Pull Request — master (#235)
by Sergei
14:23
created

Query::rightJoin()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 2
c 0
b 0
f 0
nc 1
nop 3
dl 0
loc 5
ccs 0
cts 2
cp 0
crap 2
rs 10
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
            $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 15
     * @return mixed the sum of the specified column values.
342
     */
343 15
    public function sum(string $q)
344 10
    {
345
        if ($this->emulateExecution) {
346
            return 0;
347 5
        }
348
349
        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 15
     * @return mixed the average of the specified column values.
361
     */
362 15
    public function average(string $q)
363 10
    {
364
        if ($this->emulateExecution) {
365
            return 0;
366 5
        }
367
368
        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 15
     * @return mixed the minimum of the specified column values.
380
     */
381 15
    public function min(string $q)
382
    {
383
        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 15
     * @return mixed the maximum of the specified column values.
395
     */
396 15
    public function max(string $q)
397
    {
398
        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 25
     * @return bool whether the query result contains any row of data.
407
     */
408 25
    public function exists(): bool
409 10
    {
410
        if ($this->emulateExecution) {
411
            return false;
412 15
        }
413 15
414 15
        $command = $this->createCommand();
415 15
        $params = $command->getParams();
416
        $command->setSql($this->db->getQueryBuilder()->selectExists($command->getSql()));
417 15
        $command->bindValues($params);
418
419
        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 95
     * @return bool|string
432
     */
433 95
    protected function queryScalar($selectExpression)
434 10
    {
435
        if ($this->emulateExecution) {
436
            return null;
437
        }
438 95
439 95
        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
            && empty($this->union)
444 95
            && empty($this->with)
445 95
        ) {
446 95
            $select = $this->select;
447 95
            $order = $this->orderBy;
448
            $limit = $this->limit;
449 95
            $offset = $this->offset;
450 95
451 95
            $this->select = [$selectExpression];
452 95
            $this->orderBy = [];
453
            $this->limit = null;
454
            $this->offset = null;
455 95
456
            try {
457
                $command = $this->createCommand();
458
            } catch (Throwable $e) {
459
                /** throw it later */
460 95
            }
461 95
462 95
            $this->select = $select;
463 95
            $this->orderBy = $order;
464
            $this->limit = $limit;
465 95
            $this->offset = $offset;
466
467
            if (isset($e)) {
468
                throw $e;
469 95
            }
470
471
            return $command->queryScalar();
472 10
        }
473 10
474 10
        $command = static::createInstance($this->db)
475 10
            ->select([$selectExpression])
476
            ->from(['c' => $this])
477 10
            ->createCommand();
478
479 10
        $this->setCommandCache($command);
480
481
        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 110
     * @return array table names indexed by aliases
493
     */
494 110
    public function getTablesUsedInFrom(): array
495
    {
496
        if (empty($this->from)) {
497
            return [];
498 110
        }
499 105
500 5
        if (is_array($this->from)) {
501
            $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
        } elseif ($this->from instanceof Expression) {
505 5
            $tableNames = [$this->from];
506
        } else {
507
            throw new InvalidConfigException(gettype($this->from) . ' in $from is not supported.');
508 105
        }
509
510
        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 260
     * @return string[] table names indexed by aliases
523
     */
524 260
    protected function cleanUpTableNames(array $tableNames): array
525 260
    {
526 260
        $cleanedUpTableNames = [];
527
        foreach ($tableNames as $alias => $tableName) {
528 225
            if (is_string($tableName) && !is_string($alias)) {
529
                $pattern = <<<PATTERN
530
~
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 225
~iux
559 225
PATTERN;
560 15
                if (preg_match($pattern, $tableName, $matches)) {
561
                    if (isset($matches[2])) {
562 220
                        [, $tableName, $alias] = $matches;
563
                    } else {
564
                        $tableName = $alias = $matches[1];
565
                    }
566
                }
567 260
            }
568 10
569 5
            if ($tableName instanceof Expression) {
570 5
                if (!is_string($alias)) {
571
                    throw new InvalidArgumentException(
572
                        'To use Expression in from() method, pass it in array format with alias.'
573 5
                    );
574 250
                }
575 5
                $cleanedUpTableNames[$this->ensureNameQuoted($alias)] = $tableName;
576
            } elseif ($tableName instanceof self) {
577 245
                $cleanedUpTableNames[$this->ensureNameQuoted($alias)] = $tableName;
578
            } else {
579
                $cleanedUpTableNames[$this->ensureNameQuoted($alias)] = $this->ensureNameQuoted($tableName);
580
            }
581 255
        }
582
583
        return $cleanedUpTableNames;
584
    }
585
586
    /**
587
     * Ensures name is wrapped with {{ and }}.
588
     *
589
     * @param string $name
590
     *
591 255
     * @return string
592
     */
593 255
    private function ensureNameQuoted(string $name): string
594 255
    {
595 250
        $name = str_replace(["'", '"', '`', '[', ']'], '', $name);
596
        if ($name && !preg_match('/^{{.*}}$/', $name)) {
597
            return '{{' . $name . '}}';
598 20
        }
599
600
        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 789
     * @return $this the query object itself.
624
     */
625 789
    public function select($columns, ?string $option = null): self
626 789
    {
627
        $this->select = $this->normalizeSelect($columns);
628 789
        $this->selectOption = $option;
629
630
        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 15
     * {@see select()}
649
     */
650 15
    public function addSelect($columns): self
651
    {
652
        if ($this->select === null) {
653
            return $this->select($columns);
654 15
        }
655
656
        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 15
        }
659
660 15
        $this->select = array_merge($this->select, $this->normalizeSelect($columns));
661
662
        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 789
     * @return array
671
     */
672 789
    protected function normalizeSelect($columns): array
673 5
    {
674 789
        if ($columns instanceof ExpressionInterface) {
675 113
            $columns = [$columns];
676
        } elseif (!is_array($columns)) {
677
            $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
678 789
        }
679 789
680 348
        $select = [];
681
        foreach ($columns as $columnAlias => $columnDefinition) {
682 182
            if (is_string($columnAlias)) {
683 182
                /** Already in the normalized format, good for them */
684
                $select[$columnAlias] = $columnDefinition;
685 341
                continue;
686
            }
687 336
            if (is_string($columnDefinition)) {
688 336
                if (
689 336
                    preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-_.]+)$/', $columnDefinition, $matches) &&
690
                    !preg_match('/^\d+$/', $matches[2]) &&
691
                    strpos($matches[2], '.') === false
692 25
                ) {
693 25
                    /** Using "columnName as alias" or "columnName alias" syntax */
694
                    $select[$matches[2]] = $matches[1];
695 336
                    continue;
696
                }
697 280
                if (strpos($columnDefinition, '(') === false) {
698 280
                    /** Normal column name, just alias it to itself to ensure it's not selected twice */
699
                    $select[$columnDefinition] = $columnDefinition;
700
                    continue;
701
                }
702 106
            }
703
            /** Either a string calling a function, DB expression, or sub-query */
704
            $select[] = $columnDefinition;
705 789
        }
706
707
        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 627
     * @return $this the query object itself
716
     */
717 627
    public function distinct(?bool $value = true): self
718
    {
719 627
        $this->distinct = $value;
720
721
        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 963
     * @return $this the query object itself
760
     */
761 963
    public function from($tables): self
762 16
    {
763
        if ($tables instanceof ExpressionInterface) {
764 963
            $tables = [$tables];
765 256
        }
766
        if (is_string($tables)) {
767 963
            $tables = preg_split('/\s*,\s*/', trim($tables), -1, PREG_SPLIT_NO_EMPTY);
768
        }
769 963
        $this->from = $tables;
770
771
        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 1368
     * {@see QueryInterface::where()}
792
     */
793 1368
    public function where($condition, array $params = []): self
794 1368
    {
795
        $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
        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 348
     * {@see orWhere()}
814
     */
815 348
    public function andWhere($condition, array $params = []): self
816 323
    {
817 131
        if ($this->where === null) {
818 6
            $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
        } elseif (is_array($this->where) && isset($this->where[0]) && strcasecmp($this->where[0], 'and') === 0) {
820 131
            $this->where[] = $condition;
821
        } else {
822
            $this->where = ['and', $this->where, $condition];
823 348
        }
824
825 348
        $this->addParams($params);
826
827
        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 6
     * {@see andWhere()}
843
     */
844 6
    public function orWhere($condition, array $params = []): self
845
    {
846
        if ($this->where === null) {
847 6
            $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
            $this->where = ['or', $this->where, $condition];
850 6
        }
851
852 6
        $this->addParams($params);
853
854
        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 5
     * @return $this the query object itself.
885
     */
886 5
    public function andFilterCompare(string $name, ?string $value, string $defaultOperator = '='): self
887 5
    {
888 5
        if (preg_match('/^(<>|>=|>|<=|<|=)/', (string) $value, $matches)) {
889
            $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
            $operator = $defaultOperator;
893 5
        }
894
895
        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 110
     * @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
        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 5
     * @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
        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 5
     * @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
        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 637
     * {@see addGroupBy()}
1024
     */
1025 637
    public function groupBy($columns): self
1026 5
    {
1027 637
        if ($columns instanceof ExpressionInterface) {
1028 25
            $columns = [$columns];
1029
        } elseif (!is_array($columns)) {
1030 637
            $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
1031
        }
1032 637
        $this->groupBy = $columns;
1033
1034
        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 5
     * {@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 5
            $columns = [$columns];
1059
        } elseif (!is_array($columns)) {
1060 5
            $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
1061
        }
1062
        if ($this->groupBy === null) {
1063 5
            $this->groupBy = $columns;
1064
        } else {
1065
            $this->groupBy = array_merge($this->groupBy, $columns);
1066 5
        }
1067
1068
        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 637
     * {@see orHaving()}
1082
     */
1083 637
    public function having($condition, array $params = []): self
1084 637
    {
1085
        $this->having = $condition;
1086 637
        $this->addParams($params);
1087
1088
        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 5
     * {@see orHaving()}
1103
     */
1104 5
    public function andHaving($condition, array $params = []): self
1105
    {
1106
        if ($this->having === null) {
1107 5
            $this->having = $condition;
1108
        } else {
1109
            $this->having = ['and', $this->having, $condition];
1110 5
        }
1111
1112 5
        $this->addParams($params);
1113
1114
        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 5
     * {@see andHaving()}
1130
     */
1131 5
    public function orHaving($condition, $params = []): self
1132
    {
1133
        if ($this->having === null) {
1134 5
            $this->having = $condition;
1135
        } else {
1136
            $this->having = ['or', $this->having, $condition];
1137 5
        }
1138
1139 5
        $this->addParams($params);
1140
1141
        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 10
     * {@see orFilterHaving()}
1174
     */
1175 10
    public function filterHaving(array $condition): self
1176
    {
1177 10
        $condition = $this->filterCondition($condition);
1178 10
1179
        if ($condition !== []) {
1180
            $this->having($condition);
1181 10
        }
1182
1183
        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 10
     * {@see orFilterHaving()}
1204
     */
1205 10
    public function andFilterHaving(array $condition): self
1206
    {
1207 10
        $condition = $this->filterCondition($condition);
1208
1209
        if ($condition !== []) {
1210
            $this->andHaving($condition);
1211 10
        }
1212
1213
        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 10
     * {@see andFilterHaving()}
1234
     */
1235 10
    public function orFilterHaving(array $condition): self
1236
    {
1237 10
        $condition = $this->filterCondition($condition);
1238
1239
        if ($condition !== []) {
1240
            $this->orHaving($condition);
1241 10
        }
1242
1243
        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 17
     * @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
        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 640
     * {@see addParams()}
1270
     */
1271 640
    public function params(array $params): self
1272
    {
1273 640
        $this->params = $params;
1274
1275
        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 1459
     * {@see params()}
1287
     */
1288 1459
    public function addParams(array $params): self
1289 62
    {
1290 62
        if (!empty($params)) {
1291
            if (empty($this->params)) {
1292 11
                $this->params = $params;
1293 11
            } else {
1294
                foreach ($params as $name => $value) {
1295
                    if (is_int($name)) {
1296 11
                        $this->params[] = $value;
1297
                    } else {
1298
                        $this->params[$name] = $value;
1299
                    }
1300
                }
1301
            }
1302 1459
        }
1303
1304
        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 5
     * @return $this the Query object itself.
1318
     */
1319 5
    public function cache($duration = true, ?Dependency $dependency = null): self
1320 5
    {
1321
        $this->queryCacheDuration = $duration;
1322 5
        $this->queryCacheDependency = $dependency;
1323
1324
        return $this;
1325
    }
1326
1327
    /**
1328
     * Disables query cache for this Query.
1329
     *
1330 5
     * @return $this the Query object itself.
1331
     */
1332 5
    public function noCache(): self
1333
    {
1334 5
        $this->queryCacheDuration = -1;
1335
1336
        return $this;
1337
    }
1338
1339
    /**
1340
     * Sets $command cache, if this query has enabled caching.
1341
     *
1342
     * @param Command $command
1343
     *
1344 672
     * @return Command
1345
     */
1346 672
    protected function setCommandCache(Command $command): Command
1347 5
    {
1348 5
        if ($this->queryCacheDuration !== null || $this->queryCacheDependency !== null) {
1349
            $duration = $this->queryCacheDuration === true ? null : $this->queryCacheDuration;
1350
            $command->cache($duration, $this->queryCacheDependency);
1351 672
        }
1352
1353
        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 622
     * @return $this the new Query object.
1365
     */
1366 622
    public static function create(ConnectionInterface $db, self $from): self
1367 622
    {
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
            ->params($from->params)
1383
            ->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 1525
    }
1395
1396 1525
    public function getDistinct(): ?bool
1397
    {
1398
        return $this->distinct;
1399 1561
    }
1400
1401 1561
    public function getFrom()
1402
    {
1403
        return $this->from;
1404 1530
    }
1405
1406 1530
    public function getGroupBy(): array
1407
    {
1408
        return $this->groupBy;
1409 1540
    }
1410
1411 1540
    public function getHaving()
1412
    {
1413
        return $this->having;
1414 1530
    }
1415
1416 1530
    public function getJoin(): array
1417
    {
1418
        return $this->join;
1419 1565
    }
1420
1421 1565
    public function getParams(): array
1422
    {
1423
        return $this->params;
1424 1545
    }
1425
1426 1545
    public function getSelect(): array
1427
    {
1428
        return $this->select;
1429 1525
    }
1430
1431 1525
    public function getSelectOption(): ?string
1432
    {
1433
        return $this->selectOption;
1434 1525
    }
1435
1436 1525
    public function getUnion(): array
1437
    {
1438
        return $this->union;
1439 1520
    }
1440
1441 1520
    public function getWithQueries(): array
1442
    {
1443
        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 15
     * @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
        return $this;
1460
    }
1461
1462
    /**
1463
     * This function can be overridden to customize the returned class.
1464
     *
1465
     * @param ConnectionInterface $value
1466
     *
1467 10
     * @return $this
1468
     */
1469 10
    protected static function createInstance(ConnectionInterface $value): self
1470
    {
1471
        return new self($value);
1472 622
    }
1473
1474 622
    public function selectOption(?string $value): self
1475
    {
1476 622
        $this->selectOption = $value;
1477
1478
        return $this;
1479 622
    }
1480
1481 622
    public function setJoin(array $value): self
1482
    {
1483 622
        $this->join = $value;
1484
1485
        return $this;
1486 622
    }
1487
1488 622
    public function setUnion(array $value): self
1489
    {
1490 622
        $this->union = $value;
1491
1492
        return $this;
1493 622
    }
1494
1495 622
    public function withQueries(array $value): self
1496
    {
1497 622
        $this->withQueries = $value;
1498
1499
        return $this;
1500
    }
1501
1502
    /**
1503
     * @param mixed $value
1504
     *
1505
     * @return int|string
1506
     */
1507
    private function typecastToIntegerOrString($value)
1508
    {
1509
        if (is_int($value)) {
1510
            return $value;
1511
        }
1512
1513
        $string = (string)$value;
1514
        $integer = (int)$string;
1515
        return (string)$integer === $string ? $integer : $string;
1516
    }
1517
}
1518