Passed
Push — master ( 4a78e7...e96213 )
by Def
24:10 queued 21:57
created

Query::cache()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 2
dl 0
loc 6
ccs 0
cts 0
cp 0
crap 2
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 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
        return $this->db->createCommand($sql, $params);
307
    }
308 5
309
    public function distinct(bool|null $value = true): static
310
    {
311
        $this->distinct = $value;
312
313
        return $this;
314
    }
315
316
    public function each(int $batchSize = 100): BatchQueryResultInterface
317
    {
318
        return $this->db->createBatchQueryResult($this, true)->batchSize($batchSize);
319
    }
320
321
    /**
322 95
     * @throws Exception|InvalidConfigException|Throwable
323
     */
324 95
    public function exists(): bool
325 10
    {
326
        if ($this->emulateExecution) {
327
            return false;
328 95
        }
329
330
        $command = $this->createCommand();
331
        $params = $command->getParams();
332
        $command->setSql($this->db->getQueryBuilder()->selectExists($command->getSql()));
333
        $command->bindValues($params);
334
335
        return (bool) $command->queryScalar();
336
    }
337
338
    public function emulateExecution(bool $value = true): static
339
    {
340
        $this->emulateExecution = $value;
341 15
342
        return $this;
343 15
    }
344 10
345
    public function filterHaving(array $condition): static
346
    {
347 5
        $condition = $this->filterCondition($condition);
348
349
        if ($condition !== []) {
350
            $this->having($condition);
351
        }
352
353
        return $this;
354
    }
355
356
    public function filterWhere(array $condition): static
357
    {
358
        $condition = $this->filterCondition($condition);
359
360 15
        if ($condition !== []) {
361
            $this->where($condition);
362 15
        }
363 10
364
        return $this;
365
    }
366 5
367
    public function from(array|ExpressionInterface|string $tables): static
368
    {
369
        if ($tables instanceof ExpressionInterface) {
0 ignored issues
show
introduced by
$tables is never a sub-type of Yiisoft\Db\Expression\ExpressionInterface.
Loading history...
370
            $tables = [$tables];
371
        }
372
373
        if (is_string($tables)) {
0 ignored issues
show
introduced by
The condition is_string($tables) is always false.
Loading history...
374
            $tables = preg_split('/\s*,\s*/', trim($tables), -1, PREG_SPLIT_NO_EMPTY);
375
        }
376
377
        $this->from = $tables;
378
379 15
        return $this;
380
    }
381 15
382
    public function getDistinct(): bool|null
383
    {
384
        return $this->distinct;
385
    }
386
387
    public function getFrom(): array
388
    {
389
        return $this->from;
390
    }
391
392
    public function getGroupBy(): array
393
    {
394 15
        return $this->groupBy;
395
    }
396 15
397
    public function getHaving(): string|array|ExpressionInterface|null
398
    {
399
        return $this->having;
400
    }
401
402
    public function getIndexBy(): Closure|string|null
403
    {
404
        return $this->indexBy;
405
    }
406 25
407
    public function getJoin(): array
408 25
    {
409 10
        return $this->join;
410
    }
411
412 15
    public function getLimit(): Expression|int|null
413 15
    {
414 15
        return $this->limit;
415 15
    }
416
417 15
    public function getOffset(): Expression|int|null
418
    {
419
        return $this->offset;
420
    }
421
422
    public function getOrderBy(): array
423
    {
424
        return $this->orderBy;
425
    }
426
427
    public function getParams(): array
428
    {
429
        return $this->params;
430
    }
431 95
432
    public function getSelect(): array
433 95
    {
434 10
        return $this->select;
435
    }
436
437
    public function getSelectOption(): string|null
438 95
    {
439 95
        return $this->selectOption;
440 95
    }
441 95
442 95
    public function getTablesUsedInFrom(): array
443
    {
444 95
        return $this->createQueryHelper()->cleanUpTableNames($this->from, $this->db->getQuoter());
445 95
    }
446 95
447 95
    public function getUnion(): array
448
    {
449 95
        return $this->union;
450 95
    }
451 95
452 95
    public function getWhere(): array|string|ExpressionInterface|null
453
    {
454
        return $this->where;
455 95
    }
456
457
    public function getWithQueries(): array
458
    {
459
        return $this->withQueries;
460 95
    }
461 95
462 95
    public function groupBy(array|string|ExpressionInterface $columns): static
463 95
    {
464
        if ($columns instanceof ExpressionInterface) {
0 ignored issues
show
introduced by
$columns is never a sub-type of Yiisoft\Db\Expression\ExpressionInterface.
Loading history...
465 95
            $columns = [$columns];
466
        } elseif (!is_array($columns)) {
0 ignored issues
show
introduced by
The condition is_array($columns) is always true.
Loading history...
467
            $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
468
        }
469 95
        $this->groupBy = $columns;
470
471
        return $this;
472 10
    }
473 10
474 10
    public function having(array|ExpressionInterface|string|null $condition, array $params = []): static
475 10
    {
476
        $this->having = $condition;
477 10
        $this->addParams($params);
478
479 10
        return $this;
480
    }
481
482
    public function indexBy(Closure|string|null $column): static
483
    {
484
        $this->indexBy = $column;
485
486
        return $this;
487
    }
488
489
    public function innerJoin(array|string $table, array|string $on = '', array $params = []): static
490
    {
491
        $this->join[] = ['INNER JOIN', $table, $on];
492 110
493
        return $this->addParams($params);
494 110
    }
495
496
    public function join(string $type, array|string $table, array|string $on = '', array $params = []): static
497
    {
498 110
        $this->join[] = [$type, $table, $on];
499 105
500 5
        return $this->addParams($params);
501
    }
502 5
503
    public function leftJoin(array|string $table, array|string $on = '', array $params = []): static
504
    {
505 5
        $this->join[] = ['LEFT JOIN', $table, $on];
506
507
        return $this->addParams($params);
508 105
    }
509
510
    public function limit(Expression|int|null $limit): static
511
    {
512
        $this->limit = $limit;
513
514
        return $this;
515
    }
516
517
    public function max(string $q): int|float|null|string
518
    {
519
        $max = $this->queryScalar("MAX($q)");
520
521
        return is_numeric($max) ? $max : null;
522 260
    }
523
524 260
    public function min(string $q): int|float|null|string
525 260
    {
526 260
        $min = $this->queryScalar("MIN($q)");
527
528 225
        return is_numeric($min) ? $min : null;
529
    }
530
531
    public function offset(Expression|int|null $offset): static
532
    {
533
        $this->offset = $offset;
534
535
        return $this;
536
    }
537
538
    public function one(): array|object|null
539
    {
540
        return match ($this->emulateExecution) {
541
            true => null,
542
            false => $this->createCommand()->queryOne(),
543
        };
544
    }
545
546
    public function orderBy(array|string|ExpressionInterface $columns): static
547
    {
548
        $this->orderBy = $this->createQueryHelper()->normalizeOrderBy($columns);
549
550
        return $this;
551
    }
552
553
    public function orFilterHaving(array $condition): static
554
    {
555
        $condition = $this->filterCondition($condition);
556
557
        if ($condition !== []) {
558 225
            $this->orHaving($condition);
559 225
        }
560 15
561
        return $this;
562 220
    }
563
564
    public function orFilterWhere(array $condition): static
565
    {
566
        $condition = $this->filterCondition($condition);
567 260
568 10
        if ($condition !== []) {
569 5
            $this->orWhere($condition);
570 5
        }
571
572
        return $this;
573 5
    }
574 250
575 5
    public function orHaving(array|string|ExpressionInterface $condition, array $params = []): static
576
    {
577 245
        if ($this->having === null) {
578
            $this->having = $condition;
579
        } else {
580
            $this->having = ['or', $this->having, $condition];
581 255
        }
582
583
        $this->addParams($params);
584
585
        return $this;
586
    }
587
588
    public function orWhere(array|string|ExpressionInterface $condition, array $params = []): static
589
    {
590
        if ($this->where === null) {
591 255
            $this->where = $condition;
592
        } else {
593 255
            $this->where = ['or', $this->where, $condition];
594 255
        }
595 250
596
        $this->addParams($params);
597
598 20
        return $this;
599
    }
600
601
    public function params(array $params): static
602
    {
603
        $this->params = $params;
604
605
        return $this;
606
    }
607
608
    /**
609
     * @psalm-suppress MixedArrayOffset
610
     */
611
    public function populate(array $rows): array
612
    {
613
        if ($this->indexBy === null) {
614
            return $rows;
615
        }
616
617
        $result = [];
618
619
        /** @psalm-var array[][] $row */
620
        foreach ($rows as $row) {
621
            $result[ArrayHelper::getValueByPath($row, $this->indexBy)] = $row;
622
        }
623 796
624
        return $result;
625 796
    }
626 796
627
    public function prepare(QueryBuilderInterface $builder): QueryInterface
628 796
    {
629
        return $this;
630
    }
631
632
    public function rightJoin(array|string $table, array|string $on = '', array $params = []): static
633
    {
634
        $this->join[] = ['RIGHT JOIN', $table, $on];
635
636
        return $this->addParams($params);
637
    }
638
639
    public function scalar(): bool|int|null|string|float
640
    {
641
        return match ($this->emulateExecution) {
642
            true => null,
643
            false => $this->createCommand()->queryScalar(),
644
        };
645
    }
646
647
    public function select(array|string|ExpressionInterface $columns, string $option = null): static
648 15
    {
649
        $this->select = $this->createQueryHelper()->normalizeSelect($columns);
650 15
        $this->selectOption = $option;
651
652
        return $this;
653
    }
654 15
655
    public function selectOption(string|null $value): static
656
    {
657
        $this->selectOption = $value;
658 15
659
        return $this;
660 15
    }
661
662
    public function setJoin(array $value): static
663
    {
664
        $this->join = $value;
665
666
        return $this;
667
    }
668
669
    public function setUnion(array $value): static
670 796
    {
671
        $this->union = $value;
672 796
673 5
        return $this;
674 796
    }
675 113
676
    public function shouldEmulateExecution(): bool
677
    {
678 796
        return $this->emulateExecution;
679 796
    }
680 355
681
    public function sum(string $q): int|float|null|string
682 182
    {
683 182
        return match ($this->emulateExecution) {
684
            true => null,
685 348
            false => is_numeric($sum = $this->queryScalar("SUM($q)")) ? $sum : null,
686
        };
687 343
    }
688 343
689 343
    public function union(QueryInterface|string $sql, bool $all = false): static
690
    {
691
        $this->union[] = ['query' => $sql, 'all' => $all];
692 26
693 26
        return $this;
694
    }
695 342
696
    public function where(array|string|ExpressionInterface|null $condition, array $params = []): static
697 286
    {
698 286
        $this->where = $condition;
699
        $this->addParams($params);
700
701
        return $this;
702 106
    }
703
704
    public function withQuery(QueryInterface|string $query, string $alias, bool $recursive = false): static
705 796
    {
706
        $this->withQueries[] = ['query' => $query, 'alias' => $alias, 'recursive' => $recursive];
707
708
        return $this;
709
    }
710
711
    public function withQueries(array $withQueries): static
712
    {
713
        $this->withQueries = $withQueries;
714
715 627
        return $this;
716
    }
717 627
718
    /**
719 627
     * Queries a scalar value by setting {@see select} first.
720
     *
721
     * Restores the value of select to make this query reusable.
722
     *
723
     * @param ExpressionInterface|string $selectExpression
724
     *
725
     * @throws Exception
726
     * @throws InvalidArgumentException
727
     * @throws InvalidConfigException
728
     * @throws NotSupportedException
729
     * @throws Throwable
730
     *
731
     * @return bool|float|int|string|null
732
     *
733
     * @psalm-suppress PossiblyUndefinedVariable
734
     */
735
    protected function queryScalar(string|ExpressionInterface $selectExpression): bool|int|null|string|float
736
    {
737
        if ($this->emulateExecution) {
738
            return null;
739
        }
740
741
        if (
742
            !$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...
743
            && empty($this->groupBy)
744
            && empty($this->having)
745
            && empty($this->union)
746
            && empty($this->with)
747
        ) {
748
            $select = $this->select;
749
            $order = $this->orderBy;
750
            $limit = $this->limit;
751
            $offset = $this->offset;
752
753
            $this->select = [$selectExpression];
754
            $this->orderBy = [];
755
            $this->limit = null;
756
            $this->offset = null;
757
758
            $command = $this->createCommand();
759 970
760
            $this->select = $select;
761 970
            $this->orderBy = $order;
762 16
            $this->limit = $limit;
763
            $this->offset = $offset;
764 970
765 263
            return $command->queryScalar();
766
        }
767 970
768
        $query = (new self($this->db))->select($selectExpression)->from(['c' => $this]);
769 970
        [$sql, $params] = $this->db->getQueryBuilder()->build($query);
770
        $command = $this->db->createCommand($sql, $params);
771
772
        return $command->queryScalar();
773
    }
774
775
    private function createQueryHelper(): QueryHelper
776
    {
777
        if ($this->queryHelper === null) {
778
            $this->queryHelper = new QueryHelper();
779
        }
780
781
        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...
782
    }
783
784
    private function filterCondition(array $condition): array
785
    {
786
        return (array) $this->createQueryHelper()->filterCondition($condition);
787
    }
788
}
789