Passed
Push — master ( 75b09b...1404ee )
by Alexander
03:17
created

ActiveQuery::andOnCondition()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 6
c 0
b 0
f 0
dl 0
loc 11
rs 10
ccs 6
cts 6
cp 1
cc 2
nc 2
nop 2
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\ActiveRecord;
6
7
use ReflectionException;
8
use Throwable;
9
use Yiisoft\Arrays\ArrayHelper;
10
use Yiisoft\Db\Command\Command;
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\Query\Query;
18
use Yiisoft\Db\Query\QueryBuilder;
19
20
use function array_merge;
21
use function array_values;
22
use function count;
23
use function get_class;
24
use function implode;
25
use function in_array;
26
use function is_array;
27
use function is_int;
28
use function is_string;
29
use function preg_match;
30
use function reset;
31
use function serialize;
32
use function strpos;
33
use function substr;
34
35
/**
36
 * ActiveQuery represents a DB query associated with an Active Record class.
37
 *
38
 * An ActiveQuery can be a normal query or be used in a relational context.
39
 *
40
 * ActiveQuery instances are usually created by {@see ActiveQuery::findOne()}, {@see ActiveQuery::findBySql()},
41
 * {@see ActiveQuery::findAll()}
42
 *
43
 * Relational queries are created by {@see ActiveRecord::hasOne()} and {@see ActiveRecord::hasMany()}.
44
 *
45
 * Normal Query
46
 * ------------
47
 *
48
 * ActiveQuery mainly provides the following methods to retrieve the query results:
49
 *
50
 * - {@see one()}: returns a single record populated with the first row of data.
51
 * - {@see all()}: returns all records based on the query results.
52
 * - {@see count()}: returns the number of records.
53
 * - {@see sum()}: returns the sum over the specified column.
54
 * - {@see average()}: returns the average over the specified column.
55
 * - {@see min()}: returns the min over the specified column.
56
 * - {@see max()}: returns the max over the specified column.
57
 * - {@see scalar()}: returns the value of the first column in the first row of the query result.
58
 * - {@see column()}: returns the value of the first column in the query result.
59
 * - {@see exists()}: returns a value indicating whether the query result has data or not.
60
 *
61
 * Because ActiveQuery extends from {@see Query}, one can use query methods, such as {@see where()}, {@see orderBy()} to
62
 * customize the query options.
63
 *
64
 * ActiveQuery also provides the following additional query options:
65
 *
66
 * - {@see with()}: list of relations that this query should be performed with.
67
 * - {@see joinWith()}: reuse a relation query definition to add a join to a query.
68
 * - {@see indexBy()}: the name of the column by which the query result should be indexed.
69
 * - {@see asArray()}: whether to return each record as an array.
70
 *
71
 * These options can be configured using methods of the same name. For example:
72
 *
73
 * ```php
74
 * $customerQuery = new ActiveQuery(Customer::class, $db);
75
 * $query = $customerQuery->with('orders')->asArray()->all();
76
 * ```
77
 *
78
 * Relational query
79
 * ----------------
80
 *
81
 * In relational context ActiveQuery represents a relation between two Active Record classes.
82
 *
83
 * Relational ActiveQuery instances are usually created by calling {@see ActiveRecord::hasOne()} and
84
 * {@see ActiveRecord::hasMany()}. An Active Record class declares a relation by defining a getter method which calls
85
 * one of the above methods and returns the created ActiveQuery object.
86
 *
87
 * A relation is specified by {@see link} which represents the association between columns of different tables; and the
88
 * multiplicity of the relation is indicated by {@see multiple}.
89
 *
90
 * If a relation involves a junction table, it may be specified by {@see via()} or {@see viaTable()} method.
91
 *
92
 * These methods may only be called in a relational context. Same is true for {@see inverseOf()}, which marks a relation
93
 * as inverse of another relation and {@see onCondition()} which adds a condition that is to be added to relational
94
 * query join condition.
95
 */
96
class ActiveQuery extends Query implements ActiveQueryInterface
97
{
98
    use ActiveQueryTrait;
99
    use ActiveRelationTrait;
100
101
    protected string $arClass;
102
    protected ConnectionInterface $db;
103
    private ?string $sql = null;
104
    private $on;
105
    private array $joinWith = [];
106
    private ?ActiveRecordInterface $arInstance = null;
107
    private ?ActiveRecordFactory $arFactory;
108
109 720
    public function __construct(string $modelClass, ConnectionInterface $db, ActiveRecordFactory $arFactory = null)
110
    {
111 720
        $this->arClass = $modelClass;
112 720
        $this->arFactory = $arFactory;
113 720
        $this->db = $db;
114
115 720
        parent::__construct($db);
116 720
    }
117
118
    /**
119
     * Executes query and returns all results as an array.
120
     *
121
     * If null, the DB connection returned by {@see arClass} will be used.
122
     *
123
     * @throws Exception|InvalidConfigException|Throwable
124
     *
125
     * @return array|ActiveRecord[] the query results. If the query results in nothing, an empty array will be returned.
126
     */
127 244
    public function all(): array
128
    {
129 244
        return parent::all();
130
    }
131
132
    /**
133
     * Prepares for building SQL.
134
     *
135
     * This method is called by {@see QueryBuilder} when it starts to build SQL from a query object.
136
     *
137
     * You may override this method to do some final preparation work when converting a query into a SQL statement.
138
     *
139
     * @param QueryBuilder $builder
140
     *
141
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException|ReflectionException
142
     * @throws Throwable
143
     *
144
     * @return Query a prepared query instance which will be used by {@see QueryBuilder} to build the SQL.
145
     */
146 497
    public function prepare(QueryBuilder $builder): Query
147
    {
148
        /**
149
         * NOTE: because the same ActiveQuery may be used to build different SQL statements, one for count query, the
150
         * other for row data query, it is important to make sure the same ActiveQuery can be used to build SQL
151
         * statements multiple times.
152
         */
153 497
        if (!empty($this->joinWith)) {
154 80
            $this->buildJoinWith();
155
            /** clean it up to avoid issue {@see https://github.com/yiisoft/yii2/issues/2687} */
156 80
            $this->joinWith = [];
157
        }
158
159 497
        if (empty($this->getFrom())) {
160 489
            $this->from = [$this->getPrimaryTableName()];
161
        }
162
163 497
        if (empty($this->getSelect()) && !empty($this->getJoin())) {
164 76
            [, $alias] = $this->getTableNameAndAlias();
165
166 76
            $this->select(["$alias.*"]);
167
        }
168
169 497
        if ($this->primaryModel === null) {
170
            /** eager loading */
171 489
            $query = Query::create($this->db, $this);
172
        } else {
173
            /** lazy loading of a relation */
174 113
            $where = $this->getWhere();
175
176 113
            if ($this->via instanceof self) {
177
                /** via junction table */
178 20
                $viaModels = $this->via->findJunctionRows([$this->primaryModel]);
179
180 20
                $this->filterByModels($viaModels);
181 101
            } elseif (is_array($this->via)) {
182
                /**
183
                 * via relation
184
                 *
185
                 * @var $viaQuery ActiveQuery
186
                 */
187 28
                [$viaName, $viaQuery, $viaCallableUsed] = $this->via;
188
189 28
                if ($viaQuery->getMultiple()) {
190 28
                    if ($viaCallableUsed) {
191 20
                        $viaModels = $viaQuery->all();
192 8
                    } elseif ($this->primaryModel->isRelationPopulated($viaName)) {
0 ignored issues
show
Bug introduced by
The method isRelationPopulated() does not exist on Yiisoft\ActiveRecord\ActiveRecordInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Yiisoft\ActiveRecord\ActiveRecordInterface. ( Ignorable by Annotation )

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

192
                    } elseif ($this->primaryModel->/** @scrutinizer ignore-call */ isRelationPopulated($viaName)) {
Loading history...
193
                        $viaModels = $this->primaryModel->$viaName;
194
                    } else {
195 8
                        $viaModels = $viaQuery->all();
196 28
                        $this->primaryModel->populateRelation($viaName, $viaModels);
197
                    }
198
                } else {
199
                    if ($viaCallableUsed) {
200
                        $model = $viaQuery->one();
201
                    } elseif ($this->primaryModel->isRelationPopulated($viaName)) {
202
                        $model = $this->primaryModel->$viaName;
203
                    } else {
204
                        $model = $viaQuery->one();
205
                        $this->primaryModel->populateRelation($viaName, $model);
206
                    }
207
                    $viaModels = $model === null ? [] : [$model];
208
                }
209 28
                $this->filterByModels($viaModels);
210
            } else {
211 101
                $this->filterByModels([$this->primaryModel]);
212
            }
213
214 113
            $query = Query::create($this->db, $this);
215 113
            $this->where($where);
216
        }
217
218 497
        if (!empty($this->on)) {
219 24
            $query->andWhere($this->on);
220
        }
221
222 497
        return $query;
223
    }
224
225
    /**
226
     * Converts the raw query results into the format as specified by this query.
227
     *
228
     * This method is internally used to convert the data fetched from database into the format as required by this
229
     * query.
230
     *
231
     * @param array $rows the raw query result from database.
232
     *
233
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException|ReflectionException
234
     *
235
     * @return array the converted query result.
236
     */
237 419
    public function populate(array $rows): array
238
    {
239 419
        if (empty($rows)) {
240 74
            return [];
241
        }
242
243 406
        $models = $this->createModels($rows);
244
245 406
        if (!empty($this->join) && $this->getIndexBy() === null) {
0 ignored issues
show
introduced by
The condition $this->getIndexBy() === null is always false.
Loading history...
246 64
            $models = $this->removeDuplicatedModels($models);
247
        }
248
249 406
        if (!empty($this->with)) {
250 131
            $this->findWith($this->with, $models);
251
        }
252
253 406
        if ($this->inverseOf !== null) {
254 16
            $this->addInverseRelations($models);
255
        }
256
257 406
        return parent::populate($models);
258
    }
259
260
    /**
261
     * Removes duplicated models by checking their primary key values.
262
     *
263
     * This method is mainly called when a join query is performed, which may cause duplicated rows being returned.
264
     *
265
     * @param array $models the models to be checked.
266
     *
267
     * @throws Exception|InvalidConfigException
268
     *
269
     * @return array the distinctive models.
270
     */
271 64
    private function removeDuplicatedModels(array $models): array
272
    {
273 64
        $hash = [];
274
275 64
        $pks = $this->getARInstance()->primaryKey();
276
277 64
        if (count($pks) > 1) {
278
            /** composite primary key */
279 8
            foreach ($models as $i => $model) {
280 8
                $key = [];
281 8
                foreach ($pks as $pk) {
282 8
                    if (!isset($model[$pk])) {
283
                        /** do not continue if the primary key is not part of the result set */
284 4
                        break 2;
285
                    }
286 7
                    $key[] = $model[$pk];
287
                }
288
289 4
                $key = serialize($key);
290
291 4
                if (isset($hash[$key])) {
292
                    unset($models[$i]);
293
                } else {
294 4
                    $hash[$key] = true;
295
                }
296
            }
297 60
        } elseif (empty($pks)) {
298
            throw new InvalidConfigException("Primary key of '{$this->getARInstance()}' can not be empty.");
299
        } else {
300
            /** single column primary key */
301 60
            $pk = reset($pks);
302
303 60
            foreach ($models as $i => $model) {
304 60
                if (!isset($model[$pk])) {
305
                    /** do not continue if the primary key is not part of the result set */
306 4
                    break;
307
                }
308
309 56
                $key = $model[$pk];
310
311 56
                if (isset($hash[$key])) {
312 28
                    unset($models[$i]);
313 56
                } elseif ($key !== null) {
314 56
                    $hash[$key] = true;
315
                }
316
            }
317
        }
318
319 64
        return array_values($models);
320
    }
321
322
    /**
323
     * Executes query and returns a single row of result.
324
     *
325
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException|ReflectionException
326
     * @throws Throwable
327
     *
328
     * @return ActiveRecord|array|null a single row of query result. Depending on the setting of {@see asArray}, the
329
     * query result may be either an array or an ActiveRecord object. `null` will be returned if the query results in
330
     * nothing.
331
     */
332 305
    public function one()
333
    {
334 305
        $row = parent::one();
335
336 305
        if ($row !== false) {
0 ignored issues
show
introduced by
The condition $row !== false is always true.
Loading history...
337 301
            $models = $this->populate([$row]);
338
339 301
            return reset($models) ?: null;
340
        }
341
342 32
        return null;
343
    }
344
345
    /**
346
     * Creates a DB command that can be used to execute this query.
347
     *
348
     * @throws Exception|InvalidConfigException
349
     *
350
     * @return Command the created DB command instance.
351
     */
352 449
    public function createCommand(): Command
353
    {
354 449
        if ($this->sql === null) {
355 445
            [$sql, $params] = $this->db->getQueryBuilder()->build($this);
356
        } else {
357 4
            $sql = $this->sql;
358 4
            $params = $this->params;
359
        }
360
361 449
        $command = $this->db->createCommand($sql, $params);
362
363 449
        $this->setCommandCache($command);
364
365 449
        return $command;
366
    }
367
368
    /**
369
     * Queries a scalar value by setting {@see select} first.
370
     *
371
     * Restores the value of select to make this query reusable.
372
     *
373
     * @param string|ExpressionInterface $selectExpression
374
     *
375
     * @throws Exception|InvalidConfigException|Throwable
376
     *
377
     * @return bool|null|string
378
     */
379 61
    protected function queryScalar($selectExpression)
380
    {
381 61
        if ($this->sql === null) {
382 57
            return parent::queryScalar($selectExpression);
383
        }
384
385 4
        $command = (new Query($this->db))->select([$selectExpression])
386 4
            ->from(['c' => "({$this->sql})"])
387 4
            ->params($this->params)
388 4
            ->createCommand();
389
390 4
        $this->setCommandCache($command);
391
392 4
        return $command->queryScalar();
393
    }
394
395
    /**
396
     * Joins with the specified relations.
397
     *
398
     * This method allows you to reuse existing relation definitions to perform JOIN queries. Based on the definition of
399
     * the specified relation(s), the method will append one or multiple JOIN statements to the current query.
400
     *
401
     * If the `$eagerLoading` parameter is true, the method will also perform eager loading for the specified relations,
402
     * which is equivalent to calling {@see with()} using the specified relations.
403
     *
404
     * Note that because a JOIN query will be performed, you are responsible to disambiguate column names.
405
     *
406
     * This method differs from {@see with()} in that it will build up and execute a JOIN SQL statement  for the primary
407
     * table. And when `$eagerLoading` is true, it will call {@see with()} in addition with the specified relations.
408
     *
409
     * @param string|array $with the relations to be joined. This can either be a string, representing a relation name
410
     * or an array with the following semantics:
411
     *
412
     * - Each array element represents a single relation.
413
     * - You may specify the relation name as the array key and provide an anonymous functions that can be used to
414
     *   modify the relation queries on-the-fly as the array value.
415
     * - If a relation query does not need modification, you may use the relation name as the array value.
416
     *
417
     * The relation name may optionally contain an alias for the relation table (e.g. `books b`).
418
     *
419
     * Sub-relations can also be specified, see {@see with()} for the syntax.
420
     *
421
     * In the following you find some examples:
422
     *
423
     * ```php
424
     * // find all orders that contain books, and eager loading "books".
425
     * $orderQuery = new ActiveQuery(Order::class, $db);
426
     * $orderQuery->joinWith('books', true, 'INNER JOIN')->all();
427
     *
428
     * // find all orders, eager loading "books", and sort the orders and books by the book names.
429
     * $orderQuery = new ActiveQuery(Order::class, $db);
430
     * $orderQuery->joinWith([
431
     *     'books' => function (ActiveQuery $query) {
432
     *         $query->orderBy('item.name');
433
     *     }
434
     * ])->all();
435
     *
436
     * // find all orders that contain books of the category 'Science fiction', using the alias "b" for the books table.
437
     * $order = new ActiveQuery(Order::class, $db);
438
     * $orderQuery->joinWith(['books b'], true, 'INNER JOIN')->where(['b.category' => 'Science fiction'])->all();
439
     * ```
440
     *
441
     * @param bool|array $eagerLoading whether to eager load the relations specified in `$with`. When this is a boolean,
442
     * it applies to all relations specified in `$with`. Use an array to explicitly list which relations in `$with` need
443
     * to be eagerly loaded.  Note, that this does not mean, that the relations are populated from the query result. An
444
     * extra query will still be performed to bring in the related data. Defaults to `true`.
445
     * @param string|array $joinType the join type of the relations specified in `$with`.  When this is a string, it
446
     * applies to all relations specified in `$with`. Use an array in the format of `relationName => joinType` to
447
     * specify different join types for different relations.
448
     *
449
     * @return $this the query object itself.
450
     */
451 99
    public function joinWith($with, $eagerLoading = true, $joinType = 'LEFT JOIN'): self
452
    {
453 99
        $relations = [];
454
455 99
        foreach ((array) $with as $name => $callback) {
456 99
            if (is_int($name)) {
457 95
                $name = $callback;
458 95
                $callback = null;
459
            }
460
461 99
            if (preg_match('/^(.*?)(?:\s+AS\s+|\s+)(\w+)$/i', $name, $matches)) {
462
                /** relation is defined with an alias, adjust callback to apply alias */
463 20
                [, $relation, $alias] = $matches;
464
465 20
                $name = $relation;
466
467 20
                $callback = static function ($query) use ($callback, $alias) {
468
                    /** @var $query ActiveQuery */
469 20
                    $query->alias($alias);
470
471 20
                    if ($callback !== null) {
472 16
                        $callback($query);
473
                    }
474 20
                };
475
            }
476
477 99
            if ($callback === null) {
478 95
                $relations[] = $name;
479
            } else {
480 48
                $relations[$name] = $callback;
481
            }
482
        }
483
484 99
        $this->joinWith[] = [$relations, $eagerLoading, $joinType];
485
486 99
        return $this;
487
    }
488
489 84
    private function buildJoinWith(): void
490
    {
491 84
        $join = $this->join;
492
493 84
        $this->join = [];
494
495 84
        $arClass = $this->getARInstance();
496
497 84
        foreach ($this->joinWith as [$with, $eagerLoading, $joinType]) {
498 84
            $this->joinWithRelations($arClass, $with, $joinType);
499
500 84
            if (is_array($eagerLoading)) {
501
                foreach ($with as $name => $callback) {
502
                    if (is_int($name)) {
503
                        if (!in_array($callback, $eagerLoading, true)) {
504
                            unset($with[$name]);
505
                        }
506
                    } elseif (!in_array($name, $eagerLoading, true)) {
507
                        unset($with[$name]);
508
                    }
509
                }
510 84
            } elseif (!$eagerLoading) {
511 16
                $with = [];
512
            }
513
514 84
            $this->with($with);
515
        }
516
517
        /**
518
         * Remove duplicated joins added by joinWithRelations that may be added e.g. when joining a relation and a via
519
         * relation at the same time.
520
         */
521 84
        $uniqueJoins = [];
522
523 84
        foreach ($this->join as $j) {
524 84
            $uniqueJoins[serialize($j)] = $j;
525
        }
526 84
        $this->join = array_values($uniqueJoins);
527
528
        /** {@see https://github.com/yiisoft/yii2/issues/16092 } */
529 84
        $uniqueJoinsByTableName = [];
530
531 84
        foreach ($this->join as $config) {
532 84
            $tableName = serialize($config[1]);
533 84
            if (!array_key_exists($tableName, $uniqueJoinsByTableName)) {
534 84
                $uniqueJoinsByTableName[$tableName] = $config;
535
            }
536
        }
537
538 84
        $this->join = array_values($uniqueJoinsByTableName);
539
540 84
        if (!empty($join)) {
541
            /** Append explicit join to joinWith() {@see https://github.com/yiisoft/yii2/issues/2880} */
542
            $this->join = empty($this->join) ? $join : array_merge($this->join, $join);
543
        }
544 84
    }
545
546
    /**
547
     * Inner joins with the specified relations.
548
     *
549
     * This is a shortcut method to {@see joinWith()} with the join type set as "INNER JOIN". Please refer to
550
     * {@see joinWith()} for detailed usage of this method.
551
     *
552
     * @param string|array $with the relations to be joined with.
553
     * @param bool|array $eagerLoading whether to eager load the relations. Note, that this does not mean, that the
554
     * relations are populated from the query result. An extra query will still be performed to bring in the related
555
     * data.
556
     *
557
     * @return $this the query object itself.
558
     *
559
     * {@see joinWith()}
560
     */
561 53
    public function innerJoinWith($with, $eagerLoading = true): self
562
    {
563 53
        return $this->joinWith($with, $eagerLoading, 'INNER JOIN');
564
    }
565
566
    /**
567
     * Modifies the current query by adding join fragments based on the given relations.
568
     *
569
     * @param ActiveRecordInterface $arClass the primary model.
570
     * @param array $with the relations to be joined.
571
     * @param string|array $joinType the join type.
572
     */
573 84
    private function joinWithRelations(ActiveRecordInterface $arClass, array $with, $joinType): void
574
    {
575 84
        $relations = [];
576
577 84
        foreach ($with as $name => $callback) {
578 84
            if (is_int($name)) {
579 80
                $name = $callback;
580 80
                $callback = null;
581
            }
582
583 84
            $primaryModel = $arClass;
584 84
            $parent = $this;
585 84
            $prefix = '';
586
587 84
            while (($pos = strpos($name, '.')) !== false) {
588 20
                $childName = substr($name, $pos + 1);
589 20
                $name = substr($name, 0, $pos);
590 20
                $fullName = $prefix === '' ? $name : "$prefix.$name";
591
592 20
                if (!isset($relations[$fullName])) {
593 12
                    $relations[$fullName] = $relation = $primaryModel->getRelation($name);
594 12
                    $this->joinWithRelation($parent, $relation, $this->getJoinType($joinType, $fullName));
0 ignored issues
show
Bug introduced by
It seems like $relation can also be of type null; however, parameter $child of Yiisoft\ActiveRecord\Act...ery::joinWithRelation() does only seem to accept Yiisoft\ActiveRecord\ActiveQuery, 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

594
                    $this->joinWithRelation($parent, /** @scrutinizer ignore-type */ $relation, $this->getJoinType($joinType, $fullName));
Loading history...
595
                } else {
596 8
                    $relation = $relations[$fullName];
597
                }
598
599 20
                $primaryModel = $relation->getARInstance();
0 ignored issues
show
Bug introduced by
The method getARInstance() does not exist on Yiisoft\ActiveRecord\ActiveQueryInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Yiisoft\ActiveRecord\ActiveQueryInterface. ( Ignorable by Annotation )

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

599
                /** @scrutinizer ignore-call */ 
600
                $primaryModel = $relation->getARInstance();
Loading history...
600
601 20
                $parent = $relation;
602 20
                $prefix = $fullName;
603 20
                $name = $childName;
604
            }
605
606 84
            $fullName = $prefix === '' ? $name : "$prefix.$name";
607
608 84
            if (!isset($relations[$fullName])) {
609 84
                $relations[$fullName] = $relation = $primaryModel->getRelation($name);
610
611 84
                if ($callback !== null) {
612 48
                    $callback($relation);
613
                }
614
615 84
                if (!empty($relation->joinWith)) {
616 12
                    $relation->buildJoinWith();
617
                }
618
619 84
                $this->joinWithRelation($parent, $relation, $this->getJoinType($joinType, $fullName));
0 ignored issues
show
Bug introduced by
It seems like $parent can also be of type null; however, parameter $parent of Yiisoft\ActiveRecord\Act...ery::joinWithRelation() does only seem to accept Yiisoft\ActiveRecord\ActiveQuery, 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

619
                $this->joinWithRelation(/** @scrutinizer ignore-type */ $parent, $relation, $this->getJoinType($joinType, $fullName));
Loading history...
620
            }
621
        }
622 84
    }
623
624
    /**
625
     * Returns the join type based on the given join type parameter and the relation name.
626
     *
627
     * @param string|array $joinType the given join type(s).
628
     * @param string $name relation name.
629
     *
630
     * @return string the real join type.
631
     */
632 84
    private function getJoinType($joinType, string $name): string
633
    {
634 84
        if (is_array($joinType) && isset($joinType[$name])) {
635
            return $joinType[$name];
636
        }
637
638 84
        return is_string($joinType) ? $joinType : 'INNER JOIN';
639
    }
640
641
    /**
642
     * Returns the table name and the table alias for {@see arClass}.
643
     *
644
     * @return array the table name and the table alias.
645
     */
646 113
    private function getTableNameAndAlias(): array
647
    {
648 113
        if (empty($this->from)) {
649 104
            $tableName = $this->getPrimaryTableName();
650
        } else {
651 89
            $tableName = '';
652
653 89
            foreach ($this->from as $alias => $tableName) {
654 89
                if (is_string($alias)) {
655 28
                    return [$tableName, $alias];
656
                }
657 81
                break;
658
            }
659
        }
660
661 109
        if (preg_match('/^(.*?)\s+({{\w+}}|\w+)$/', $tableName, $matches)) {
662 8
            $alias = $matches[2];
663
        } else {
664 109
            $alias = $tableName;
665
        }
666
667 109
        return [$tableName, $alias];
668
    }
669
670
    /**
671
     * Joins a parent query with a child query.
672
     *
673
     * The current query object will be modified accordingly.
674
     *
675
     * @param ActiveQuery $parent
676
     * @param ActiveQuery $child
677
     * @param string $joinType
678
     */
679 84
    private function joinWithRelation(ActiveQuery $parent, ActiveQuery $child, string $joinType): void
680
    {
681 84
        $via = $child->via;
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
        [$parentTable, $parentAlias] = $parent->getTableNameAndAlias();
701 84
        [$childTable, $childAlias] = $child->getTableNameAndAlias();
702
703 84
        if (!empty($child->link)) {
704 84
            if (strpos($parentAlias, '{{') === false) {
705 84
                $parentAlias = '{{' . $parentAlias . '}}';
706
            }
707
708 84
            if (strpos($childAlias, '{{') === false) {
709 84
                $childAlias = '{{' . $childAlias . '}}';
710
            }
711
712 84
            $on = [];
713
714 84
            foreach ($child->link as $childColumn => $parentColumn) {
715 84
                $on[] = "$parentAlias.[[$parentColumn]] = $childAlias.[[$childColumn]]";
716
            }
717
718 84
            $on = implode(' AND ', $on);
719
720 84
            if (!empty($child->on)) {
721 84
                $on = ['and', $on, $child->on];
722
            }
723
        } else {
724
            $on = $child->on;
725
        }
726
727 84
        $this->join($joinType, empty($child->getFrom()) ? $childTable : $child->getFrom(), $on);
728
729 84
        if (!empty($child->getWhere())) {
730 28
            $this->andWhere($child->getWhere());
731
        }
732
733 84
        if (!empty($child->getHaving())) {
734
            $this->andHaving($child->getHaving());
735
        }
736
737 84
        if (!empty($child->getOrderBy())) {
738 32
            $this->addOrderBy($child->getOrderBy());
739
        }
740
741 84
        if (!empty($child->getGroupBy())) {
742
            $this->addGroupBy($child->getGroupBy());
743
        }
744
745 84
        if (!empty($child->getParams())) {
746
            $this->addParams($child->getParams());
747
        }
748
749 84
        if (!empty($child->getJoin())) {
750 12
            foreach ($child->getJoin() as $join) {
751 12
                $this->join[] = $join;
752
            }
753
        }
754
755 84
        if (!empty($child->getUnion())) {
756
            foreach ($child->getUnion() as $union) {
757
                $this->union[] = $union;
758
            }
759
        }
760 84
    }
761
762
    /**
763
     * Sets the ON condition for a relational query.
764
     *
765
     * The condition will be used in the ON part when {@see ActiveQuery::joinWith()} is called.
766
     *
767
     * Otherwise, the condition will be used in the WHERE part of a query.
768
     *
769
     * Use this method to specify additional conditions when declaring a relation in the {@see ActiveRecord} class:
770
     *
771
     * ```php
772
     * public function getActiveUsers(): ActiveQuery
773
     * {
774
     *     return $this->hasMany(User::class, ['id' => 'user_id'])->onCondition(['active' => true]);
775
     * }
776
     * ```
777
     *
778
     * Note that this condition is applied in case of a join as well as when fetching the related records. This only
779
     * fields of the related table can be used in the condition. Trying to access fields of the primary record will
780
     * cause an error in a non-join-query.
781
     *
782
     * @param string|array $condition the ON condition. Please refer to {@see Query::where()} on how to specify this
783
     * parameter.
784
     * @param array $params the parameters (name => value) to be bound to the query.
785
     *
786
     * @return $this the query object itself
787
     */
788 29
    public function onCondition($condition, array $params = []): self
789
    {
790 29
        $this->on = $condition;
791
792 29
        $this->addParams($params);
793
794 29
        return $this;
795
    }
796
797
    /**
798
     * Adds an additional ON condition to the existing one.
799
     *
800
     * The new condition and the existing one will be joined using the 'AND' operator.
801
     *
802
     * @param string|array $condition the new ON condition. Please refer to {@see where()} on how to specify this
803
     * parameter.
804
     * @param array $params the parameters (name => value) to be bound to the query.
805
     *
806
     * @return $this the query object itself.
807
     *
808
     * {@see onCondition()}
809
     * {@see orOnCondition()}
810
     */
811 10
    public function andOnCondition($condition, array $params = []): self
812
    {
813 10
        if ($this->on === null) {
814 5
            $this->on = $condition;
815
        } else {
816 5
            $this->on = ['and', $this->on, $condition];
817
        }
818
819 10
        $this->addParams($params);
820
821 10
        return $this;
822
    }
823
824
    /**
825
     * Adds an additional ON condition to the existing one.
826
     *
827
     * The new condition and the existing one will be joined using the 'OR' operator.
828
     *
829
     * @param string|array $condition the new ON condition. Please refer to {@see where()} on how to specify this
830
     * parameter.
831
     * @param array $params the parameters (name => value) to be bound to the query.
832
     *
833
     * @return $this the query object itself.
834
     *
835
     * {@see onCondition()}
836
     * {@see andOnCondition()}
837
     */
838 10
    public function orOnCondition($condition, array $params = []): self
839
    {
840 10
        if ($this->on === null) {
841 5
            $this->on = $condition;
842
        } else {
843 5
            $this->on = ['or', $this->on, $condition];
844
        }
845
846 10
        $this->addParams($params);
847
848 10
        return $this;
849
    }
850
851
    /**
852
     * Specifies the junction table for a relational query.
853
     *
854
     * Use this method to specify a junction table when declaring a relation in the {@see ActiveRecord} class:
855
     *
856
     * ```php
857
     * public function getItems()
858
     * {
859
     *     return $this->hasMany(Item::class, ['id' => 'item_id'])->viaTable('order_item', ['order_id' => 'id']);
860
     * }
861
     * ```
862
     *
863
     * @param string $tableName the name of the junction table.
864
     * @param array $link the link between the junction table and the table associated with {@see primaryModel}.
865
     * The keys of the array represent the columns in the junction table, and the values represent the columns in the
866
     * {@see primaryModel} table.
867
     * @param callable|null $callable a PHP callback for customizing the relation associated with the junction table.
868
     * Its signature should be `function($query)`, where `$query` is the query to be customized.
869
     *
870
     * @return $this the query object itself.
871
     *
872
     * {@see via()}
873
     */
874 32
    public function viaTable(string $tableName, array $link, callable $callable = null): self
875
    {
876 32
        $arClass = $this->primaryModel ? get_class($this->primaryModel) : $this->arClass;
877
878 32
        $arClassInstance = new self($arClass, $this->db);
879
880 32
        $relation = $arClassInstance->from([$tableName])->link($link)->multiple(true)->asArray(true);
881
882 32
        $this->via = $relation;
883
884 32
        if ($callable !== null) {
885 8
            $callable($relation);
886
        }
887
888 32
        return $this;
889
    }
890
891
    /**
892
     * Define an alias for the table defined in {@see arClass}.
893
     *
894
     * This method will adjust {@see from} so that an already defined alias will be overwritten. If none was defined,
895
     * {@see from} will be populated with the given alias.
896
     *
897
     * @param string $alias the table alias.
898
     *
899
     * @return $this the query object itself.
900
     */
901 41
    public function alias(string $alias): self
902
    {
903 41
        if (empty($this->from) || count($this->from) < 2) {
904 41
            [$tableName] = $this->getTableNameAndAlias();
905 41
            $this->from = [$alias => $tableName];
906
        } else {
907 4
            $tableName = $this->getPrimaryTableName();
908
909 4
            foreach ($this->from as $key => $table) {
910 4
                if ($table === $tableName) {
911 4
                    unset($this->from[$key]);
912 4
                    $this->from[$alias] = $tableName;
913
                }
914
            }
915
        }
916
917 41
        return $this;
918
    }
919
920
    /**
921
     * Returns table names used in {@see from} indexed by aliases.
922
     *
923
     * Both aliases and names are enclosed into {{ and }}.
924
     *
925
     * @throws InvalidArgumentException|InvalidConfigException
926
     *
927
     * @return array table names indexed by aliases.
928
     */
929 168
    public function getTablesUsedInFrom(): array
930
    {
931 168
        if (empty($this->from)) {
932 136
            return $this->cleanUpTableNames([$this->getPrimaryTableName()]);
933
        }
934
935 44
        return parent::getTablesUsedInFrom();
936
    }
937
938 553
    protected function getPrimaryTableName(): string
939
    {
940 553
        return $this->getARInstance()->tableName();
0 ignored issues
show
Bug introduced by
The method tableName() does not exist on Yiisoft\ActiveRecord\ActiveRecordInterface. It seems like you code against a sub-type of Yiisoft\ActiveRecord\ActiveRecordInterface such as Yiisoft\ActiveRecord\ActiveRecord. ( Ignorable by Annotation )

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

940
        return $this->getARInstance()->/** @scrutinizer ignore-call */ tableName();
Loading history...
941
    }
942
943
    /**
944
     * @return string|array the join condition to be used when this query is used in a relational context.
945
     *
946
     * The condition will be used in the ON part when {@see ActiveQuery::joinWith()} is called. Otherwise, the condition
947
     * will be used in the WHERE part of a query.
948
     *
949
     * Please refer to {@see Query::where()} on how to specify this parameter.
950
     *
951
     * {@see onCondition()}
952
     */
953 52
    public function getOn()
954
    {
955 52
        return $this->on;
956
    }
957
958
    /**
959
     * @return array $value a list of relations that this query should be joined with.
960
     */
961 204
    public function getJoinWith(): array
962
    {
963 204
        return $this->joinWith;
964
    }
965
966
    /**
967
     * @return string|null the SQL statement to be executed for retrieving AR records.
968
     *
969
     * This is set by {@see ActiveRecord::findBySql()}.
970
     */
971 4
    public function getSql(): ?string
972
    {
973 4
        return $this->sql;
974
    }
975
976 5
    public function getARClass(): ?string
977
    {
978 5
        return $this->arClass;
979
    }
980
981
    /**
982
     * @param mixed $condition primary key value or a set of column values.
983
     *
984
     * @throws InvalidConfigException
985
     *
986
     * @return ActiveRecordInterface|null ActiveRecord instance matching the condition, or `null` if nothing matches.
987
     */
988 228
    public function findOne($condition): ?ActiveRecordInterface
989
    {
990 228
        return $this->findByCondition($condition)->one();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->findByCondition($condition)->one() could return the type array which is incompatible with the type-hinted return Yiisoft\ActiveRecord\ActiveRecordInterface|null. Consider adding an additional type-check to rule them out.
Loading history...
991
    }
992
993
    /**
994
     * @param mixed $condition primary key value or a set of column values.
995
     *
996
     * @throws InvalidConfigException
997
     *
998
     * @return array of ActiveRecord instance, or an empty array if nothing matches.
999
     */
1000 5
    public function findAll($condition): array
1001
    {
1002 5
        return $this->findByCondition($condition)->all();
1003
    }
1004
1005
    /**
1006
     * Finds ActiveRecord instance(s) by the given condition.
1007
     *
1008
     * This method is internally called by {@see findOne()} and {@see findAll()}.
1009
     *
1010
     * @param mixed $condition please refer to {@see findOne()} for the explanation of this parameter.
1011
     *
1012
     * @throws InvalidConfigException if there is no primary key defined.
1013
     *
1014
     * @return ActiveQueryInterface the newly created {@see QueryInterface} instance.
1015
     */
1016 285
    protected function findByCondition($condition): ActiveQueryInterface
1017
    {
1018 285
        $arInstance = $this->getARInstance();
1019
1020 285
        if (!is_array($condition)) {
1021 185
            $condition = [$condition];
1022
        }
1023
1024 285
        if (!ArrayHelper::isAssociative($condition) && !$condition instanceof ExpressionInterface) {
1025
            /** query by primary key */
1026 189
            $primaryKey = $arInstance->primaryKey();
1027
1028 189
            if (isset($primaryKey[0])) {
1029 189
                $pk = $primaryKey[0];
1030
1031 189
                if (!empty($this->getJoin()) || !empty($this->getJoinWith())) {
1032
                    $pk = $arInstance->tableName() . '.' . $pk;
1033
                }
1034
1035
                /**
1036
                 * if condition is scalar, search for a single primary key, if it is array, search for multiple primary
1037
                 * key values
1038
                 */
1039 189
                $condition = [$pk => is_array($condition) ? array_values($condition) : $condition];
0 ignored issues
show
introduced by
The condition is_array($condition) is always true.
Loading history...
1040
            } else {
1041 189
                throw new InvalidConfigException('"' . get_class($arInstance) . '" must have a primary key.');
1042
            }
1043 108
        } elseif (is_array($condition)) {
0 ignored issues
show
introduced by
The condition is_array($condition) is always true.
Loading history...
1044 108
            $aliases = $arInstance->filterValidAliases($this);
0 ignored issues
show
Bug introduced by
The method filterValidAliases() does not exist on Yiisoft\ActiveRecord\ActiveRecordInterface. It seems like you code against a sub-type of Yiisoft\ActiveRecord\ActiveRecordInterface such as Yiisoft\ActiveRecord\ActiveRecord. ( Ignorable by Annotation )

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

1044
            /** @scrutinizer ignore-call */ 
1045
            $aliases = $arInstance->filterValidAliases($this);
Loading history...
1045 108
            $condition = $arInstance->filterCondition($condition, $aliases);
0 ignored issues
show
Bug introduced by
The method filterCondition() does not exist on Yiisoft\ActiveRecord\ActiveRecordInterface. It seems like you code against a sub-type of Yiisoft\ActiveRecord\ActiveRecordInterface such as Yiisoft\ActiveRecord\ActiveRecord. ( Ignorable by Annotation )

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

1045
            /** @scrutinizer ignore-call */ 
1046
            $condition = $arInstance->filterCondition($condition, $aliases);
Loading history...
1046
        }
1047
1048 253
        return $this->where($condition);
1049
    }
1050
1051
    /**
1052
     * Creates an {@see ActiveQuery} instance with a given SQL statement.
1053
     *
1054
     * Note that because the SQL statement is already specified, calling additional query modification methods (such as
1055
     * `where()`, `order()`) on the created {@see ActiveQuery} instance will have no effect. However, calling `with()`,
1056
     * `asArray()` or `indexBy()` is still fine.
1057
     *
1058
     * Below is an example:
1059
     *
1060
     * ```php
1061
     * $customerQuery = new ActiveQuery(Customer::class, $db);
1062
     * $customers = $customerQuery->findBySql('SELECT * FROM customer')->all();
1063
     * ```
1064
     *
1065
     * @param string $sql the SQL statement to be executed.
1066
     * @param array $params parameters to be bound to the SQL statement during execution.
1067
     *
1068
     * @return Query the newly created {@see ActiveQuery} instance
1069
     */
1070 8
    public function findBySql(string $sql, array $params = []): Query
1071
    {
1072 8
        return $this->sql($sql)->params($params);
1073
    }
1074
1075 15
    public function on($value): self
1076
    {
1077 15
        $this->on = $value;
1078 15
        return $this;
1079
    }
1080
1081 12
    public function sql(?string $value): self
1082
    {
1083 12
        $this->sql = $value;
1084 12
        return $this;
1085
    }
1086
1087 625
    public function getARInstance(): ActiveRecordInterface
1088
    {
1089 625
        if ($this->arFactory !== null) {
1090 1
            return $this->getARInstanceFactory();
1091
        }
1092
1093 625
        $class = $this->arClass;
1094
1095 625
        return new $class($this->db);
1096
    }
1097
1098 1
    public function getARInstanceFactory(): ActiveRecordInterface
1099
    {
1100 1
        $this->arFactory->withConnection($this->db);
0 ignored issues
show
Bug introduced by
The method withConnection() does not exist on null. ( Ignorable by Annotation )

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

1100
        $this->arFactory->/** @scrutinizer ignore-call */ 
1101
                          withConnection($this->db);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1101
1102 1
        return $this->arFactory->createAR($this->arClass);
1103
    }
1104
}
1105