Passed
Pull Request — master (#685)
by Alexander
02:16
created

Query   F

Complexity

Total Complexity 160

Size/Duplication

Total Lines 830
Duplicated Lines 0 %

Test Coverage

Coverage 95.79%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 332
dl 0
loc 830
rs 2
c 2
b 0
f 0
ccs 205
cts 214
cp 0.9579
wmc 160

74 Methods

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