Passed
Pull Request — master (#266)
by Def
11:38
created

ActiveQuery::removeDuplicatedModels()   B

Complexity

Conditions 10
Paths 5

Size

Total Lines 49
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 10.1167

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 10
eloc 27
c 2
b 0
f 0
nc 5
nop 1
dl 0
loc 49
ccs 17
cts 19
cp 0.8947
crap 10.1167
rs 7.6666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\ActiveRecord;
6
7
use Closure;
8
use ReflectionException;
9
use Throwable;
10
use Yiisoft\Db\Command\CommandInterface;
11
use Yiisoft\Db\Connection\ConnectionInterface;
12
use Yiisoft\Db\Exception\Exception;
13
use Yiisoft\Db\Exception\InvalidArgumentException;
14
use Yiisoft\Db\Exception\InvalidConfigException;
15
use Yiisoft\Db\Exception\NotSupportedException;
16
use Yiisoft\Db\Expression\ExpressionInterface;
17
use Yiisoft\Db\Helper\DbArrayHelper;
18
use Yiisoft\Db\Query\BatchQueryResultInterface;
19
use Yiisoft\Db\Query\Query;
20
use Yiisoft\Db\Query\QueryInterface;
21
use Yiisoft\Db\QueryBuilder\QueryBuilderInterface;
22
use Yiisoft\Definitions\Exception\CircularReferenceException;
23
use Yiisoft\Definitions\Exception\NotInstantiableException;
24
use Yiisoft\Factory\NotFoundException;
25
26
use function array_merge;
27
use function array_values;
28
use function count;
29
use function implode;
30
use function in_array;
31
use function is_array;
32
use function is_int;
33
use function is_string;
34
use function preg_match;
35
use function reset;
36
use function serialize;
37
use function strpos;
38
use function substr;
39
40
/**
41
 * Represents a db query associated with an Active Record class.
42
 *
43
 * An ActiveQuery can be a normal query or be used in a relational context.
44
 *
45
 * ActiveQuery instances are usually created by {@see findOne()}, {@see findBySql()}, {@see findAll()}.
46
 *
47
 * Relational queries are created by {@see ActiveRecord::hasOne()} and {@see ActiveRecord::hasMany()}.
48
 *
49
 * Normal Query
50
 * ------------
51
 *
52
 * ActiveQuery mainly provides the following methods to retrieve the query results:
53
 *
54
 * - {@see one()}: returns a single record populated with the first row of data.
55
 * - {@see all()}: returns all records based on the query results.
56
 * - {@see count()}: returns the number of records.
57
 * - {@see sum()}: returns the sum over the specified column.
58
 * - {@see average()}: returns the average over the specified column.
59
 * - {@see min()}: returns the min over the specified column.
60
 * - {@see max()}: returns the max over the specified column.
61
 * - {@see scalar()}: returns the value of the first column in the first row of the query result.
62
 * - {@see column()}: returns the value of the first column in the query result.
63
 * - {@see exists()}: returns a value indicating whether the query result has data or not.
64
 *
65
 * Because ActiveQuery extends from {@see Query}, one can use query methods, such as {@see where()}, {@see orderBy()} to
66
 * customize the query options.
67
 *
68
 * ActiveQuery also provides the following more query options:
69
 *
70
 * - {@see with()}: list of relations that this query should be performed with.
71
 * - {@see joinWith()}: reuse a relation query definition to add a join to a query.
72
 * - {@see indexBy()}: the name of the column by which the query result should be indexed.
73
 * - {@see asArray()}: whether to return each record as an array.
74
 *
75
 * These options can be configured using methods of the same name. For example:
76
 *
77
 * ```php
78
 * $customerQuery = new ActiveQuery(Customer::class, $db);
79
 * $query = $customerQuery->with('orders')->asArray()->all();
80
 * ```
81
 *
82
 * Relational query
83
 * ----------------
84
 *
85
 * In relational context ActiveQuery represents a relation between two Active Record classes.
86
 *
87
 * Relational ActiveQuery instances are usually created by calling {@see ActiveRecord::hasOne()} and
88
 * {@see ActiveRecord::hasMany()}. An Active Record class declares a relation by defining a getter method which calls
89
 * one of the above methods and returns the created ActiveQuery object.
90
 *
91
 * A relation is specified by {@see link()} which represents the association between columns of different tables; and
92
 * the multiplicity of the relation is indicated by {@see multiple()}.
93
 *
94
 * If a relation involves a junction table, it may be specified by {@see via()} or {@see viaTable()} method.
95
 *
96
 * These methods may only be called in a relational context. Same is true for {@see inverseOf()}, which marks a relation
97
 * as inverse of another relation and {@see onCondition()} which adds a condition that's to be added to relational
98
 * query join condition.
99
 */
100
class ActiveQuery extends Query implements ActiveQueryInterface
101
{
102
    use ActiveQueryTrait;
103
    use ActiveRelationTrait;
104
105
    private string|null $sql = null;
106
    private array|string|null $on = null;
107
    private array $joinWith = [];
108
    private ActiveRecordInterface|null $arInstance = null;
109 720
110
    final public function __construct(
111 720
        protected string $arClass,
112 720
        protected ConnectionInterface $db,
113 720
        private ActiveRecordFactory|null $arFactory = null,
114
        private string $tableName = ''
115 720
    ) {
116 720
        parent::__construct($db);
117
    }
118
119
    /**
120
     * Executes a query and returns all results as an array.
121
     *
122
     * If null, the db connection returned by {@see arClass} will be used.
123
     *
124
     * @throws Exception
125
     * @throws InvalidConfigException
126
     * @throws Throwable
127 244
     *
128
     * @return array The query results. If the query results in nothing, an empty array will be returned.
129 244
     *
130
     * @psalm-return ActiveRecord[]|array
131
     */
132
    public function all(): array
133
    {
134
        if ($this->shouldEmulateExecution()) {
135
            return [];
136
        }
137
138
        return $this->populate($this->createCommand()->queryAll(), $this->indexBy);
139
    }
140
141
    public function batch(int $batchSize = 100): BatchQueryResultInterface
142
    {
143
        return parent::batch($batchSize)->setPopulatedMethod(
144
            fn (array $rows, null|Closure|string $indexBy) => $this->populate($rows, $indexBy)
145
        );
146 497
    }
147
148
    public function each(int $batchSize = 100): BatchQueryResultInterface
149
    {
150
        return parent::each($batchSize)->setPopulatedMethod(
151
            fn (array $rows, null|Closure|string $indexBy) => $this->populate($rows, $indexBy)
152
        );
153 497
    }
154 80
155
    /**
156 80
     * @throws CircularReferenceException
157
     * @throws Exception
158
     * @throws InvalidConfigException
159 497
     * @throws NotFoundException
160 489
     * @throws NotInstantiableException
161
     * @throws Throwable
162
     * @throws \Yiisoft\Definitions\Exception\InvalidConfigException
163 497
     */
164 76
    public function prepare(QueryBuilderInterface $builder): QueryInterface
165
    {
166 76
        /**
167
         * NOTE: Because the same ActiveQuery may be used to build different SQL statements, one for count query, the
168
         * other for row data query, it's important to make sure the same ActiveQuery can be used to build SQL
169 497
         * statements many times.
170
         */
171 489
        if (!empty($this->joinWith)) {
172
            $this->buildJoinWith();
173
            /**
174 113
             * Clean it up to avoid issue @link https://github.com/yiisoft/yii2/issues/2687
175
             */
176 113
            $this->joinWith = [];
177
        }
178 20
179
        if (empty($this->getFrom())) {
180 20
            $this->from = [$this->getPrimaryTableName()];
181 101
        }
182
183
        if (empty($this->getSelect()) && !empty($this->getJoins())) {
184
            [, $alias] = $this->getTableNameAndAlias();
185
186
            $this->select(["$alias.*"]);
187 28
        }
188
189 28
        if ($this->primaryModel === null) {
190 28
            $query = $this->createInstance();
191 20
        } else {
192 8
            $where = $this->getWhere();
193
194
            if ($this->via instanceof self) {
195 8
                /** via junction table */
196 28
                $viaModels = $this->via->findJunctionRows([$this->primaryModel]);
197
198
                $this->filterByModels($viaModels);
199
            } elseif (is_array($this->via)) {
200
                [$viaName, $viaQuery, $viaCallableUsed] = $this->via;
201
202
                if ($viaQuery->getMultiple()) {
203
                    if ($viaCallableUsed) {
204
                        $viaModels = $viaQuery->all();
205
                    } elseif ($this->primaryModel->isRelationPopulated($viaName)) {
206
                        $viaModels = $this->primaryModel->$viaName;
207
                    } else {
208
                        $viaModels = $viaQuery->all();
209 28
                        $this->primaryModel->populateRelation($viaName, $viaModels);
210
                    }
211 101
                } else {
212
                    if ($viaCallableUsed) {
213
                        $model = $viaQuery->onePopulate();
214 113
                    } elseif ($this->primaryModel->isRelationPopulated($viaName)) {
215 113
                        $model = $this->primaryModel->$viaName;
216
                    } else {
217
                        $model = $viaQuery->onePopulate();
218 497
                        $this->primaryModel->populateRelation($viaName, $model);
219 24
                    }
220
                    $viaModels = $model === null ? [] : [$model];
221
                }
222 497
                $this->filterByModels($viaModels);
223
            } else {
224
                $this->filterByModels([$this->primaryModel]);
225
            }
226
227
            $query = $this->createInstance();
228
            $this->where($where);
229
        }
230
231
        if (!empty($this->on)) {
232
            $query->andWhere($this->on);
233
        }
234
235
        return $query;
236
    }
237 419
238
    /**
239 419
     * @throws Exception
240 74
     * @throws InvalidArgumentException
241
     * @throws InvalidConfigException
242
     * @throws NotSupportedException
243 406
     * @throws ReflectionException
244
     * @throws Throwable
245 406
     */
246 64
    public function populate(array $rows, Closure|string|null $indexBy = null): array
247
    {
248
        if (empty($rows)) {
249 406
            return [];
250 131
        }
251
252
        $models = $this->createModels($rows);
253 406
254 16
        if (empty($models)) {
255
            return [];
256
        }
257 406
258
        if (!empty($this->join) && $this->getIndexBy() === null) {
259
            $models = $this->removeDuplicatedModels($models);
260
        }
261
262
        if (!empty($this->with)) {
263
            $this->findWith($this->with, $models);
264
        }
265
266
        if ($this->inverseOf !== null) {
267
            $this->addInverseRelations($models);
268
        }
269
270
        return DbArrayHelper::populate($models, $indexBy);
271 64
    }
272
273 64
    /**
274
     * Removes duplicated models by checking their primary key values.
275 64
     *
276
     * This method is mainly called when a join query is performed, which may cause duplicated rows being returned.
277 64
     *
278
     * @param array $models The models to be checked.
279 8
     *
280 8
     * @throws CircularReferenceException
281 8
     * @throws Exception
282 8
     * @throws InvalidConfigException
283
     * @throws NotFoundException
284 4
     * @throws NotInstantiableException
285
     *
286 7
     * @return array The distinctive models.
287
     */
288
    private function removeDuplicatedModels(array $models): array
289 4
    {
290
        $hash = [];
291 4
292
        $pks = $this->getARInstance()->primaryKey();
293
294 4
        if (count($pks) > 1) {
295
            // Composite primary key.
296
            foreach ($models as $i => $model) {
297 60
                $key = [];
298
                foreach ($pks as $pk) {
299
                    if (!isset($model[$pk])) {
300
                        // Don't continue if the primary key isn't part of the result set.
301 60
                        break 2;
302
                    }
303 60
                    $key[] = $model[$pk];
304 60
                }
305
306 4
                $key = serialize($key);
307
308
                if (isset($hash[$key])) {
309 56
                    unset($models[$i]);
310
                } else {
311 56
                    $hash[$key] = true;
312 28
                }
313 56
            }
314 56
        } elseif (empty($pks)) {
315
            throw new InvalidConfigException("Primary key of '$this->arClass' can not be empty.");
316
        } else {
317
            // Single column primary key.
318
            $pk = reset($pks);
319 64
320
            foreach ($models as $i => $model) {
321
                if (!isset($model[$pk])) {
322
                    // Don't continue if the primary key isn't part of the result set.
323
                    break;
324
                }
325
326
                $key = $model[$pk];
327
328
                if (isset($hash[$key])) {
329
                    unset($models[$i]);
330
                } else {
331
                    $hash[$key] = true;
332 305
                }
333
            }
334 305
        }
335
336 305
        return array_values($models);
337 301
    }
338
339 301
    /**
340
     * @throws Exception
341
     * @throws InvalidArgumentException
342 32
     * @throws InvalidConfigException
343
     * @throws NotSupportedException
344
     * @throws ReflectionException
345
     * @throws Throwable
346
     */
347
    public function allPopulate(): array
348
    {
349
        $rows = $this->all();
350
351
        if ($rows !== []) {
352 449
            $rows = $this->populate($rows, $this->indexBy);
353
        }
354 449
355 445
        return $rows;
356
    }
357 4
358 4
    /**
359
     * @throws Exception
360
     * @throws InvalidArgumentException
361 449
     * @throws InvalidConfigException
362
     * @throws NotSupportedException
363 449
     * @throws ReflectionException
364
     * @throws Throwable
365 449
     */
366
    public function onePopulate(): array|ActiveRecordInterface|null
367
    {
368
        $row = $this->one();
369
370
        if ($row !== null) {
371
            $activeRecord = $this->populate([$row], $this->indexBy);
372
            $row = reset($activeRecord) ?: null;
373
        }
374
375
        return $row;
376
    }
377
378
    /**
379 61
     * Creates a db command that can be used to execute this query.
380
     *
381 61
     * @throws Exception
382 57
     */
383
    public function createCommand(): CommandInterface
384
    {
385 4
        if ($this->sql === null) {
386 4
            [$sql, $params] = $this->db->getQueryBuilder()->build($this);
387 4
        } else {
388 4
            $sql = $this->sql;
389
            $params = $this->params;
390 4
        }
391
392 4
        return $this->db->createCommand($sql, $params);
393
    }
394
395
    /**
396
     * Queries a scalar value by setting {@see select()} first.
397
     *
398
     * Restores the value of select to make this query reusable.
399
     *
400
     * @param ExpressionInterface|string $selectExpression The expression to be selected.
401
     *
402
     * @throws Exception
403
     * @throws InvalidArgumentException
404
     * @throws InvalidConfigException
405
     * @throws NotSupportedException
406
     * @throws Throwable
407
     */
408
    protected function queryScalar(string|ExpressionInterface $selectExpression): bool|string|null|int|float
409
    {
410
        if ($this->sql === null) {
411
            return parent::queryScalar($selectExpression);
412
        }
413
414
        $command = (new Query($this->db))->select([$selectExpression])
415
            ->from(['c' => "($this->sql)"])
416
            ->params($this->params)
417
            ->createCommand();
418
419
        return $command->queryScalar();
420
    }
421
422
    public function joinWith(
423
        array|string $with,
424
        array|bool $eagerLoading = true,
425
        array|string $joinType = 'LEFT JOIN'
426
    ): self {
427
        $relations = [];
428
429
        foreach ((array) $with as $name => $callback) {
430
            if (is_int($name)) {
431
                $name = $callback;
432
                $callback = null;
433
            }
434
435
            if (preg_match('/^(.*?)(?:\s+AS\s+|\s+)(\w+)$/i', $name, $matches)) {
436
                /** The relation is defined with an alias, adjust callback to apply alias */
437
                [, $relation, $alias] = $matches;
438
439
                $name = $relation;
440
441
                $callback = static function (self $query) use ($callback, $alias): void {
442
                    $query->alias($alias);
443
444
                    if ($callback !== null) {
445
                        $callback($query);
446
                    }
447
                };
448
            }
449
450
            if ($callback === null) {
451 99
                $relations[] = $name;
452
            } else {
453 99
                $relations[$name] = $callback;
454
            }
455 99
        }
456 99
457 95
        $this->joinWith[] = [$relations, $eagerLoading, $joinType];
458 95
459
        return $this;
460
    }
461 99
462
    public function resetJoinWith(): void
463 20
    {
464
        $this->joinWith = [];
465 20
    }
466
467 20
        /**
468
     * @throws CircularReferenceException
469 20
     * @throws InvalidConfigException
470
     * @throws NotFoundException
471 20
     * @throws NotInstantiableException
472 16
     * @throws \Yiisoft\Definitions\Exception\InvalidConfigException
473
     */
474 20
    public function buildJoinWith(): void
475
    {
476
        $join = $this->join;
477 99
478 95
        $this->join = [];
479
480 48
        $arClass = $this->getARInstance();
481
482
        foreach ($this->joinWith as [$with, $eagerLoading, $joinType]) {
483
            $this->joinWithRelations($arClass, $with, $joinType);
484 99
485
            if (is_array($eagerLoading)) {
486 99
                foreach ($with as $name => $callback) {
487
                    if (is_int($name)) {
488
                        if (!in_array($callback, $eagerLoading, true)) {
489 84
                            unset($with[$name]);
490
                        }
491 84
                    } elseif (!in_array($name, $eagerLoading, true)) {
492
                        unset($with[$name]);
493 84
                    }
494
                }
495 84
            } elseif (!$eagerLoading) {
496
                $with = [];
497 84
            }
498 84
499
            $this->with($with);
500 84
        }
501
502
        /**
503
         * Remove duplicated joins added by joinWithRelations that may be added, for example, when joining a relation
504
         * and a via relation at the same time.
505
         */
506
        $uniqueJoins = [];
507
508
        foreach ($this->join as $j) {
509
            $uniqueJoins[serialize($j)] = $j;
510 84
        }
511 16
        $this->join = array_values($uniqueJoins);
512
513
        /**
514 84
         * @link https://github.com/yiisoft/yii2/issues/16092
515
         */
516
        $uniqueJoinsByTableName = [];
517
518
        foreach ($this->join as $config) {
519
            $tableName = serialize($config[1]);
520
            if (!array_key_exists($tableName, $uniqueJoinsByTableName)) {
521 84
                $uniqueJoinsByTableName[$tableName] = $config;
522
            }
523 84
        }
524 84
525
        $this->join = array_values($uniqueJoinsByTableName);
526 84
527
        if (!empty($join)) {
528
            /**
529 84
             * Append explicit join to {@see joinWith()} {@link https://github.com/yiisoft/yii2/issues/2880}
530
             */
531 84
            $this->join = empty($this->join) ? $join : array_merge($this->join, $join);
532 84
        }
533 84
    }
534 84
535
    public function innerJoinWith(array|string $with, array|bool $eagerLoading = true): self
536
    {
537
        return $this->joinWith($with, $eagerLoading, 'INNER JOIN');
538 84
    }
539
540 84
    /**
541
     * Modifies the current query by adding join fragments based on the given relations.
542
     *
543
     * @param ActiveRecordInterface $arClass The primary model.
544 84
     * @param array $with The relations to be joined.
545
     * @param array|string $joinType The join type.
546
     *
547
     * @throws CircularReferenceException
548
     * @throws InvalidConfigException
549
     * @throws NotFoundException
550
     * @throws NotInstantiableException
551
     * @throws \Yiisoft\Definitions\Exception\InvalidConfigException
552
     */
553
    private function joinWithRelations(ActiveRecordInterface $arClass, array $with, array|string $joinType): void
554
    {
555
        $relations = [];
556
557
        foreach ($with as $name => $callback) {
558
            if (is_int($name)) {
559
                $name = $callback;
560
                $callback = null;
561 53
            }
562
563 53
            $primaryModel = $arClass;
564
            $parent = $this;
565
            $prefix = '';
566
567
            while (($pos = strpos($name, '.')) !== false) {
568
                $childName = substr($name, $pos + 1);
569
                $name = substr($name, 0, $pos);
570
                $fullName = $prefix === '' ? $name : "$prefix.$name";
571
572
                if (!isset($relations[$fullName])) {
573 84
                    $relations[$fullName] = $relation = $primaryModel->getRelation($name);
574
                    if ($relation instanceof ActiveQueryInterface) {
575 84
                        $this->joinWithRelation($parent, $relation, $this->getJoinType($joinType, $fullName));
576
                    }
577 84
                } else {
578 84
                    $relation = $relations[$fullName];
579 80
                }
580 80
581
                if ($relation instanceof ActiveQueryInterface) {
582
                    $primaryModel = $relation->getARInstance();
583 84
                    $parent = $relation;
584 84
                }
585 84
586
                $prefix = $fullName;
587 84
                $name = $childName;
588 20
            }
589 20
590 20
            $fullName = $prefix === '' ? $name : "$prefix.$name";
591
592 20
            if (!isset($relations[$fullName])) {
593 12
                $relations[$fullName] = $relation = $primaryModel->getRelation($name);
594 12
595
                if ($callback !== null) {
596 8
                    $callback($relation);
597
                }
598
599 20
                if ($relation instanceof ActiveQueryInterface && !empty($relation->getJoinWith())) {
600
                    $relation->buildJoinWith();
601 20
                }
602 20
603 20
                if ($relation instanceof ActiveQueryInterface) {
604
                    $this->joinWithRelation($parent, $relation, $this->getJoinType($joinType, $fullName));
605
                }
606 84
            }
607
        }
608 84
    }
609 84
610
    /**
611 84
     * Returns the join type based on the given join type parameter and the relation name.
612 48
     *
613
     * @param array|string $joinType The given join type(s).
614
     * @param string $name The relation name.
615 84
     *
616 12
     * @return string The real join type.
617
     */
618
    private function getJoinType(array|string $joinType, string $name): string
619 84
    {
620
        if (is_array($joinType) && isset($joinType[$name])) {
621
            return $joinType[$name];
622 84
        }
623
624
        return is_string($joinType) ? $joinType : 'INNER JOIN';
625
    }
626
627
    /**
628
     * Returns the table name and the table alias for {@see arClass}.
629
     *
630
     * @throws CircularReferenceException
631
     * @throws InvalidConfigException
632 84
     * @throws NotFoundException
633
     * @throws NotInstantiableException
634 84
     */
635
    private function getTableNameAndAlias(): array
636
    {
637
        if (empty($this->from)) {
638 84
            $tableName = $this->getPrimaryTableName();
639
        } else {
640
            $tableName = '';
641
642
            foreach ($this->from as $alias => $tableName) {
643
                if (is_string($alias)) {
644
                    return [$tableName, $alias];
645
                }
646 113
                break;
647
            }
648 113
        }
649 104
650
        if (preg_match('/^(.*?)\s+({{\w+}}|\w+)$/', $tableName, $matches)) {
651 89
            $alias = $matches[2];
652
        } else {
653 89
            $alias = $tableName;
654 89
        }
655 28
656
        return [$tableName, $alias];
657 81
    }
658
659
    /**
660
     * Joins a parent query with a child query.
661 109
     *
662 8
     * The current query object will be modified so.
663
     *
664 109
     * @param ActiveQueryInterface $parent The parent query.
665
     * @param ActiveQueryInterface $child The child query.
666
     * @param string $joinType The join type.
667 109
     *
668
     * @throws CircularReferenceException
669
     * @throws NotFoundException
670
     * @throws NotInstantiableException
671
     * @throws \Yiisoft\Definitions\Exception\InvalidConfigException
672
     */
673
    private function joinWithRelation(ActiveQueryInterface $parent, ActiveQueryInterface $child, string $joinType): void
674
    {
675
        $via = $child->getVia();
676
        /** @var ActiveQuery $child */
677
        $child->via = null;
678
679 84
        if ($via instanceof self) {
680
            // via table
681 84
            $this->joinWithRelation($parent, $via, $joinType);
682 84
            $this->joinWithRelation($via, $child, $joinType);
683
684 84
            return;
685
        }
686 12
687 12
        if (is_array($via)) {
688
            // via relation
689 12
            $this->joinWithRelation($parent, $via[1], $joinType);
690
            $this->joinWithRelation($via[1], $child, $joinType);
691
692 84
            return;
693
        }
694 28
695 28
        /** @var ActiveQuery $parent */
696
        [$parentTable, $parentAlias] = $parent->getTableNameAndAlias();
697 28
        [$childTable, $childAlias] = $child->getTableNameAndAlias();
698
699
        if (!empty($child->getLink())) {
700 84
            if (!str_contains($parentAlias, '{{')) {
701 84
                $parentAlias = '{{' . $parentAlias . '}}';
702
            }
703 84
704 84
            if (!str_contains($childAlias, '{{')) {
705 84
                $childAlias = '{{' . $childAlias . '}}';
706
            }
707
708 84
            $on = [];
709 84
710
            foreach ($child->getLink() as $childColumn => $parentColumn) {
711
                $on[] = "$parentAlias.[[$parentColumn]] = $childAlias.[[$childColumn]]";
712 84
            }
713
714 84
            $on = implode(' AND ', $on);
715 84
716
            if (!empty($child->getOn())) {
717
                $on = ['and', $on, $child->getOn()];
718 84
            }
719
        } else {
720 84
            $on = $child->getOn();
721 84
        }
722
723
        $this->join($joinType, empty($child->getFrom()) ? $childTable : $child->getFrom(), $on ?? '');
0 ignored issues
show
Bug introduced by
It seems like $on ?? '' can also be of type null; however, parameter $on of Yiisoft\Db\Query\Query::join() does only seem to accept array|string, maybe add an additional type check? ( Ignorable by Annotation )

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

723
        $this->join($joinType, empty($child->getFrom()) ? $childTable : $child->getFrom(), /** @scrutinizer ignore-type */ $on ?? '');
Loading history...
724
725
        $where = $child->getWhere();
726
727 84
        if (!empty($where)) {
728
            $this->andWhere($where);
729 84
        }
730 28
731
        $having = $child->getHaving();
732
733 84
        if (!empty($having)) {
734
            $this->andHaving($having);
735
        }
736
737 84
        if (!empty($child->getOrderBy())) {
738 32
            $this->addOrderBy($child->getOrderBy());
739
        }
740
741 84
        if (!empty($child->getGroupBy())) {
742
            $this->addGroupBy($child->getGroupBy());
743
        }
744
745 84
        if (!empty($child->getParams())) {
746
            $this->addParams($child->getParams());
747
        }
748
749 84
        if (!empty($child->getJoins())) {
750 12
            foreach ($child->getJoins() as $join) {
751 12
                $this->join[] = $join;
752
            }
753
        }
754
755 84
        if (!empty($child->getUnions())) {
756
            foreach ($child->getUnions() as $union) {
757
                $this->union[] = $union;
758
            }
759
        }
760 84
    }
761
762
    public function onCondition(array|string $condition, array $params = []): self
763
    {
764
        $this->on = $condition;
765
766
        $this->addParams($params);
767
768
        return $this;
769
    }
770
771
    public function andOnCondition(array|string $condition, array $params = []): self
772
    {
773
        if ($this->on === null) {
774
            $this->on = $condition;
775
        } else {
776
            $this->on = ['and', $this->on, $condition];
777
        }
778
779
        $this->addParams($params);
780
781
        return $this;
782
    }
783
784
    public function orOnCondition(array|string $condition, array $params = []): self
785
    {
786
        if ($this->on === null) {
787
            $this->on = $condition;
788 29
        } else {
789
            $this->on = ['or', $this->on, $condition];
790 29
        }
791
792 29
        $this->addParams($params);
793
794 29
        return $this;
795
    }
796
797
    public function viaTable(string $tableName, array $link, callable $callable = null): self
798
    {
799
        $arClass = $this->primaryModel::class ?? $this->arClass;
800
        $arClassInstance = new self($arClass, $this->db);
801
802
        /** @psalm-suppress UndefinedMethod */
803
        $relation = $arClassInstance->from([$tableName])->link($link)->multiple(true)->asArray();
804
805
        $this->via = $relation;
806
807
        if ($callable !== null) {
808
            $callable($relation);
809
        }
810
811 10
        return $this;
812
    }
813 10
814 5
    public function alias(string $alias): self
815
    {
816 5
        if (empty($this->from) || count($this->from) < 2) {
817
            [$tableName] = $this->getTableNameAndAlias();
818
            $this->from = [$alias => $tableName];
819 10
        } else {
820
            $tableName = $this->getPrimaryTableName();
821 10
822
            foreach ($this->from as $key => $table) {
823
                if ($table === $tableName) {
824
                    unset($this->from[$key]);
825
                    $this->from[$alias] = $tableName;
826
                }
827
            }
828
        }
829
830
        return $this;
831
    }
832
833
    /**
834
     * @throws CircularReferenceException
835
     * @throws InvalidArgumentException
836
     * @throws NotFoundException
837
     * @throws NotInstantiableException
838 10
     * @throws \Yiisoft\Definitions\Exception\InvalidConfigException
839
     */
840 10
    public function getTablesUsedInFrom(): array
841 5
    {
842
        if (empty($this->from)) {
843 5
            return $this->db->getQuoter()->cleanUpTableNames([$this->getPrimaryTableName()]);
844
        }
845
846 10
        return parent::getTablesUsedInFrom();
847
    }
848 10
849
    /**
850
     * @throws CircularReferenceException
851
     * @throws NotFoundException
852
     * @throws NotInstantiableException
853
     * @throws \Yiisoft\Definitions\Exception\InvalidConfigException
854
     */
855
    protected function getPrimaryTableName(): string
856
    {
857
        return $this->getARInstance()->getTableName();
858
    }
859
860
    public function getOn(): array|string|null
861
    {
862
        return $this->on;
863
    }
864
865
    /**
866
     * @return array $value A list of relations that this query should be joined with.
867
     */
868
    public function getJoinWith(): array
869
    {
870
        return $this->joinWith;
871
    }
872
873
    public function getSql(): string|null
874 32
    {
875
        return $this->sql;
876 32
    }
877
878 32
    public function getARClass(): string|null
879
    {
880 32
        return $this->arClass;
881
    }
882 32
883
    /**
884 32
     * @throws Exception
885 8
     * @throws InvalidArgumentException
886
     * @throws InvalidConfigException
887
     * @throws Throwable
888 32
     */
889
    public function findOne(mixed $condition): array|ActiveRecordInterface|null
890
    {
891
        return $this->findByCondition($condition)->onePopulate();
892
    }
893
894
    /**
895
     * @param mixed $condition The primary key value or a set of column values.
896
     *
897
     * @throws Exception
898
     * @throws InvalidArgumentException
899
     * @throws InvalidConfigException
900
     * @throws Throwable
901 41
     *
902
     * @return array Of ActiveRecord instance, or an empty array if nothing matches.
903 41
     */
904 41
    public function findAll(mixed $condition): array
905 41
    {
906
        return $this->findByCondition($condition)->all();
907 4
    }
908
909 4
    /**
910 4
     * Finds ActiveRecord instance(s) by the given condition.
911 4
     *
912 4
     * This method is internally called by {@see findOne()} and {@see findAll()}.
913
     *
914
     * @param mixed $condition Please refer to {@see findOne()} for the explanation of this parameter.
915
     *
916
     * @throws CircularReferenceException
917 41
     * @throws Exception
918
     * @throws InvalidArgumentException
919
     * @throws NotFoundException
920
     * @throws NotInstantiableException
921
     */
922
    protected function findByCondition(mixed $condition): static
923
    {
924
        $arInstance = $this->getARInstance();
925
926
        if (!is_array($condition)) {
927
            $condition = [$condition];
928
        }
929 168
930
        if (!DbArrayHelper::isAssociative($condition) && !$condition instanceof ExpressionInterface) {
931 168
            /** query by primary key */
932 136
            $primaryKey = $arInstance->primaryKey();
933
934
            if (isset($primaryKey[0])) {
935 44
                $pk = $primaryKey[0];
936
937
                if (!empty($this->getJoins()) || !empty($this->getJoinWith())) {
938 553
                    $pk = $arInstance->getTableName() . '.' . $pk;
939
                }
940 553
941
                /**
942
                 * if the condition is scalar, search for a single primary key, if it's array, search for many primary
943
                 * key values.
944
                 */
945
                $condition = [$pk => array_values($condition)];
946
            } else {
947
                throw new InvalidConfigException('"' . $arInstance::class . '" must have a primary key.');
948
            }
949
        } else {
950
            $aliases = $arInstance->filterValidAliases($this);
951
            $condition = $arInstance->filterCondition($condition, $aliases);
952
        }
953 52
954
        return $this->where($condition);
955 52
    }
956
957
    public function findBySql(string $sql, array $params = []): self
958
    {
959
        return $this->sql($sql)->params($params);
960
    }
961 204
962
    public function on(array|string|null $value): self
963 204
    {
964
        $this->on = $value;
965
        return $this;
966
    }
967
968
    public function sql(string|null $value): self
969
    {
970
        $this->sql = $value;
971 4
        return $this;
972
    }
973 4
974
    public function getARInstance(): ActiveRecordInterface
975
    {
976 5
        if ($this->arFactory !== null) {
977
            return $this->arFactory->createAR($this->arClass, $this->tableName, $this->db);
978 5
        }
979
980
        /** @psalm-var class-string<ActiveRecordInterface> $class */
981
        $class = $this->arClass;
982
983
        return new $class($this->db, null, $this->tableName);
984
    }
985
986
    private function createInstance(): static
987
    {
988 228
        return (new static($this->arClass, $this->db))
989
            ->where($this->getWhere())
990 228
            ->limit($this->getLimit())
991
            ->offset($this->getOffset())
992
            ->orderBy($this->getOrderBy())
993
            ->indexBy($this->getIndexBy())
994
            ->select($this->select)
995
            ->selectOption($this->selectOption)
996
            ->distinct($this->distinct)
997
            ->from($this->from)
998
            ->groupBy($this->groupBy)
999
            ->setJoins($this->join)
1000 5
            ->having($this->having)
1001
            ->setUnions($this->union)
1002 5
            ->params($this->params)
1003
            ->withQueries($this->withQueries);
1004
    }
1005
}
1006