Passed
Pull Request — master (#540)
by Def
02:18
created

Query::populate()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 6
nc 3
nop 1
dl 0
loc 15
ccs 5
cts 5
cp 1
crap 3
rs 10
c 0
b 0
f 0

2 Methods

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