Passed
Pull Request — master (#365)
by Sergei
14:46
created

ActiveQuery::joinWithRelation()   F

Complexity

Conditions 18
Paths 1730

Size

Total Lines 85
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 37
CRAP Score 18.8795

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 44
c 1
b 0
f 0
dl 0
loc 85
ccs 37
cts 43
cp 0.8605
rs 0.7
cc 18
nc 1730
nop 3
crap 18.8795

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

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