Passed
Pull Request — master (#540)
by Def
11:32
created

Query   F

Complexity

Total Complexity 161

Size/Duplication

Total Lines 858
Duplicated Lines 0 %

Test Coverage

Coverage 95.79%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 325
dl 0
loc 858
ccs 205
cts 214
cp 0.9579
rs 2
c 1
b 0
f 0
wmc 161

75 Methods

Rating   Name   Duplication   Size   Complexity  
A getUnion() 0 3 1
A getLimit() 0 3 1
A emulateExecution() 0 5 1
A getWhere() 0 3 1
A getOffset() 0 3 1
A orderBy() 0 5 1
A exists() 0 12 2
A from() 0 13 3
A getJoin() 0 3 1
A getGroupBy() 0 3 1
A distinct() 0 5 1
A getWithQueries() 0 3 1
A batch() 0 3 1
A orFilterHaving() 0 9 2
A getSelectOption() 0 3 1
A filterWhere() 0 9 2
A each() 0 3 1
A getIndexBy() 0 3 1
A getParams() 0 3 1
A limit() 0 5 1
A params() 0 5 1
A having() 0 6 1
A average() 0 5 2
A getDistinct() 0 3 1
A getSelect() 0 3 1
B column() 0 41 11
A orHaving() 0 11 2
A indexBy() 0 5 1
A groupBy() 0 10 3
A getHaving() 0 3 1
A offset() 0 5 1
A one() 0 5 1
A max() 0 5 2
A leftJoin() 0 5 1
A count() 0 5 2
A innerJoin() 0 5 1
A getFrom() 0 3 1
A getTablesUsedInFrom() 0 3 1
A orFilterWhere() 0 9 2
A min() 0 5 2
A filterHaving() 0 9 2
A orWhere() 0 11 2
A createCommand() 0 4 1
A join() 0 5 1
A getOrderBy() 0 3 1
A addOrderBy() 0 11 2
A addGroupBy() 0 15 4
A __toString() 0 3 1
A __construct() 0 2 1
A andFilterHaving() 0 9 2
A andFilterCompare() 0 10 2
A andHaving() 0 11 2
A addSelect() 0 9 2
A andWhere() 0 13 5
A addParams() 0 21 5
A andFilterWhere() 0 9 2
A isEmpty() 0 3 5
A where() 0 6 1
D filterCondition() 0 61 19
A select() 0 6 1
A shouldEmulateExecution() 0 3 1
A setJoin() 0 5 1
B normalizeSelect() 0 41 10
B queryScalar() 0 38 7
A normalizeOrderBy() 0 22 6
A setUnion() 0 5 1
A all() 0 7 2
A prepare() 0 3 1
A rightJoin() 0 5 1
A sum() 0 5 2
A scalar() 0 5 1
A withQuery() 0 5 1
A union() 0 5 1
A withQueries() 0 5 1
A selectOption() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like Query often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Query, and based on these observations, apply Extract Interface, too.

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