Passed
Pull Request — master (#644)
by Wilmer
02:19
created

Query::__toString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
ccs 0
cts 0
cp 0
cc 1
nc 1
nop 0
crap 2
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 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
            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
241 432
            ->createBatchQueryResult($this)
242
            ->batchSize($batchSize)
243 432
            ->setPopulatedMethod(fn (array $rows, Closure|string|null $indexBy = null): array => ArrayHelper::populate($rows, $indexBy))
244 10
        ;
245
    }
246
247 422
    public function column(): array
248
    {
249
        if ($this->emulateExecution) {
250
            return [];
251
        }
252
253
        if ($this->indexBy === null) {
254
            return $this->createCommand()->queryColumn();
255
        }
256
257
        if (is_string($this->indexBy) && count($this->select) === 1) {
258
            if (!str_contains($this->indexBy, '.') && count($tables = $this->getTablesUsedInFrom()) > 0) {
259
                $this->select[] = key($tables) . '.' . $this->indexBy;
260 25
            } else {
261
                $this->select[] = $this->indexBy;
262 25
            }
263 10
        }
264
265
        $rows = $this->createCommand()->queryAll();
266 15
        $results = [];
267
        $column = null;
268
269
        if (is_string($this->indexBy)) {
270
            if (($dotPos = strpos($this->indexBy, '.')) === false) {
271
                $column = $this->indexBy;
272
            } else {
273
                $column = substr($this->indexBy, $dotPos + 1);
274
            }
275
        }
276
277
        /** @psalm-var array<array-key, array<string, string>> $rows */
278 25
        foreach ($rows as $row) {
279
            $value = reset($row);
280 25
281 10
            if ($this->indexBy instanceof Closure) {
282
                /** @psalm-suppress MixedArrayOffset */
283
                $results[($this->indexBy)($row)] = $value;
284 15
            } else {
285 15
                $results[$row[$column] ?? $row[$this->indexBy]] = $value;
286
            }
287
        }
288 5
289 5
        return $results;
290 5
    }
291
292
    public function count(string $q = '*'): int|string
293
    {
294
        return match ($this->emulateExecution) {
295
            true => 0,
296 5
            false => is_numeric($count = $this->queryScalar("COUNT($q)")) ? (int) $count : 0,
297 5
        };
298 5
    }
299 5
300
    public function createCommand(): CommandInterface
301 5
    {
302 5
        [$sql, $params] = $this->db->getQueryBuilder()->build($this);
303
        return $this->db->createCommand($sql, $params);
304 5
    }
305
306
    public function distinct(bool|null $value = true): static
307
    {
308 5
        $this->distinct = $value;
309
        return $this;
310
    }
311
312
    public function each(int $batchSize = 100): BatchQueryResultInterface
313
    {
314
        return $this->db
315
            ->createBatchQueryResult($this, true)
316
            ->batchSize($batchSize)
317
            ->setPopulatedMethod(fn (array $rows, Closure|string|null $indexBy = null): array => ArrayHelper::populate($rows, $indexBy))
318
        ;
319
    }
320
321
    public function exists(): bool
322 95
    {
323
        if ($this->emulateExecution) {
324 95
            return false;
325 10
        }
326
327
        $command = $this->createCommand();
328 95
        $params = $command->getParams();
329
        $command->setSql($this->db->getQueryBuilder()->selectExists($command->getSql()));
330
        $command->bindValues($params);
331
332
        return (bool) $command->queryScalar();
333
    }
334
335
    public function emulateExecution(bool $value = true): static
336
    {
337
        $this->emulateExecution = $value;
338
        return $this;
339
    }
340
341 15
    public function filterHaving(array $condition): static
342
    {
343 15
        $condition = $this->filterCondition($condition);
344 10
345
        if ($condition !== []) {
346
            $this->having($condition);
347 5
        }
348
349
        return $this;
350
    }
351
352
    public function filterWhere(array $condition): static
353
    {
354
        $condition = $this->filterCondition($condition);
355
356
        if ($condition !== []) {
357
            $this->where($condition);
358
        }
359
360 15
        return $this;
361
    }
362 15
363 10
    public function from(array|ExpressionInterface|string $tables): static
364
    {
365
        if ($tables instanceof ExpressionInterface) {
0 ignored issues
show
introduced by
$tables is never a sub-type of Yiisoft\Db\Expression\ExpressionInterface.
Loading history...
366 5
            $tables = [$tables];
367
        }
368
369
        if (is_string($tables)) {
0 ignored issues
show
introduced by
The condition is_string($tables) is always false.
Loading history...
370
            $tables = preg_split('/\s*,\s*/', trim($tables), -1, PREG_SPLIT_NO_EMPTY);
371
        }
372
373
        $this->from = $tables;
374
375
        return $this;
376
    }
377
378
    public function getDistinct(): bool|null
379 15
    {
380
        return $this->distinct;
381 15
    }
382
383
    public function getFrom(): array
384
    {
385
        return $this->from;
386
    }
387
388
    public function getGroupBy(): array
389
    {
390
        return $this->groupBy;
391
    }
392
393
    public function getHaving(): string|array|ExpressionInterface|null
394 15
    {
395
        return $this->having;
396 15
    }
397
398
    public function getIndexBy(): Closure|string|null
399
    {
400
        return $this->indexBy;
401
    }
402
403
    public function getJoin(): array
404
    {
405
        return $this->join;
406 25
    }
407
408 25
    public function getLimit(): ExpressionInterface|int|null
409 10
    {
410
        return $this->limit;
411
    }
412 15
413 15
    public function getOffset(): ExpressionInterface|int|null
414 15
    {
415 15
        return $this->offset;
416
    }
417 15
418
    public function getOrderBy(): array
419
    {
420
        return $this->orderBy;
421
    }
422
423
    public function getParams(): array
424
    {
425
        return $this->params;
426
    }
427
428
    public function getSelect(): array
429
    {
430
        return $this->select;
431 95
    }
432
433 95
    public function getSelectOption(): string|null
434 10
    {
435
        return $this->selectOption;
436
    }
437
438 95
    public function getTablesUsedInFrom(): array
439 95
    {
440 95
        return $this->db->getQuoter()->cleanUpTableNames($this->from);
441 95
    }
442 95
443
    public function getUnion(): array
444 95
    {
445 95
        return $this->union;
446 95
    }
447 95
448
    public function getWhere(): array|string|ExpressionInterface|null
449 95
    {
450 95
        return $this->where;
451 95
    }
452 95
453
    public function getWithQueries(): array
454
    {
455 95
        return $this->withQueries;
456
    }
457
458
    public function groupBy(array|string|ExpressionInterface $columns): static
459
    {
460 95
        if ($columns instanceof ExpressionInterface) {
0 ignored issues
show
introduced by
$columns is never a sub-type of Yiisoft\Db\Expression\ExpressionInterface.
Loading history...
461 95
            $columns = [$columns];
462 95
        } elseif (!is_array($columns)) {
0 ignored issues
show
introduced by
The condition is_array($columns) is always true.
Loading history...
463 95
            $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
464
        }
465 95
        $this->groupBy = $columns;
466
467
        return $this;
468
    }
469 95
470
    public function having(array|ExpressionInterface|string|null $condition, array $params = []): static
471
    {
472 10
        $this->having = $condition;
473 10
        $this->addParams($params);
474 10
        return $this;
475 10
    }
476
477 10
    public function indexBy(Closure|string|null $column): static
478
    {
479 10
        $this->indexBy = $column;
480
        return $this;
481
    }
482
483
    public function innerJoin(array|string $table, array|string $on = '', array $params = []): static
484
    {
485
        $this->join[] = ['INNER JOIN', $table, $on];
486
        return $this->addParams($params);
487
    }
488
489
    public function join(string $type, array|string $table, array|string $on = '', array $params = []): static
490
    {
491
        $this->join[] = [$type, $table, $on];
492 110
        return $this->addParams($params);
493
    }
494 110
495
    public function leftJoin(array|string $table, array|string $on = '', array $params = []): static
496
    {
497
        $this->join[] = ['LEFT JOIN', $table, $on];
498 110
        return $this->addParams($params);
499 105
    }
500 5
501
    public function limit(ExpressionInterface|int|null $limit): static
502 5
    {
503
        $this->limit = $limit;
504
        return $this;
505 5
    }
506
507
    public function max(string $q): int|float|null|string
508 105
    {
509
        $max = $this->queryScalar("MAX($q)");
510
        return is_numeric($max) ? $max : null;
511
    }
512
513
    public function min(string $q): int|float|null|string
514
    {
515
        $min = $this->queryScalar("MIN($q)");
516
        return is_numeric($min) ? $min : null;
517
    }
518
519
    public function offset(ExpressionInterface|int|null $offset): static
520
    {
521
        $this->offset = $offset;
522 260
        return $this;
523
    }
524 260
525 260
    public function one(): array|null
526 260
    {
527
        return match ($this->emulateExecution) {
528 225
            true => null,
529
            false => $this->createCommand()->queryOne(),
530
        };
531
    }
532
533
    public function orderBy(array|string|ExpressionInterface $columns): static
534
    {
535
        $this->orderBy = $this->normalizeOrderBy($columns);
536
        return $this;
537
    }
538
539
    public function orFilterHaving(array $condition): static
540
    {
541
        $condition = $this->filterCondition($condition);
542
543
        if ($condition !== []) {
544
            $this->orHaving($condition);
545
        }
546
547
        return $this;
548
    }
549
550
    public function orFilterWhere(array $condition): static
551
    {
552
        $condition = $this->filterCondition($condition);
553
554
        if ($condition !== []) {
555
            $this->orWhere($condition);
556
        }
557
558 225
        return $this;
559 225
    }
560 15
561
    public function orHaving(array|string|ExpressionInterface $condition, array $params = []): static
562 220
    {
563
        if ($this->having === null) {
564
            $this->having = $condition;
565
        } else {
566
            $this->having = ['or', $this->having, $condition];
567 260
        }
568 10
569 5
        $this->addParams($params);
570 5
571
        return $this;
572
    }
573 5
574 250
    public function orWhere(array|string|ExpressionInterface $condition, array $params = []): static
575 5
    {
576
        if ($this->where === null) {
577 245
            $this->where = $condition;
578
        } else {
579
            $this->where = ['or', $this->where, $condition];
580
        }
581 255
582
        $this->addParams($params);
583
584
        return $this;
585
    }
586
587
    public function params(array $params): static
588
    {
589
        $this->params = $params;
590
        return $this;
591 255
    }
592
593 255
    public function prepare(QueryBuilderInterface $builder): QueryInterface
594 255
    {
595 250
        return $this;
596
    }
597
598 20
    public function rightJoin(array|string $table, array|string $on = '', array $params = []): static
599
    {
600
        $this->join[] = ['RIGHT JOIN', $table, $on];
601
        return $this->addParams($params);
602
    }
603
604
    public function scalar(): bool|int|null|string|float
605
    {
606
        return match ($this->emulateExecution) {
607
            true => null,
608
            false => $this->createCommand()->queryScalar(),
609
        };
610
    }
611
612
    public function select(array|string|ExpressionInterface $columns, string $option = null): static
613
    {
614
        $this->select = $this->normalizeSelect($columns);
615
        $this->selectOption = $option;
616
        return $this;
617
    }
618
619
    public function selectOption(string|null $value): static
620
    {
621
        $this->selectOption = $value;
622
        return $this;
623 796
    }
624
625 796
    public function setJoin(array $value): static
626 796
    {
627
        $this->join = $value;
628 796
        return $this;
629
    }
630
631
    public function setUnion(array $value): static
632
    {
633
        $this->union = $value;
634
        return $this;
635
    }
636
637
    public function shouldEmulateExecution(): bool
638
    {
639
        return $this->emulateExecution;
640
    }
641
642
    public function sum(string $q): int|float|null|string
643
    {
644
        return match ($this->emulateExecution) {
645
            true => null,
646
            false => is_numeric($sum = $this->queryScalar("SUM($q)")) ? $sum : null,
647
        };
648 15
    }
649
650 15
    public function union(QueryInterface|string $sql, bool $all = false): static
651
    {
652
        $this->union[] = ['query' => $sql, 'all' => $all];
653
        return $this;
654 15
    }
655
656
    public function where(array|string|ExpressionInterface|null $condition, array $params = []): static
657
    {
658 15
        $this->where = $condition;
659
        $this->addParams($params);
660 15
        return $this;
661
    }
662
663
    public function withQuery(QueryInterface|string $query, string $alias, bool $recursive = false): static
664
    {
665
        $this->withQueries[] = ['query' => $query, 'alias' => $alias, 'recursive' => $recursive];
666
        return $this;
667
    }
668
669
    public function withQueries(array $withQueries): static
670 796
    {
671
        $this->withQueries = $withQueries;
672 796
        return $this;
673 5
    }
674 796
675 113
    /**
676
     * Queries a scalar value by setting {@see select()} first.
677
     *
678 796
     * Restores the value of select to make this query reusable.
679 796
     *
680 355
     * @param ExpressionInterface|string $selectExpression
681
     *
682 182
     * @throws Exception
683 182
     * @throws InvalidArgumentException
684
     * @throws InvalidConfigException
685 348
     * @throws NotSupportedException
686
     * @throws Throwable
687 343
     */
688 343
    protected function queryScalar(string|ExpressionInterface $selectExpression): bool|int|null|string|float
689 343
    {
690
        if ($this->emulateExecution) {
691
            return null;
692 26
        }
693 26
694
        if (
695 342
            !$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...
696
            && empty($this->groupBy)
697 286
            && empty($this->having)
698 286
            && empty($this->union)
699
            && empty($this->with)
700
        ) {
701
            $select = $this->select;
702 106
            $order = $this->orderBy;
703
            $limit = $this->limit;
704
            $offset = $this->offset;
705 796
706
            $this->select = [$selectExpression];
707
            $this->orderBy = [];
708
            $this->limit = null;
709
            $this->offset = null;
710
711
            $command = $this->createCommand();
712
713
            $this->select = $select;
714
            $this->orderBy = $order;
715 627
            $this->limit = $limit;
716
            $this->offset = $offset;
717 627
718
            return $command->queryScalar();
719 627
        }
720
721
        $query = (new self($this->db))->select($selectExpression)->from(['c' => $this]);
722
        [$sql, $params] = $this->db->getQueryBuilder()->build($query);
723
        $command = $this->db->createCommand($sql, $params);
724
725
        return $command->queryScalar();
726
    }
727
728
    /**
729
     * Removes {@see Query::isEmpty()} from the given query condition.
730
     *
731
     * @param array|string $condition The original condition.
732
     *
733
     * @return array|string The condition with {@see Query::isEmpty()} removed.
734
     */
735
    private function filterCondition(array|string $condition): array|string
736
    {
737
        if (!is_array($condition)) {
0 ignored issues
show
introduced by
The condition is_array($condition) is always true.
Loading history...
738
            return $condition;
739
        }
740
741
        if (!isset($condition[0])) {
742
            /**
743
             * Hash format: 'column1' => 'value1', 'column2' => 'value2', ...
744
             *
745
             * @psalm-var mixed $value
746
             */
747
            foreach ($condition as $name => $value) {
748
                if ($this->isEmpty($value)) {
749
                    unset($condition[$name]);
750
                }
751
            }
752
753
            return $condition;
754
        }
755
756
        /**
757
         * Operator format: operator, operand 1, operand 2, ...
758
         *
759 970
         * @psalm-var string $operator
760
         */
761 970
        $operator = array_shift($condition);
762 16
763
        switch (strtoupper($operator)) {
764 970
            case 'NOT':
765 263
            case 'AND':
766
            case 'OR':
767 970
                /** @psalm-var array<array-key, array|string> $condition */
768
                foreach ($condition as $i => $operand) {
769 970
                    $subCondition = $this->filterCondition($operand);
770
                    if ($this->isEmpty($subCondition)) {
771
                        unset($condition[$i]);
772
                    } else {
773
                        $condition[$i] = $subCondition;
774
                    }
775
                }
776
777
                if (empty($condition)) {
778
                    return [];
779
                }
780
781
                break;
782
            case 'BETWEEN':
783
            case 'NOT BETWEEN':
784
                if (array_key_exists(1, $condition) && array_key_exists(2, $condition)) {
785
                    if ($this->isEmpty($condition[1]) || $this->isEmpty($condition[2])) {
786
                        return [];
787
                    }
788
                } else {
789
                    return [];
790
                }
791 1375
792
                break;
793 1375
            default:
794 1375
                if (array_key_exists(1, $condition) && $this->isEmpty($condition[1])) {
795
                    return [];
796 1375
                }
797
        }
798
799
        array_unshift($condition, $operator);
800
801
        return $condition;
802
    }
803
804
    /**
805
     * Returns a value indicating whether the give value is "empty".
806
     *
807
     * The value is "empty" if one of the following conditions is satisfied:
808
     *
809
     * - It's `null`,
810
     * - an empty string (`''`),
811
     * - a string containing only space characters,
812
     * - or an empty array.
813 348
     *
814
     * @param mixed $value The value to check.
815 348
     *
816 323
     * @return bool If the value is empty.
817 131
     */
818 6
    private function isEmpty(mixed $value): bool
819
    {
820 131
        return $value === '' || $value === [] || $value === null || (is_string($value) && trim($value) === '');
821
    }
822
823 348
    /**
824
     * Normalizes a format of `ORDER BY` data.
825 348
     *
826
     * @param array|ExpressionInterface|string $columns The columns value to normalize.
827
     *
828
     * See {@see orderBy()} and {@see addOrderBy()}.
829
     */
830
    private function normalizeOrderBy(array|string|ExpressionInterface $columns): array
831
    {
832
        if ($columns instanceof ExpressionInterface) {
0 ignored issues
show
introduced by
$columns is never a sub-type of Yiisoft\Db\Expression\ExpressionInterface.
Loading history...
833
            return [$columns];
834
        }
835
836
        if (is_array($columns)) {
0 ignored issues
show
introduced by
The condition is_array($columns) is always true.
Loading history...
837
            return $columns;
838
        }
839
840
        $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
841
        $result = [];
842 6
843
        foreach ($columns as $column) {
844 6
            if (preg_match('/^(.*?)\s+(asc|desc)$/i', $column, $matches)) {
845
                $result[$matches[1]] = strcasecmp($matches[2], 'desc') ? SORT_ASC : SORT_DESC;
846
            } else {
847 6
                $result[$column] = SORT_ASC;
848
            }
849
        }
850 6
851
        return $result;
852 6
    }
853
854
    /**
855
     * Normalizes the `SELECT` columns passed to {@see select()} or {@see addSelect()}.
856
     */
857
    private function normalizeSelect(array|ExpressionInterface|string $columns): array
858
    {
859
        if ($columns instanceof ExpressionInterface) {
0 ignored issues
show
introduced by
$columns is never a sub-type of Yiisoft\Db\Expression\ExpressionInterface.
Loading history...
860
            $columns = [$columns];
861
        } elseif (!is_array($columns)) {
0 ignored issues
show
introduced by
The condition is_array($columns) is always true.
Loading history...
862
            $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
863
        }
864
865
        $select = [];
866
867
        /** @psalm-var array<array-key, ExpressionInterface|string> $columns */
868
        foreach ($columns as $columnAlias => $columnDefinition) {
869
            if (is_string($columnAlias)) {
870
                // Already in the normalized format, good for them.
871
                $select[$columnAlias] = $columnDefinition;
872
                continue;
873
            }
874
875
            if (is_string($columnDefinition)) {
876
                if (
877
                    preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-_.]+)$/', $columnDefinition, $matches) &&
878
                    !preg_match('/^\d+$/', $matches[2]) &&
879
                    !str_contains($matches[2], '.')
880
                ) {
881
                    /** Using "columnName as alias" or "columnName alias" syntax */
882
                    $select[$matches[2]] = $matches[1];
883
                    continue;
884 5
                }
885
                if (!str_contains($columnDefinition, '(')) {
886 5
                    /** Normal column name, just alias it to itself to ensure it's not selected twice */
887 5
                    $select[$columnDefinition] = $columnDefinition;
888 5
                    continue;
889
                }
890 5
            }
891
892
            // Either a string calling a function, DB expression, or sub-query
893 5
            /** @psalm-var string */
894
            $select[] = $columnDefinition;
895
        }
896
897
        return $select;
898
    }
899
}
900