Passed
Pull Request — master (#677)
by Wilmer
04:11 queued 01:47
created

Query::filterHaving()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

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