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

725
        $this->join($joinType, empty($child->getFrom()) ? $childTable : $child->getFrom(), /** @scrutinizer ignore-type */ $on ?? '');
Loading history...
726
727 84
        $where = $child->getWhere();
728
729 84
        if (!empty($where)) {
730 28
            $this->andWhere($where);
731
        }
732
733 84
        $having = $child->getHaving();
734
735
        if (!empty($having)) {
736
            $this->andHaving($having);
737 84
        }
738 32
739
        if (!empty($child->getOrderBy())) {
740
            $this->addOrderBy($child->getOrderBy());
741 84
        }
742
743
        if (!empty($child->getGroupBy())) {
744
            $this->addGroupBy($child->getGroupBy());
745 84
        }
746
747
        if (!empty($child->getParams())) {
748
            $this->addParams($child->getParams());
749 84
        }
750 12
751 12
        if (!empty($child->getJoins())) {
752
            foreach ($child->getJoins() as $join) {
753
                $this->join[] = $join;
754
            }
755 84
        }
756
757
        if (!empty($child->getUnions())) {
758
            foreach ($child->getUnions() as $union) {
759
                $this->union[] = $union;
760 84
            }
761
        }
762
    }
763
764
    public function onCondition(array|string $condition, array $params = []): self
765
    {
766
        $this->on = $condition;
767
768
        $this->addParams($params);
769
770
        return $this;
771
    }
772
773
    public function andOnCondition(array|string $condition, array $params = []): self
774
    {
775
        if ($this->on === null) {
776
            $this->on = $condition;
777
        } else {
778
            $this->on = ['and', $this->on, $condition];
779
        }
780
781
        $this->addParams($params);
782
783
        return $this;
784
    }
785
786
    public function orOnCondition(array|string $condition, array $params = []): self
787
    {
788 29
        if ($this->on === null) {
789
            $this->on = $condition;
790 29
        } else {
791
            $this->on = ['or', $this->on, $condition];
792 29
        }
793
794 29
        $this->addParams($params);
795
796
        return $this;
797
    }
798
799
    public function viaTable(string $tableName, array $link, callable $callable = null): self
800
    {
801
        $arClass = $this->primaryModel ? $this->primaryModel::class : $this->arClass;
802
        $arClassInstance = new self($arClass, $this->db);
803
804
        /** @psalm-suppress UndefinedMethod */
805
        $relation = $arClassInstance->from([$tableName])->link($link)->multiple(true)->asArray();
806
807
        $this->via = $relation;
808
809
        if ($callable !== null) {
810
            $callable($relation);
811 10
        }
812
813 10
        return $this;
814 5
    }
815
816 5
    public function alias(string $alias): self
817
    {
818
        if (empty($this->from) || count($this->from) < 2) {
819 10
            [$tableName] = $this->getTableNameAndAlias();
820
            $this->from = [$alias => $tableName];
821 10
        } else {
822
            $tableName = $this->getPrimaryTableName();
823
824
            foreach ($this->from as $key => $table) {
825
                if ($table === $tableName) {
826
                    unset($this->from[$key]);
827
                    $this->from[$alias] = $tableName;
828
                }
829
            }
830
        }
831
832
        return $this;
833
    }
834
835
    /**
836
     * @throws CircularReferenceException
837
     * @throws InvalidArgumentException
838 10
     * @throws NotFoundException
839
     * @throws NotInstantiableException
840 10
     * @throws \Yiisoft\Definitions\Exception\InvalidConfigException
841 5
     */
842
    public function getTablesUsedInFrom(): array
843 5
    {
844
        if (empty($this->from)) {
845
            return $this->db->getQuoter()->cleanUpTableNames([$this->getPrimaryTableName()]);
846 10
        }
847
848 10
        return parent::getTablesUsedInFrom();
849
    }
850
851
    /**
852
     * @throws CircularReferenceException
853
     * @throws NotFoundException
854
     * @throws NotInstantiableException
855
     * @throws \Yiisoft\Definitions\Exception\InvalidConfigException
856
     */
857
    protected function getPrimaryTableName(): string
858
    {
859
        return $this->getARInstance()->getTableName();
860
    }
861
862
    public function getOn(): array|string|null
863
    {
864
        return $this->on;
865
    }
866
867
    /**
868
     * @return array $value A list of relations that this query should be joined with.
869
     */
870
    public function getJoinWith(): array
871
    {
872
        return $this->joinWith;
873
    }
874 32
875
    public function getSql(): string|null
876 32
    {
877
        return $this->sql;
878 32
    }
879
880 32
    public function getARClass(): string|null
881
    {
882 32
        return $this->arClass;
883
    }
884 32
885 8
    /**
886
     * @throws Exception
887
     * @throws InvalidArgumentException
888 32
     * @throws InvalidConfigException
889
     * @throws Throwable
890
     */
891
    public function findOne(mixed $condition): array|ActiveRecordInterface|null
892
    {
893
        return $this->findByCondition($condition)->onePopulate();
894
    }
895
896
    /**
897
     * @param mixed $condition The primary key value or a set of column values.
898
     *
899
     * @throws Exception
900
     * @throws InvalidArgumentException
901 41
     * @throws InvalidConfigException
902
     * @throws Throwable
903 41
     *
904 41
     * @return array Of ActiveRecord instance, or an empty array if nothing matches.
905 41
     */
906
    public function findAll(mixed $condition): array
907 4
    {
908
        return $this->findByCondition($condition)->all();
909 4
    }
910 4
911 4
    /**
912 4
     * Finds ActiveRecord instance(s) by the given condition.
913
     *
914
     * This method is internally called by {@see findOne()} and {@see findAll()}.
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)) {
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