ActiveQuery::getARClass()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

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

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