Passed
Push — master ( 107cd3...b4e4db )
by Sergei
03:04
created

ActiveQuery::findByCondition()   A

Complexity

Conditions 6
Paths 8

Size

Total Lines 33
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 16
c 1
b 0
f 0
dl 0
loc 33
ccs 7
cts 7
cp 1
rs 9.1111
cc 6
nc 8
nop 1
crap 6
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