Passed
Pull Request — master (#466)
by Def
02:13
created

Query::setCommandCache()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 7
rs 10
c 0
b 0
f 0
ccs 0
cts 0
cp 0
cc 3
nc 2
nop 1
crap 12
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Query;
6
7
use Closure;
8
use Throwable;
9
use Yiisoft\Arrays\ArrayHelper;
10
use Yiisoft\Db\Command\CommandInterface;
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\Db\Query\Helper\QueryHelper;
19
use Yiisoft\Db\QueryBuilder\QueryBuilderInterface;
20
21
use function array_merge;
22
use function count;
23
use function is_array;
24
use function is_int;
25
use function is_string;
26
use function key;
27
use function preg_match;
28
use function preg_split;
29
use function reset;
30
use function str_contains;
31
use function strcasecmp;
32
use function strlen;
33
use function substr;
34
use function trim;
35
36
/**
37
 * Query represents a SELECT SQL statement in a way that is independent of DBMS.
38
 *
39
 * Query provides a set of methods to facilitate the specification of different clauses in a SELECT statement. These
40
 * methods can be chained together.
41
 *
42
 * By calling {@see createCommand()}, we can get a {@see Command} instance which can be further used to perform/execute
43
 * the DB query against a database.
44
 *
45
 * For example,
46
 *
47
 * ```php
48
 * $query = new Query;
49
 * // compose the query
50
 * $query->select('id, name')
51
 *     ->from('user')
52
 *     ->limit(10);
53
 * // build and execute the query
54
 * $rows = $query->all();
55
 * // alternatively, you can create DB command and execute it
56
 * $command = $query->createCommand();
57
 * // $command->sql returns the actual SQL
58
 * $rows = $command->queryAll();
59
 * ```
60
 *
61
 * Query internally uses the {@see QueryBuilder} class to generate the SQL statement.
62
 */
63
class Query implements QueryInterface
64
{
65
    protected array $select = [];
66
    protected string|null $selectOption = null;
67
    protected bool|null $distinct = null;
68
    protected array $from = [];
69
    protected array $groupBy = [];
70
    protected array|ExpressionInterface|string|null $having = null;
71
    protected array $join = [];
72
    private array $orderBy = [];
73
    protected array $params = [];
74
    protected array $union = [];
75
    protected array $withQueries = [];
76
    private bool $emulateExecution = false;
77
    private Closure|string|null $indexBy = null;
78
    private Expression|int|null $limit = null;
79
    private Expression|int|null $offset = null;
80
    private QueryHelper|null $queryHelper = null;
81
    private array|string|ExpressionInterface|null $where = null;
82
83
    public function __construct(private ConnectionInterface $db)
84
    {
85
    }
86
87
    /**
88
     * Returns the SQL representation of Query.
89 1891
     *
90
     * @return string
91 1891
     */
92 1891
    public function __toString(): string
93
    {
94
        return serialize($this);
95
    }
96
97
    public function addGroupBy(array|string|ExpressionInterface $columns): static
98
    {
99
        if ($columns instanceof ExpressionInterface) {
0 ignored issues
show
introduced by
$columns is never a sub-type of Yiisoft\Db\Expression\ExpressionInterface.
Loading history...
100
            $columns = [$columns];
101
        } elseif (!is_array($columns)) {
0 ignored issues
show
introduced by
The condition is_array($columns) is always true.
Loading history...
102
            $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
103 127
        }
104
105 127
        if ($this->groupBy === []) {
106
            $this->groupBy = $columns;
107 127
        } else {
108
            $this->groupBy = array_merge($this->groupBy, $columns);
109 127
        }
110
111 127
        return $this;
112
    }
113
114
    public function addOrderBy(array|string|ExpressionInterface $columns): static
115
    {
116
        $columns = $this->createQueryHelper()->normalizeOrderBy($columns);
117
118
        if ($this->orderBy === []) {
119
            $this->orderBy = $columns;
120
        } else {
121
            $this->orderBy = array_merge($this->orderBy, $columns);
122
        }
123
124 915
        return $this;
125
    }
126 915
127
    public function addParams(array $params): static
128
    {
129
        if (!empty($params)) {
130
            if (empty($this->params)) {
131
                $this->params = $params;
132
            } else {
133
                /**
134
                 * @psalm-var array $params
135
                 * @psalm-var mixed $value
136
                 */
137
                foreach ($params as $name => $value) {
138
                    if (is_int($name)) {
139
                        $this->params[] = $value;
140
                    } else {
141
                        $this->params[$name] = $value;
142
                    }
143
                }
144
            }
145
        }
146
147
        return $this;
148
    }
149
150
    public function andFilterHaving(array $condition): static
151 30
    {
152
        $condition = $this->filterCondition($condition);
153 30
154 30
        if ($condition !== []) {
155 30
            $this->andHaving($condition);
156 30
        }
157 30
158
        return $this;
159
    }
160
161
    public function andFilterWhere(array $condition): static
162
    {
163
        $condition = $this->filterCondition($condition);
164
165
        if ($condition !== []) {
166
            $this->andWhere($condition);
167
        }
168
169
        return $this;
170
    }
171
172
    public function andHaving(array|string|ExpressionInterface $condition, array $params = []): static
173
    {
174
        if ($this->having === null) {
175
            $this->having = $condition;
176
        } else {
177 10
            $this->having = ['and', $this->having, $condition];
178
        }
179 10
180 10
        $this->addParams($params);
181 10
182 10
        return $this;
183 10
    }
184
185
    public function addSelect(array|string|ExpressionInterface $columns): static
186
    {
187
        if ($this->select === []) {
188
            return $this->select($columns);
189
        }
190
191
        $this->select = array_merge($this->select, $this->createQueryHelper()->normalizeSelect($columns));
192
193
        return $this;
194
    }
195 311
196
    public function andFilterCompare(string $name, string|null $value, string $defaultOperator = '='): static
197 311
    {
198 15
        $operator = $defaultOperator;
199
200
        if (preg_match('/^(<>|>=|>|<=|<|=)/', (string) $value, $matches)) {
201 301
            $operator = $matches[1];
202
            $value = substr((string) $value, strlen($operator));
203 301
        }
204
205
        return $this->andFilterWhere([$operator, $name, $value]);
206
    }
207
208
    public function andWhere($condition, array $params = []): static
209
    {
210
        if ($this->where === null) {
211
            $this->where = $condition;
212
        } elseif (is_array($this->where) && isset($this->where[0]) && strcasecmp((string) $this->where[0], 'and') === 0) {
213
            $this->where[] = $condition;
214
        } else {
215
            $this->where = ['and', $this->where, $condition];
216 520
        }
217
218 520
        $this->addParams($params);
219 500
220
        return $this;
221
    }
222 55
223
    public function all(): array
224 55
    {
225 55
        return match ($this->emulateExecution) {
226
            true => [],
227
            false => $this->populate($this->createCommand()->queryAll()),
228 55
        };
229
    }
230
231
    public function average(string $q): int|float|null|string
232
    {
233
        return match ($this->emulateExecution) {
234
            true => null,
235
            false => is_numeric($avg = $this->queryScalar("AVG($q)")) ? $avg : null,
236
        };
237
    }
238
239
    public function batch(int $batchSize = 100): BatchQueryResultInterface
240
    {
241 432
        return $this->db->createBatchQueryResult($this)->batchSize($batchSize);
242
    }
243 432
244 10
    /**
245
     * @psalm-suppress MixedArrayOffset
246
     *
247 422
     * @throws Exception
248
     * @throws InvalidConfigException
249
     * @throws NotSupportedException
250
     * @throws Throwable
251
     */
252
    public function column(): array
253
    {
254
        if ($this->emulateExecution) {
255
            return [];
256
        }
257
258
        if ($this->indexBy === null) {
259
            return $this->createCommand()->queryColumn();
260 25
        }
261
262 25
        if (is_string($this->indexBy) && count($this->select) === 1) {
263 10
            if (!str_contains($this->indexBy, '.') && count($tables = $this->getTablesUsedInFrom()) > 0) {
264
                $this->select[] = key($tables) . '.' . $this->indexBy;
265
            } else {
266 15
                $this->select[] = $this->indexBy;
267
            }
268
        }
269
270
        $rows = $this->createCommand()->queryAll();
271
        $results = [];
272
        $column = null;
273
        if (is_string($this->indexBy)) {
274
            if (($dotPos = strpos($this->indexBy, '.')) === false) {
275
                $column = $this->indexBy;
276
            } else {
277
                $column = substr($this->indexBy, $dotPos + 1);
278 25
            }
279
        }
280 25
281 10
        /** @psalm-var array<array-key, array<string, string>> $rows */
282
        foreach ($rows as $row) {
283
            $value = reset($row);
284 15
285 15
            if ($this->indexBy instanceof Closure) {
286
                $results[($this->indexBy)($row)] = $value;
287
            } else {
288 5
                $results[$row[$column] ?? $row[$this->indexBy]] = $value;
289 5
            }
290 5
        }
291
292
        return $results;
293
    }
294
295
    public function count(string $q = '*'): int|string
296 5
    {
297 5
        return match ($this->emulateExecution) {
298 5
            true => 0,
299 5
            false => is_numeric($count = $this->queryScalar("COUNT($q)")) ? (int) $count : 0,
300
        };
301 5
    }
302 5
303
    public function createCommand(): CommandInterface
304 5
    {
305
        [$sql, $params] = $this->db->getQueryBuilder()->build($this);
306
        $command = $this->db->createCommand($sql, $params);
307
308 5
        return $command;
309
    }
310
311
    public function distinct(bool|null $value = true): static
312
    {
313
        $this->distinct = $value;
314
315
        return $this;
316
    }
317
318
    public function each(int $batchSize = 100): BatchQueryResultInterface
319
    {
320
        return $this->db->createBatchQueryResult($this, true)->batchSize($batchSize);
321
    }
322 95
323
    /**
324 95
     * @throws Exception|InvalidConfigException|Throwable
325 10
     */
326
    public function exists(): bool
327
    {
328 95
        if ($this->emulateExecution) {
329
            return false;
330
        }
331
332
        $command = $this->createCommand();
333
        $params = $command->getParams();
334
        $command->setSql($this->db->getQueryBuilder()->selectExists($command->getSql()));
335
        $command->bindValues($params);
336
337
        return (bool) $command->queryScalar();
338
    }
339
340
    public function emulateExecution(bool $value = true): static
341 15
    {
342
        $this->emulateExecution = $value;
343 15
344 10
        return $this;
345
    }
346
347 5
    public function filterHaving(array $condition): static
348
    {
349
        $condition = $this->filterCondition($condition);
350
351
        if ($condition !== []) {
352
            $this->having($condition);
353
        }
354
355
        return $this;
356
    }
357
358
    public function filterWhere(array $condition): static
359
    {
360 15
        $condition = $this->filterCondition($condition);
361
362 15
        if ($condition !== []) {
363 10
            $this->where($condition);
364
        }
365
366 5
        return $this;
367
    }
368
369
    public function from(array|ExpressionInterface|string $tables): static
370
    {
371
        if ($tables instanceof ExpressionInterface) {
0 ignored issues
show
introduced by
$tables is never a sub-type of Yiisoft\Db\Expression\ExpressionInterface.
Loading history...
372
            $tables = [$tables];
373
        }
374
375
        if (is_string($tables)) {
0 ignored issues
show
introduced by
The condition is_string($tables) is always false.
Loading history...
376
            $tables = preg_split('/\s*,\s*/', trim($tables), -1, PREG_SPLIT_NO_EMPTY);
377
        }
378
379 15
        $this->from = $tables;
380
381 15
        return $this;
382
    }
383
384
    public function getDistinct(): bool|null
385
    {
386
        return $this->distinct;
387
    }
388
389
    public function getFrom(): array
390
    {
391
        return $this->from;
392
    }
393
394 15
    public function getGroupBy(): array
395
    {
396 15
        return $this->groupBy;
397
    }
398
399
    public function getHaving(): string|array|ExpressionInterface|null
400
    {
401
        return $this->having;
402
    }
403
404
    public function getIndexBy(): Closure|string|null
405
    {
406 25
        return $this->indexBy;
407
    }
408 25
409 10
    public function getJoin(): array
410
    {
411
        return $this->join;
412 15
    }
413 15
414 15
    public function getLimit(): Expression|int|null
415 15
    {
416
        return $this->limit;
417 15
    }
418
419
    public function getOffset(): Expression|int|null
420
    {
421
        return $this->offset;
422
    }
423
424
    public function getOrderBy(): array
425
    {
426
        return $this->orderBy;
427
    }
428
429
    public function getParams(): array
430
    {
431 95
        return $this->params;
432
    }
433 95
434 10
    public function getSelect(): array
435
    {
436
        return $this->select;
437
    }
438 95
439 95
    public function getSelectOption(): string|null
440 95
    {
441 95
        return $this->selectOption;
442 95
    }
443
444 95
    public function getTablesUsedInFrom(): array
445 95
    {
446 95
        return $this->createQueryHelper()->cleanUpTableNames($this->from, $this->db->getQuoter());
447 95
    }
448
449 95
    public function getUnion(): array
450 95
    {
451 95
        return $this->union;
452 95
    }
453
454
    public function getWhere(): array|string|ExpressionInterface|null
455 95
    {
456
        return $this->where;
457
    }
458
459
    public function getWithQueries(): array
460 95
    {
461 95
        return $this->withQueries;
462 95
    }
463 95
464
    public function groupBy(array|string|ExpressionInterface $columns): static
465 95
    {
466
        if ($columns instanceof ExpressionInterface) {
0 ignored issues
show
introduced by
$columns is never a sub-type of Yiisoft\Db\Expression\ExpressionInterface.
Loading history...
467
            $columns = [$columns];
468
        } elseif (!is_array($columns)) {
0 ignored issues
show
introduced by
The condition is_array($columns) is always true.
Loading history...
469 95
            $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
470
        }
471
        $this->groupBy = $columns;
472 10
473 10
        return $this;
474 10
    }
475 10
476
    public function having(array|ExpressionInterface|string|null $condition, array $params = []): static
477 10
    {
478
        $this->having = $condition;
479 10
        $this->addParams($params);
480
481
        return $this;
482
    }
483
484
    public function indexBy(Closure|string|null $column): static
485
    {
486
        $this->indexBy = $column;
487
488
        return $this;
489
    }
490
491
    public function innerJoin(array|string $table, array|string $on = '', array $params = []): static
492 110
    {
493
        $this->join[] = ['INNER JOIN', $table, $on];
494 110
495
        return $this->addParams($params);
496
    }
497
498 110
    public function join(string $type, array|string $table, array|string $on = '', array $params = []): static
499 105
    {
500 5
        $this->join[] = [$type, $table, $on];
501
502 5
        return $this->addParams($params);
503
    }
504
505 5
    public function leftJoin(array|string $table, array|string $on = '', array $params = []): static
506
    {
507
        $this->join[] = ['LEFT JOIN', $table, $on];
508 105
509
        return $this->addParams($params);
510
    }
511
512
    public function limit(Expression|int|null $limit): static
513
    {
514
        $this->limit = $limit;
515
516
        return $this;
517
    }
518
519
    public function max(string $q): int|float|null|string
520
    {
521
        $max = $this->queryScalar("MAX($q)");
522 260
523
        return is_numeric($max) ? $max : null;
524 260
    }
525 260
526 260
    public function min(string $q): int|float|null|string
527
    {
528 225
        $min = $this->queryScalar("MIN($q)");
529
530
        return is_numeric($min) ? $min : null;
531
    }
532
533
    public function offset(Expression|int|null $offset): static
534
    {
535
        $this->offset = $offset;
536
537
        return $this;
538
    }
539
540
    public function one(): array|object|null
541
    {
542
        return match ($this->emulateExecution) {
543
            true => null,
544
            false => $this->createCommand()->queryOne(),
545
        };
546
    }
547
548
    public function orderBy(array|string|ExpressionInterface $columns): static
549
    {
550
        $this->orderBy = $this->createQueryHelper()->normalizeOrderBy($columns);
551
552
        return $this;
553
    }
554
555
    public function orFilterHaving(array $condition): static
556
    {
557
        $condition = $this->filterCondition($condition);
558 225
559 225
        if ($condition !== []) {
560 15
            $this->orHaving($condition);
561
        }
562 220
563
        return $this;
564
    }
565
566
    public function orFilterWhere(array $condition): static
567 260
    {
568 10
        $condition = $this->filterCondition($condition);
569 5
570 5
        if ($condition !== []) {
571
            $this->orWhere($condition);
572
        }
573 5
574 250
        return $this;
575 5
    }
576
577 245
    public function orHaving(array|string|ExpressionInterface $condition, array $params = []): static
578
    {
579
        if ($this->having === null) {
580
            $this->having = $condition;
581 255
        } else {
582
            $this->having = ['or', $this->having, $condition];
583
        }
584
585
        $this->addParams($params);
586
587
        return $this;
588
    }
589
590
    public function orWhere(array|string|ExpressionInterface $condition, array $params = []): static
591 255
    {
592
        if ($this->where === null) {
593 255
            $this->where = $condition;
594 255
        } else {
595 250
            $this->where = ['or', $this->where, $condition];
596
        }
597
598 20
        $this->addParams($params);
599
600
        return $this;
601
    }
602
603
    public function params(array $params): static
604
    {
605
        $this->params = $params;
606
607
        return $this;
608
    }
609
610
    /**
611
     * @psalm-suppress MixedArrayOffset
612
     */
613
    public function populate(array $rows): array
614
    {
615
        if ($this->indexBy === null) {
616
            return $rows;
617
        }
618
619
        $result = [];
620
621
        /** @psalm-var array[][] $row */
622
        foreach ($rows as $row) {
623 796
            $result[ArrayHelper::getValueByPath($row, $this->indexBy)] = $row;
624
        }
625 796
626 796
        return $result;
627
    }
628 796
629
    public function prepare(QueryBuilderInterface $builder): QueryInterface
630
    {
631
        return $this;
632
    }
633
634
    public function rightJoin(array|string $table, array|string $on = '', array $params = []): static
635
    {
636
        $this->join[] = ['RIGHT JOIN', $table, $on];
637
638
        return $this->addParams($params);
639
    }
640
641
    public function scalar(): bool|int|null|string|float
642
    {
643
        return match ($this->emulateExecution) {
644
            true => null,
645
            false => $this->createCommand()->queryScalar(),
646
        };
647
    }
648 15
649
    public function select(array|string|ExpressionInterface $columns, string $option = null): static
650 15
    {
651
        $this->select = $this->createQueryHelper()->normalizeSelect($columns);
652
        $this->selectOption = $option;
653
654 15
        return $this;
655
    }
656
657
    public function selectOption(string|null $value): static
658 15
    {
659
        $this->selectOption = $value;
660 15
661
        return $this;
662
    }
663
664
    public function setJoin(array $value): static
665
    {
666
        $this->join = $value;
667
668
        return $this;
669
    }
670 796
671
    public function setUnion(array $value): static
672 796
    {
673 5
        $this->union = $value;
674 796
675 113
        return $this;
676
    }
677
678 796
    public function shouldEmulateExecution(): bool
679 796
    {
680 355
        return $this->emulateExecution;
681
    }
682 182
683 182
    public function sum(string $q): int|float|null|string
684
    {
685 348
        return match ($this->emulateExecution) {
686
            true => null,
687 343
            false => is_numeric($sum = $this->queryScalar("SUM($q)")) ? $sum : null,
688 343
        };
689 343
    }
690
691
    public function union(QueryInterface|string $sql, bool $all = false): static
692 26
    {
693 26
        $this->union[] = ['query' => $sql, 'all' => $all];
694
695 342
        return $this;
696
    }
697 286
698 286
    public function where(array|string|ExpressionInterface|null $condition, array $params = []): static
699
    {
700
        $this->where = $condition;
701
        $this->addParams($params);
702 106
703
        return $this;
704
    }
705 796
706
    public function withQuery(QueryInterface|string $query, string $alias, bool $recursive = false): static
707
    {
708
        $this->withQueries[] = ['query' => $query, 'alias' => $alias, 'recursive' => $recursive];
709
710
        return $this;
711
    }
712
713
    public function withQueries(array $withQueries): static
714
    {
715 627
        $this->withQueries = $withQueries;
716
717 627
        return $this;
718
    }
719 627
720
    /**
721
     * Queries a scalar value by setting {@see select} first.
722
     *
723
     * Restores the value of select to make this query reusable.
724
     *
725
     * @param ExpressionInterface|string $selectExpression
726
     *
727
     * @throws Exception
728
     * @throws InvalidArgumentException
729
     * @throws InvalidConfigException
730
     * @throws NotSupportedException
731
     * @throws Throwable
732
     *
733
     * @return bool|float|int|string|null
734
     *
735
     * @psalm-suppress PossiblyUndefinedVariable
736
     */
737
    protected function queryScalar(string|ExpressionInterface $selectExpression): bool|int|null|string|float
738
    {
739
        if ($this->emulateExecution) {
740
            return null;
741
        }
742
743
        if (
744
            !$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...
745
            && empty($this->groupBy)
746
            && empty($this->having)
747
            && empty($this->union)
748
            && empty($this->with)
749
        ) {
750
            $select = $this->select;
751
            $order = $this->orderBy;
752
            $limit = $this->limit;
753
            $offset = $this->offset;
754
755
            $this->select = [$selectExpression];
756
            $this->orderBy = [];
757
            $this->limit = null;
758
            $this->offset = null;
759 970
760
            $command = $this->createCommand();
761 970
762 16
            $this->select = $select;
763
            $this->orderBy = $order;
764 970
            $this->limit = $limit;
765 263
            $this->offset = $offset;
766
767 970
            return $command->queryScalar();
768
        }
769 970
770
        $query = (new self($this->db))->select($selectExpression)->from(['c' => $this]);
771
        [$sql, $params] = $this->db->getQueryBuilder()->build($query);
772
        $command = $this->db->createCommand($sql, $params);
773
774
        return $command->queryScalar();
775
    }
776
777
    private function createQueryHelper(): QueryHelper
778
    {
779
        if ($this->queryHelper === null) {
780
            $this->queryHelper = new QueryHelper();
781
        }
782
783
        return $this->queryHelper;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->queryHelper could return the type null which is incompatible with the type-hinted return Yiisoft\Db\Query\Helper\QueryHelper. Consider adding an additional type-check to rule them out.
Loading history...
784
    }
785
786
    private function filterCondition(array $condition): array
787
    {
788
        return (array) $this->createQueryHelper()->filterCondition($condition);
789
    }
790
}
791