Passed
Pull Request — master (#515)
by Wilmer
02:12
created

Query::populate()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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

2 Methods

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