GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

Issues (910)

framework/db/ActiveQuery.php (4 issues)

1
<?php
2
/**
3
 * @link https://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license https://www.yiiframework.com/license/
6
 */
7
8
namespace yii\db;
9
10
use yii\base\InvalidConfigException;
11
12
/**
13
 * ActiveQuery represents a DB query associated with an Active Record class.
14
 *
15
 * An ActiveQuery can be a normal query or be used in a relational context.
16
 *
17
 * ActiveQuery instances are usually created by [[ActiveRecord::find()]] and [[ActiveRecord::findBySql()]].
18
 * Relational queries are created by [[ActiveRecord::hasOne()]] and [[ActiveRecord::hasMany()]].
19
 *
20
 * Normal Query
21
 * ------------
22
 *
23
 * ActiveQuery mainly provides the following methods to retrieve the query results:
24
 *
25
 * - [[one()]]: returns a single record populated with the first row of data.
26
 * - [[all()]]: returns all records based on the query results.
27
 * - [[count()]]: returns the number of records.
28
 * - [[sum()]]: returns the sum over the specified column.
29
 * - [[average()]]: returns the average over the specified column.
30
 * - [[min()]]: returns the min over the specified column.
31
 * - [[max()]]: returns the max over the specified column.
32
 * - [[scalar()]]: returns the value of the first column in the first row of the query result.
33
 * - [[column()]]: returns the value of the first column in the query result.
34
 * - [[exists()]]: returns a value indicating whether the query result has data or not.
35
 *
36
 * Because ActiveQuery extends from [[Query]], one can use query methods, such as [[where()]],
37
 * [[orderBy()]] to customize the query options.
38
 *
39
 * ActiveQuery also provides the following additional query options:
40
 *
41
 * - [[with()]]: list of relations that this query should be performed with.
42
 * - [[joinWith()]]: reuse a relation query definition to add a join to a query.
43
 * - [[indexBy()]]: the name of the column by which the query result should be indexed.
44
 * - [[asArray()]]: whether to return each record as an array.
45
 *
46
 * These options can be configured using methods of the same name. For example:
47
 *
48
 * ```php
49
 * $customers = Customer::find()->with('orders')->asArray()->all();
50
 * ```
51
 *
52
 * Relational query
53
 * ----------------
54
 *
55
 * In relational context ActiveQuery represents a relation between two Active Record classes.
56
 *
57
 * Relational ActiveQuery instances are usually created by calling [[ActiveRecord::hasOne()]] and
58
 * [[ActiveRecord::hasMany()]]. An Active Record class declares a relation by defining
59
 * a getter method which calls one of the above methods and returns the created ActiveQuery object.
60
 *
61
 * A relation is specified by [[link]] which represents the association between columns
62
 * of different tables; and the multiplicity of the relation is indicated by [[multiple]].
63
 *
64
 * If a relation involves a junction table, it may be specified by [[via()]] or [[viaTable()]] method.
65
 * These methods may only be called in a relational context. Same is true for [[inverseOf()]], which
66
 * marks a relation as inverse of another relation and [[onCondition()]] which adds a condition that
67
 * is to be added to relational query join condition.
68
 *
69
 * @author Qiang Xue <[email protected]>
70
 * @author Carsten Brandt <[email protected]>
71
 * @since 2.0
72
 */
73
class ActiveQuery extends Query implements ActiveQueryInterface
74
{
75
    use ActiveQueryTrait;
76
    use ActiveRelationTrait;
77
78
    /**
79
     * @event Event an event that is triggered when the query is initialized via [[init()]].
80
     */
81
    const EVENT_INIT = 'init';
82
83
    /**
84
     * @var string|null the SQL statement to be executed for retrieving AR records.
85
     * This is set by [[ActiveRecord::findBySql()]].
86
     */
87
    public $sql;
88
    /**
89
     * @var string|array|null the join condition to be used when this query is used in a relational context.
90
     * The condition will be used in the ON part when [[ActiveQuery::joinWith()]] is called.
91
     * Otherwise, the condition will be used in the WHERE part of a query.
92
     * Please refer to [[Query::where()]] on how to specify this parameter.
93
     * @see onCondition()
94
     */
95
    public $on;
96
    /**
97
     * @var array|null a list of relations that this query should be joined with
98
     */
99
    public $joinWith;
100
101
102
    /**
103
     * Constructor.
104
     * @param string $modelClass the model class associated with this query
105
     * @param array $config configurations to be applied to the newly created query object
106
     */
107 670
    public function __construct($modelClass, $config = [])
108
    {
109 670
        $this->modelClass = $modelClass;
110 670
        parent::__construct($config);
111
    }
112
113
    /**
114
     * Initializes the object.
115
     * This method is called at the end of the constructor. The default implementation will trigger
116
     * an [[EVENT_INIT]] event. If you override this method, make sure you call the parent implementation at the end
117
     * to ensure triggering of the event.
118
     */
119 670
    public function init()
120
    {
121 670
        parent::init();
122 670
        $this->trigger(self::EVENT_INIT);
123
    }
124
125
    /**
126
     * Executes query and returns all results as an array.
127
     * @param Connection|null $db the DB connection used to create the DB command.
128
     * If null, the DB connection returned by [[modelClass]] will be used.
129
     * @return array|ActiveRecord[] the query results. If the query results in nothing, an empty array will be returned.
130
     */
131 251
    public function all($db = null)
132
    {
133 251
        return parent::all($db);
134
    }
135
136
    /**
137
     * {@inheritdoc}
138
     */
139 520
    public function prepare($builder)
140
    {
141
        // NOTE: because the same ActiveQuery may be used to build different SQL statements
142
        // (e.g. by ActiveDataProvider, one for count query, the other for row data query,
143
        // it is important to make sure the same ActiveQuery can be used to build SQL statements
144
        // multiple times.
145 520
        if (!empty($this->joinWith)) {
146 84
            $this->buildJoinWith();
147 84
            $this->joinWith = null;    // clean it up to avoid issue https://github.com/yiisoft/yii2/issues/2687
148
        }
149
150 520
        if (empty($this->from)) {
151 496
            $this->from = [$this->getPrimaryTableName()];
152
        }
153
154 520
        if (empty($this->select) && !empty($this->join)) {
155 75
            list(, $alias) = $this->getTableNameAndAlias();
156 75
            $this->select = ["$alias.*"];
157
        }
158
159 520
        if ($this->primaryModel === null) {
160
            // eager loading
161 513
            $query = Query::create($this);
162
        } else {
163
            // lazy loading of a relation
164 124
            $where = $this->where;
165
166 124
            if ($this->via instanceof self) {
167
                // via junction table
168 15
                $viaModels = $this->via->findJunctionRows([$this->primaryModel]);
169 15
                $this->filterByModels($viaModels);
170 115
            } elseif (is_array($this->via)) {
171
                // via relation
172
                /* @var $viaQuery ActiveQuery */
173 42
                list($viaName, $viaQuery, $viaCallableUsed) = $this->via;
174 42
                if ($viaQuery->multiple) {
175 42
                    if ($viaCallableUsed) {
176 24
                        $viaModels = $viaQuery->all();
177 21
                    } elseif ($this->primaryModel->isRelationPopulated($viaName)) {
178 3
                        $viaModels = $this->primaryModel->$viaName;
179
                    } else {
180 21
                        $viaModels = $viaQuery->all();
181 42
                        $this->primaryModel->populateRelation($viaName, $viaModels);
182
                    }
183
                } else {
184
                    if ($viaCallableUsed) {
185
                        $model = $viaQuery->one();
186
                    } elseif ($this->primaryModel->isRelationPopulated($viaName)) {
187
                        $model = $this->primaryModel->$viaName;
188
                    } else {
189
                        $model = $viaQuery->one();
190
                        $this->primaryModel->populateRelation($viaName, $model);
191
                    }
192
                    $viaModels = $model === null ? [] : [$model];
193
                }
194 42
                $this->filterByModels($viaModels);
195
            } else {
196 115
                $this->filterByModels([$this->primaryModel]);
197
            }
198
199 124
            $query = Query::create($this);
200 124
            $this->where = $where;
201
        }
202
203 520
        if (!empty($this->on)) {
204 24
            $query->andWhere($this->on);
205
        }
206
207 520
        return $query;
208
    }
209
210
    /**
211
     * {@inheritdoc}
212
     */
213 420
    public function populate($rows)
214
    {
215 420
        if (empty($rows)) {
216 75
            return [];
217
        }
218
219 406
        $models = $this->createModels($rows);
220 406
        if (!empty($this->join) && $this->indexBy === null) {
221 69
            $models = $this->removeDuplicatedModels($models);
222
        }
223 406
        if (!empty($this->with)) {
224 132
            $this->findWith($this->with, $models);
225
        }
226
227 406
        if ($this->inverseOf !== null) {
228 12
            $this->addInverseRelations($models);
229
        }
230
231 406
        if (!$this->asArray) {
232 396
            foreach ($models as $model) {
233 396
                $model->afterFind();
234
            }
235
        }
236
237 406
        return parent::populate($models);
238
    }
239
240
    /**
241
     * Removes duplicated models by checking their primary key values.
242
     * This method is mainly called when a join query is performed, which may cause duplicated rows being returned.
243
     * @param array $models the models to be checked
244
     * @throws InvalidConfigException if model primary key is empty
245
     * @return array the distinctive models
246
     */
247 69
    private function removeDuplicatedModels($models)
248
    {
249 69
        $hash = [];
250
        /* @var $class ActiveRecord */
251 69
        $class = $this->modelClass;
252 69
        $pks = $class::primaryKey();
253
254 69
        if (count($pks) > 1) {
255
            // composite primary key
256 6
            foreach ($models as $i => $model) {
257 6
                $key = [];
258 6
                foreach ($pks as $pk) {
259 6
                    if (!isset($model[$pk])) {
260
                        // do not continue if the primary key is not part of the result set
261 3
                        break 2;
262
                    }
263 6
                    $key[] = $model[$pk];
264
                }
265 3
                $key = serialize($key);
266 3
                if (isset($hash[$key])) {
267
                    unset($models[$i]);
268
                } else {
269 3
                    $hash[$key] = true;
270
                }
271
            }
272 66
        } elseif (empty($pks)) {
273
            throw new InvalidConfigException("Primary key of '{$class}' can not be empty.");
274
        } else {
275
            // single column primary key
276 66
            $pk = reset($pks);
277 66
            foreach ($models as $i => $model) {
278 66
                if (!isset($model[$pk])) {
279
                    // do not continue if the primary key is not part of the result set
280 3
                    break;
281
                }
282 63
                $key = $model[$pk];
283 63
                if (isset($hash[$key])) {
284 24
                    unset($models[$i]);
285 63
                } elseif ($key !== null) {
286 63
                    $hash[$key] = true;
287
                }
288
            }
289
        }
290
291 69
        return array_values($models);
292
    }
293
294
    /**
295
     * Executes query and returns a single row of result.
296
     * @param Connection|null $db the DB connection used to create the DB command.
297
     * If `null`, the DB connection returned by [[modelClass]] will be used.
298
     * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]],
299
     * the query result may be either an array or an ActiveRecord object. `null` will be returned
300
     * if the query results in nothing.
301
     */
302 322
    public function one($db = null)
303
    {
304 322
        $row = parent::one($db);
305 322
        if ($row !== false) {
306 317
            $models = $this->populate([$row]);
307 317
            return reset($models) ?: null;
308
        }
309
310 29
        return null;
311
    }
312
313
    /**
314
     * Creates a DB command that can be used to execute this query.
315
     * @param Connection|null $db the DB connection used to create the DB command.
316
     * If `null`, the DB connection returned by [[modelClass]] will be used.
317
     * @return Command the created DB command instance.
318
     */
319 488
    public function createCommand($db = null)
320
    {
321
        /* @var $modelClass ActiveRecord */
322 488
        $modelClass = $this->modelClass;
323 488
        if ($db === null) {
324 474
            $db = $modelClass::getDb();
325
        }
326
327 488
        if ($this->sql === null) {
328 485
            list($sql, $params) = $db->getQueryBuilder()->build($this);
329
        } else {
330 3
            $sql = $this->sql;
331 3
            $params = $this->params;
332
        }
333
334 488
        $command = $db->createCommand($sql, $params);
335 488
        $this->setCommandCache($command);
336
337 488
        return $command;
338
    }
339
340
    /**
341
     * {@inheritdoc}
342
     */
343 68
    protected function queryScalar($selectExpression, $db)
344
    {
345
        /* @var $modelClass ActiveRecord */
346 68
        $modelClass = $this->modelClass;
347 68
        if ($db === null) {
348 67
            $db = $modelClass::getDb();
349
        }
350
351 68
        if ($this->sql === null) {
352 65
            return parent::queryScalar($selectExpression, $db);
353
        }
354
355 3
        $command = (new Query())->select([$selectExpression])
356 3
            ->from(['c' => "({$this->sql})"])
357 3
            ->params($this->params)
358 3
            ->createCommand($db);
359 3
        $this->setCommandCache($command);
360
361 3
        return $command->queryScalar();
362
    }
363
364
    /**
365
     * Joins with the specified relations.
366
     *
367
     * This method allows you to reuse existing relation definitions to perform JOIN queries.
368
     * Based on the definition of the specified relation(s), the method will append one or multiple
369
     * JOIN statements to the current query.
370
     *
371
     * If the `$eagerLoading` parameter is true, the method will also perform eager loading for the specified relations,
372
     * which is equivalent to calling [[with()]] using the specified relations.
373
     *
374
     * Note that because a JOIN query will be performed, you are responsible to disambiguate column names.
375
     *
376
     * This method differs from [[with()]] in that it will build up and execute a JOIN SQL statement
377
     * for the primary table. And when `$eagerLoading` is true, it will call [[with()]] in addition with the specified relations.
378
     *
379
     * @param string|array $with the relations to be joined. This can either be a string, representing a relation name or
380
     * an array with the following semantics:
381
     *
382
     * - Each array element represents a single relation.
383
     * - You may specify the relation name as the array key and provide an anonymous functions that
384
     *   can be used to modify the relation queries on-the-fly as the array value.
385
     * - If a relation query does not need modification, you may use the relation name as the array value.
386
     *
387
     * The relation name may optionally contain an alias for the relation table (e.g. `books b`).
388
     *
389
     * Sub-relations can also be specified, see [[with()]] for the syntax.
390
     *
391
     * In the following you find some examples:
392
     *
393
     * ```php
394
     * // find all orders that contain books, and eager loading "books"
395
     * Order::find()->joinWith('books', true, 'INNER JOIN')->all();
396
     * // find all orders, eager loading "books", and sort the orders and books by the book names.
397
     * Order::find()->joinWith([
398
     *     'books' => function (\yii\db\ActiveQuery $query) {
399
     *         $query->orderBy('item.name');
400
     *     }
401
     * ])->all();
402
     * // find all orders that contain books of the category 'Science fiction', using the alias "b" for the books table
403
     * Order::find()->joinWith(['books b'], true, 'INNER JOIN')->where(['b.category' => 'Science fiction'])->all();
404
     * ```
405
     *
406
     * The alias syntax is available since version 2.0.7.
407
     *
408
     * @param bool|array $eagerLoading whether to eager load the relations
409
     * specified in `$with`.  When this is a boolean, it applies to all
410
     * relations specified in `$with`. Use an array to explicitly list which
411
     * relations in `$with` need to be eagerly loaded.  Note, that this does
412
     * not mean, that the relations are populated from the query result. An
413
     * extra query will still be performed to bring in the related data.
414
     * Defaults to `true`.
415
     * @param string|array $joinType the join type of the relations specified in `$with`.
416
     * When this is a string, it applies to all relations specified in `$with`. Use an array
417
     * in the format of `relationName => joinType` to specify different join types for different relations.
418
     * @return $this the query object itself
419
     */
420 93
    public function joinWith($with, $eagerLoading = true, $joinType = 'LEFT JOIN')
421
    {
422 93
        $relations = [];
423 93
        foreach ((array) $with as $name => $callback) {
424 93
            if (is_int($name)) {
425 90
                $name = $callback;
426 90
                $callback = null;
427
            }
428
429 93
            if (preg_match('/^(.*?)(?:\s+AS\s+|\s+)(\w+)$/i', $name, $matches)) {
430
                // relation is defined with an alias, adjust callback to apply alias
431 18
                list(, $relation, $alias) = $matches;
432 18
                $name = $relation;
433 18
                $callback = function ($query) use ($callback, $alias) {
434
                    /* @var $query ActiveQuery */
435 18
                    $query->alias($alias);
436 18
                    if ($callback !== null) {
437 12
                        call_user_func($callback, $query);
438
                    }
439 18
                };
440
            }
441
442 93
            if ($callback === null) {
443 87
                $relations[] = $name;
444
            } else {
445 39
                $relations[$name] = $callback;
446
            }
447
        }
448 93
        $this->joinWith[] = [$relations, $eagerLoading, $joinType];
449 93
        return $this;
450
    }
451
452 87
    private function buildJoinWith()
453
    {
454 87
        $join = $this->join;
455 87
        $this->join = [];
456
457
        /* @var $modelClass ActiveRecordInterface */
458 87
        $modelClass = $this->modelClass;
459 87
        $model = $modelClass::instance();
460 87
        foreach ($this->joinWith as $config) {
461 87
            list($with, $eagerLoading, $joinType) = $config;
462 87
            $this->joinWithRelations($model, $with, $joinType);
463
464 87
            if (is_array($eagerLoading)) {
465
                foreach ($with as $name => $callback) {
466
                    if (is_int($name)) {
467
                        if (!in_array($callback, $eagerLoading, true)) {
468
                            unset($with[$name]);
469
                        }
470
                    } elseif (!in_array($name, $eagerLoading, true)) {
471
                        unset($with[$name]);
472
                    }
473
                }
474 87
            } elseif (!$eagerLoading) {
475 18
                $with = [];
476
            }
477
478 87
            $this->with($with);
479
        }
480
481
        // remove duplicated joins added by joinWithRelations that may be added
482
        // e.g. when joining a relation and a via relation at the same time
483 87
        $uniqueJoins = [];
484 87
        foreach ($this->join as $j) {
485 87
            $uniqueJoins[serialize($j)] = $j;
486
        }
487 87
        $this->join = array_values($uniqueJoins);
488
489
        // https://github.com/yiisoft/yii2/issues/16092
490 87
        $uniqueJoinsByTableName = [];
491 87
        foreach ($this->join as $config) {
492 87
            $tableName = serialize($config[1]);
493 87
            if (!array_key_exists($tableName, $uniqueJoinsByTableName)) {
494 87
                $uniqueJoinsByTableName[$tableName] = $config;
495
            }
496
        }
497 87
        $this->join = array_values($uniqueJoinsByTableName);
498
499 87
        if (!empty($join)) {
500
            // append explicit join to joinWith()
501
            // https://github.com/yiisoft/yii2/issues/2880
502
            $this->join = empty($this->join) ? $join : array_merge($this->join, $join);
503
        }
504
    }
505
506
    /**
507
     * Inner joins with the specified relations.
508
     * This is a shortcut method to [[joinWith()]] with the join type set as "INNER JOIN".
509
     * Please refer to [[joinWith()]] for detailed usage of this method.
510
     * @param string|array $with the relations to be joined with.
511
     * @param bool|array $eagerLoading whether to eager load the relations.
512
     * Note, that this does not mean, that the relations are populated from the
513
     * query result. An extra query will still be performed to bring in the
514
     * related data.
515
     * @return $this the query object itself
516
     * @see joinWith()
517
     */
518 48
    public function innerJoinWith($with, $eagerLoading = true)
519
    {
520 48
        return $this->joinWith($with, $eagerLoading, 'INNER JOIN');
521
    }
522
523
    /**
524
     * Modifies the current query by adding join fragments based on the given relations.
525
     * @param ActiveRecord $model the primary model
526
     * @param array $with the relations to be joined
527
     * @param string|array $joinType the join type
528
     */
529 87
    private function joinWithRelations($model, $with, $joinType)
530
    {
531 87
        $relations = [];
532
533 87
        foreach ($with as $name => $callback) {
534 87
            if (is_int($name)) {
535 81
                $name = $callback;
536 81
                $callback = null;
537
            }
538
539 87
            $primaryModel = $model;
540 87
            $parent = $this;
541 87
            $prefix = '';
542 87
            while (($pos = strpos($name, '.')) !== false) {
543 15
                $childName = substr($name, $pos + 1);
544 15
                $name = substr($name, 0, $pos);
545 15
                $fullName = $prefix === '' ? $name : "$prefix.$name";
546 15
                if (!isset($relations[$fullName])) {
547 9
                    $relations[$fullName] = $relation = $primaryModel->getRelation($name);
548 9
                    $this->joinWithRelation($parent, $relation, $this->getJoinType($joinType, $fullName));
549
                } else {
550 6
                    $relation = $relations[$fullName];
551
                }
552
                /* @var $relationModelClass ActiveRecordInterface */
553 15
                $relationModelClass = $relation->modelClass;
0 ignored issues
show
Accessing modelClass on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
554 15
                $primaryModel = $relationModelClass::instance();
555 15
                $parent = $relation;
556 15
                $prefix = $fullName;
557 15
                $name = $childName;
558
            }
559
560 87
            $fullName = $prefix === '' ? $name : "$prefix.$name";
561 87
            if (!isset($relations[$fullName])) {
562 87
                $relations[$fullName] = $relation = $primaryModel->getRelation($name);
563 87
                if ($callback !== null) {
564 39
                    call_user_func($callback, $relation);
0 ignored issues
show
$callback of type null is incompatible with the type callable expected by parameter $callback of call_user_func(). ( Ignorable by Annotation )

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

564
                    call_user_func(/** @scrutinizer ignore-type */ $callback, $relation);
Loading history...
565
                }
566 87
                if (!empty($relation->joinWith)) {
0 ignored issues
show
Accessing joinWith on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
567 9
                    $relation->buildJoinWith();
0 ignored issues
show
The method buildJoinWith() does not exist on yii\db\ActiveQueryInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to yii\db\ActiveQueryInterface. ( Ignorable by Annotation )

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

567
                    $relation->/** @scrutinizer ignore-call */ 
568
                               buildJoinWith();
Loading history...
568
                }
569 87
                $this->joinWithRelation($parent, $relation, $this->getJoinType($joinType, $fullName));
570
            }
571
        }
572
    }
573
574
    /**
575
     * Returns the join type based on the given join type parameter and the relation name.
576
     * @param string|array $joinType the given join type(s)
577
     * @param string $name relation name
578
     * @return string the real join type
579
     */
580 87
    private function getJoinType($joinType, $name)
581
    {
582 87
        if (is_array($joinType) && isset($joinType[$name])) {
583
            return $joinType[$name];
584
        }
585
586 87
        return is_string($joinType) ? $joinType : 'INNER JOIN';
587
    }
588
589
    /**
590
     * Returns the table name and the table alias for [[modelClass]].
591
     * @return array the table name and the table alias.
592
     * @since 2.0.16
593
     */
594 138
    protected function getTableNameAndAlias()
595
    {
596 138
        if (empty($this->from)) {
597 132
            $tableName = $this->getPrimaryTableName();
598
        } else {
599 87
            $tableName = '';
600
            // if the first entry in "from" is an alias-tablename-pair return it directly
601 87
            foreach ($this->from as $alias => $tableName) {
602 87
                if (is_string($alias)) {
603 24
                    return [$tableName, $alias];
604
                }
605 78
                break;
606
            }
607
        }
608
609 135
        if (preg_match('/^(.*?)\s+({{\w+}}|\w+)$/', $tableName, $matches)) {
610 6
            $alias = $matches[2];
611
        } else {
612 135
            $alias = $tableName;
613
        }
614
615 135
        return [$tableName, $alias];
616
    }
617
618
    /**
619
     * Joins a parent query with a child query.
620
     * The current query object will be modified accordingly.
621
     * @param ActiveQuery $parent
622
     * @param ActiveQuery $child
623
     * @param string $joinType
624
     */
625 87
    private function joinWithRelation($parent, $child, $joinType)
626
    {
627 87
        $via = $child->via;
628 87
        $child->via = null;
629 87
        if ($via instanceof self) {
630
            // via table
631 9
            $this->joinWithRelation($parent, $via, $joinType);
632 9
            $this->joinWithRelation($via, $child, $joinType);
633 9
            return;
634 87
        } elseif (is_array($via)) {
635
            // via relation
636 27
            $this->joinWithRelation($parent, $via[1], $joinType);
637 27
            $this->joinWithRelation($via[1], $child, $joinType);
638 27
            return;
639
        }
640
641 87
        list($parentTable, $parentAlias) = $parent->getTableNameAndAlias();
642 87
        list($childTable, $childAlias) = $child->getTableNameAndAlias();
643
644 87
        if (!empty($child->link)) {
645 87
            if (strpos($parentAlias, '{{') === false) {
646 81
                $parentAlias = '{{' . $parentAlias . '}}';
647
            }
648 87
            if (strpos($childAlias, '{{') === false) {
649 87
                $childAlias = '{{' . $childAlias . '}}';
650
            }
651
652 87
            $on = [];
653 87
            foreach ($child->link as $childColumn => $parentColumn) {
654 87
                $on[] = "$parentAlias.[[$parentColumn]] = $childAlias.[[$childColumn]]";
655
            }
656 87
            $on = implode(' AND ', $on);
657 87
            if (!empty($child->on)) {
658 87
                $on = ['and', $on, $child->on];
659
            }
660
        } else {
661
            $on = $child->on;
662
        }
663 87
        $this->join($joinType, empty($child->from) ? $childTable : $child->from, $on);
664
665 87
        if (!empty($child->where)) {
666 21
            $this->andWhere($child->where);
667
        }
668 87
        if (!empty($child->having)) {
669
            $this->andHaving($child->having);
670
        }
671 87
        if (!empty($child->orderBy)) {
672 30
            $this->addOrderBy($child->orderBy);
673
        }
674 87
        if (!empty($child->groupBy)) {
675
            $this->addGroupBy($child->groupBy);
676
        }
677 87
        if (!empty($child->params)) {
678
            $this->addParams($child->params);
679
        }
680 87
        if (!empty($child->join)) {
681 9
            foreach ($child->join as $join) {
682 9
                $this->join[] = $join;
683
            }
684
        }
685 87
        if (!empty($child->union)) {
686
            foreach ($child->union as $union) {
687
                $this->union[] = $union;
688
            }
689
        }
690
    }
691
692
    /**
693
     * Sets the ON condition for a relational query.
694
     * The condition will be used in the ON part when [[ActiveQuery::joinWith()]] is called.
695
     * Otherwise, the condition will be used in the WHERE part of a query.
696
     *
697
     * Use this method to specify additional conditions when declaring a relation in the [[ActiveRecord]] class:
698
     *
699
     * ```php
700
     * public function getActiveUsers()
701
     * {
702
     *     return $this->hasMany(User::class, ['id' => 'user_id'])
703
     *                 ->onCondition(['active' => true]);
704
     * }
705
     * ```
706
     *
707
     * Note that this condition is applied in case of a join as well as when fetching the related records.
708
     * Thus only fields of the related table can be used in the condition. Trying to access fields of the primary
709
     * record will cause an error in a non-join-query.
710
     *
711
     * @param string|array $condition the ON condition. Please refer to [[Query::where()]] on how to specify this parameter.
712
     * @param array $params the parameters (name => value) to be bound to the query.
713
     * @return $this the query object itself
714
     */
715 21
    public function onCondition($condition, $params = [])
716
    {
717 21
        $this->on = $condition;
718 21
        $this->addParams($params);
719 21
        return $this;
720
    }
721
722
    /**
723
     * Adds an additional ON condition to the existing one.
724
     * The new condition and the existing one will be joined using the 'AND' operator.
725
     * @param string|array $condition the new ON condition. Please refer to [[where()]]
726
     * on how to specify this parameter.
727
     * @param array $params the parameters (name => value) to be bound to the query.
728
     * @return $this the query object itself
729
     * @see onCondition()
730
     * @see orOnCondition()
731
     */
732 12
    public function andOnCondition($condition, $params = [])
733
    {
734 12
        if ($this->on === null) {
735 9
            $this->on = $condition;
736
        } else {
737 3
            $this->on = ['and', $this->on, $condition];
738
        }
739 12
        $this->addParams($params);
740 12
        return $this;
741
    }
742
743
    /**
744
     * Adds an additional ON condition to the existing one.
745
     * The new condition and the existing one will be joined using the 'OR' operator.
746
     * @param string|array $condition the new ON condition. Please refer to [[where()]]
747
     * on how to specify this parameter.
748
     * @param array $params the parameters (name => value) to be bound to the query.
749
     * @return $this the query object itself
750
     * @see onCondition()
751
     * @see andOnCondition()
752
     */
753 6
    public function orOnCondition($condition, $params = [])
754
    {
755 6
        if ($this->on === null) {
756 3
            $this->on = $condition;
757
        } else {
758 3
            $this->on = ['or', $this->on, $condition];
759
        }
760 6
        $this->addParams($params);
761 6
        return $this;
762
    }
763
764
    /**
765
     * Specifies the junction table for a relational query.
766
     *
767
     * Use this method to specify a junction table when declaring a relation in the [[ActiveRecord]] class:
768
     *
769
     * ```php
770
     * public function getItems()
771
     * {
772
     *     return $this->hasMany(Item::class, ['id' => 'item_id'])
773
     *                 ->viaTable('order_item', ['order_id' => 'id']);
774
     * }
775
     * ```
776
     *
777
     * @param string $tableName the name of the junction table.
778
     * @param array $link the link between the junction table and the table associated with [[primaryModel]].
779
     * The keys of the array represent the columns in the junction table, and the values represent the columns
780
     * in the [[primaryModel]] table.
781
     * @param callable|null $callable a PHP callback for customizing the relation associated with the junction table.
782
     * Its signature should be `function($query)`, where `$query` is the query to be customized.
783
     * @return $this the query object itself
784
     * @throws InvalidConfigException when query is not initialized properly
785
     * @see via()
786
     */
787 24
    public function viaTable($tableName, $link, ?callable $callable = null)
788
    {
789 24
        $modelClass = $this->primaryModel ? get_class($this->primaryModel) : $this->modelClass;
790 24
        $relation = new self($modelClass, [
791 24
            'from' => [$tableName],
792 24
            'link' => $link,
793 24
            'multiple' => true,
794 24
            'asArray' => true,
795 24
        ]);
796 24
        $this->via = $relation;
797 24
        if ($callable !== null) {
798 6
            call_user_func($callable, $relation);
799
        }
800
801 24
        return $this;
802
    }
803
804
    /**
805
     * Define an alias for the table defined in [[modelClass]].
806
     *
807
     * This method will adjust [[from]] so that an already defined alias will be overwritten.
808
     * If none was defined, [[from]] will be populated with the given alias.
809
     *
810
     * @param string $alias the table alias.
811
     * @return $this the query object itself
812
     * @since 2.0.7
813
     */
814 63
    public function alias($alias)
815
    {
816 63
        if (empty($this->from) || count($this->from) < 2) {
817 63
            list($tableName) = $this->getTableNameAndAlias();
818 63
            $this->from = [$alias => $tableName];
819
        } else {
820 3
            $tableName = $this->getPrimaryTableName();
821
822 3
            foreach ($this->from as $key => $table) {
823 3
                if ($table === $tableName) {
824 3
                    unset($this->from[$key]);
825 3
                    $this->from[$alias] = $tableName;
826
                }
827
            }
828
        }
829
830 63
        return $this;
831
    }
832
833
    /**
834
     * {@inheritdoc}
835
     * @since 2.0.12
836
     */
837 257
    public function getTablesUsedInFrom()
838
    {
839 257
        if (empty($this->from)) {
840 173
            return $this->cleanUpTableNames([$this->getPrimaryTableName()]);
841
        }
842
843 84
        return parent::getTablesUsedInFrom();
844
    }
845
846
    /**
847
     * @return string primary table name
848
     * @since 2.0.12
849
     */
850 562
    protected function getPrimaryTableName()
851
    {
852
        /* @var $modelClass ActiveRecord */
853 562
        $modelClass = $this->modelClass;
854 562
        return $modelClass::tableName();
855
    }
856
}
857