Issues (43)

src/Query/Query.php (11 issues)

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

871
            'string' => preg_split('/\s*,\s*/', trim(/** @scrutinizer ignore-type */ $columns), -1, PREG_SPLIT_NO_EMPTY),
Loading history...
872
            default => [$columns],
873
        };
874
875
        $select = [];
876
877
        foreach ($columns as $columnAlias => $columnDefinition) {
878
            if (is_string($columnAlias)) {
879
                // Already in the normalized format, good for them.
880
                $select[$columnAlias] = $columnDefinition;
881
                continue;
882
            }
883
884 5
            if (is_string($columnDefinition)) {
885
                if (
886 5
                    preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-_.]+)$/', $columnDefinition, $matches) &&
887 5
                    !preg_match('/^\d+$/', $matches[2]) &&
888 5
                    !str_contains($matches[2], '.')
889
                ) {
890 5
                    /** Using "columnName as alias" or "columnName alias" syntax */
891
                    $select[$matches[2]] = $matches[1];
892
                    continue;
893 5
                }
894
                if (!str_contains($columnDefinition, '(')) {
895
                    /** Normal column name, just alias it to itself to ensure it's not selected twice */
896
                    $select[$columnDefinition] = $columnDefinition;
897
                    continue;
898
                }
899
            }
900
901
            // Either a string calling a function, instance of ExpressionInterface or a scalar value.
902
            $select[] = $columnDefinition;
903
        }
904
905
        return $select;
906
    }
907
}
908