Passed
Pull Request — master (#19805)
by Rutger
09:09
created

ActiveQuery::prepare()   F

Complexity

Conditions 29
Paths 704

Size

Total Lines 136
Code Lines 79

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 33
CRAP Score 167.3247

Importance

Changes 0
Metric Value
cc 29
eloc 79
c 0
b 0
f 0
nc 704
nop 1
dl 0
loc 136
ccs 33
cts 73
cp 0.4521
crap 167.3247
rs 0.4111

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @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
    public $viaJoined = false;
102
103
    protected $viaJoinedApplied = false;
104
105
    /**
106
     * Constructor.
107
     * @param string $modelClass the model class associated with this query
108
     * @param array $config configurations to be applied to the newly created query object
109
     */
110 648
    public function __construct($modelClass, $config = [])
111
    {
112 648
        $this->modelClass = $modelClass;
113 648
        parent::__construct($config);
114 648
    }
115
116
    /**
117
     * Initializes the object.
118
     * This method is called at the end of the constructor. The default implementation will trigger
119
     * an [[EVENT_INIT]] event. If you override this method, make sure you call the parent implementation at the end
120
     * to ensure triggering of the event.
121
     */
122 648
    public function init()
123
    {
124 648
        parent::init();
125 648
        $this->trigger(self::EVENT_INIT);
126 648
    }
127
128
    /**
129
     * Executes query and returns all results as an array.
130
     * @param Connection|null $db the DB connection used to create the DB command.
131
     * If null, the DB connection returned by [[modelClass]] will be used.
132
     * @return array|ActiveRecord[] the query results. If the query results in nothing, an empty array will be returned.
133
     */
134 248
    public function all($db = null)
135
    {
136 248
        return parent::all($db);
137
    }
138
139
    /**
140
     * {@inheritdoc}
141
     */
142 502
    public function prepare($builder)
143
    {
144
        // NOTE: because the same ActiveQuery may be used to build different SQL statements
145
        // (e.g. by ActiveDataProvider, one for count query, the other for row data query,
146
        // it is important to make sure the same ActiveQuery can be used to build SQL statements
147
        // multiple times.
148 502
        if (!empty($this->joinWith)) {
149 84
            $this->buildJoinWith();
150 84
            $this->joinWith = null;    // clean it up to avoid issue https://github.com/yiisoft/yii2/issues/2687
151
        }
152
153 502
        if (empty($this->from)) {
154 478
            $this->from = [$this->getPrimaryTableName()];
155
        }
156
157 502
        if (empty($this->select) && !empty($this->join)) {
158 75
            list(, $alias) = $this->getTableNameAndAlias();
159 75
            $this->select = ["$alias.*"];
160
        }
161
162 502
        if ($this->primaryModel === null) {
163
            // eager loading
164 495
            $query = Query::create($this);
165
        } else {
166
            // lazy loading of a relation
167 124
            $where = $this->where;
168
169
            if (
170 124
                $this->viaJoined
171
                && ($this->via instanceof self
172 124
                    || is_array($this->via)
173
                )
174
            ) {
175
                /* @var $viaQuery ActiveQuery */
176
                $viaQuery = ($this->via instanceof self) ? $this->via : $this->via[1];
177
178
                list(, $tableAlias) = $this->getTableNameAndAlias();
179
                if (strpos($tableAlias, '{{') === false) {
180
                    $tableAlias = '{{' . $tableAlias . '}}';
181
                }
182
183
                if (empty($this->select)) {
184
                    $this->select = ["$tableAlias.*"];
185
                }
186
187
                if (empty($this->groupBy) && !$this->distinct) {
188
                    $this->groupBy(array_map(
189
                        function($column) use ($tableAlias) {
190
                            return "$tableAlias.[[$column]]";
191
                        },
192
                        array_keys($this->link)
193
                    ));
194
                }
195
196
                if (!$this->viaJoinedApplied) {
197
                    // Setup first 'via' relation in the chain based on initial link.
198
                    $previousRelation = $this;
199
                    $previousLink = $this->link;
200
                    while ($viaQuery) { // Loop over each 'via' chain while we got links.
201
                        if ($viaQuery->via) { // Check if we've got another 'via' link.
202
                            $nextViaQuery = ($viaQuery->via instanceof self) ? $viaQuery->via : $viaQuery->via[1];
203
                            $viaQuery->via = null;
204
                        } else {
205
                            // End of 'via' chain
206
                            $nextViaQuery = null;
207
208
                            // Get table alias for final junction table
209
                            list(, $viaAlias) = $viaQuery->getTableNameAndAlias();
210
                            if (strpos($viaAlias, '{{') === false) {
211
                                $viaAlias = '{{' . $viaAlias . '}}';
212
                            }
213
                            // Apply primary model relation as additional inner join `on` conditions for the final junction table.
214
                            foreach ($viaQuery->link as $primaryColumn => $viaColumn) {
215
                                $viaQuery->andOnCondition([
216
                                    "$viaAlias.[[$primaryColumn]]" => $this->primaryModel->getAttribute($viaColumn)
217
                                ]);
218
                            }
219
                        }
220
221
                        $nextLink = $viaQuery->link; // Store the current 'via' link for the next link.
222
                        $viaQuery->link = array_flip($previousLink); // Use the inverted previous link as the current one.
223
224
                        // Join the 'via' link on the previous link
225
                        $this->joinWithRelation($previousRelation, $viaQuery, 'INNER JOIN');
226
227
                        // Setup data for next iteration
228
                        $previousRelation = $viaQuery;
229
                        $viaQuery = $nextViaQuery;
230
                        $previousLink = $nextLink;
231
                    }
232
233
                    // Prevent duplicate application of the join(s) e.g. for ActiveDataProvider
234
                    $this->viaJoinedApplied = true;
235
                }
236 124
            } elseif ($this->via instanceof self) {
237
                // via junction table
238 15
                $viaModels = $this->via->findJunctionRows([$this->primaryModel]);
239 15
                $this->filterByModels($viaModels);
240 115
            } elseif (is_array($this->via)) {
241
                // via relation
242
                /* @var $viaQuery ActiveQuery */
243 42
                list($viaName, $viaQuery, $viaCallableUsed) = $this->via;
244 42
                if ($viaQuery->multiple) {
245 42
                    if ($viaCallableUsed) {
246 24
                        $viaModels = $viaQuery->all();
247 21
                    } elseif ($this->primaryModel->isRelationPopulated($viaName)) {
248 3
                        $viaModels = $this->primaryModel->$viaName;
249
                    } else {
250 21
                        $viaModels = $viaQuery->all();
251 42
                        $this->primaryModel->populateRelation($viaName, $viaModels);
252
                    }
253
                } else {
254
                    if ($viaCallableUsed) {
255
                        $model = $viaQuery->one();
256
                    } elseif ($this->primaryModel->isRelationPopulated($viaName)) {
257
                        $model = $this->primaryModel->$viaName;
258
                    } else {
259
                        $model = $viaQuery->one();
260
                        $this->primaryModel->populateRelation($viaName, $model);
261
                    }
262
                    $viaModels = $model === null ? [] : [$model];
263
                }
264 42
                $this->filterByModels($viaModels);
265
            } else {
266 115
                $this->filterByModels([$this->primaryModel]);
267
            }
268
269 124
            $query = Query::create($this);
270 124
            $this->where = $where;
271
        }
272
273 502
        if (!empty($this->on)) {
274 24
            $query->andWhere($this->on);
275
        }
276
277 502
        return $query;
278
    }
279
280
    /**
281
     * {@inheritdoc}
282
     */
283 405
    public function populate($rows)
284
    {
285 405
        if (empty($rows)) {
286 75
            return [];
287
        }
288
289 391
        $models = $this->createModels($rows);
290 391
        if (!empty($this->join) && $this->indexBy === null) {
291 69
            $models = $this->removeDuplicatedModels($models);
292
        }
293 391
        if (!empty($this->with)) {
294 129
            $this->findWith($this->with, $models);
295
        }
296
297 391
        if ($this->inverseOf !== null) {
298 12
            $this->addInverseRelations($models);
299
        }
300
301 391
        if (!$this->asArray) {
302 381
            foreach ($models as $model) {
303 381
                $model->afterFind();
304
            }
305
        }
306
307 391
        return parent::populate($models);
308
    }
309
310
    /**
311
     * Removes duplicated models by checking their primary key values.
312
     * This method is mainly called when a join query is performed, which may cause duplicated rows being returned.
313
     * @param array $models the models to be checked
314
     * @throws InvalidConfigException if model primary key is empty
315
     * @return array the distinctive models
316
     */
317 69
    private function removeDuplicatedModels($models)
318
    {
319 69
        $hash = [];
320
        /* @var $class ActiveRecord */
321 69
        $class = $this->modelClass;
322 69
        $pks = $class::primaryKey();
323
324 69
        if (count($pks) > 1) {
325
            // composite primary key
326 6
            foreach ($models as $i => $model) {
327 6
                $key = [];
328 6
                foreach ($pks as $pk) {
329 6
                    if (!isset($model[$pk])) {
330
                        // do not continue if the primary key is not part of the result set
331 3
                        break 2;
332
                    }
333 6
                    $key[] = $model[$pk];
334
                }
335 3
                $key = serialize($key);
336 3
                if (isset($hash[$key])) {
337
                    unset($models[$i]);
338
                } else {
339 3
                    $hash[$key] = true;
340
                }
341
            }
342
        } elseif (empty($pks)) {
343
            throw new InvalidConfigException("Primary key of '{$class}' can not be empty.");
344
        } else {
345
            // single column primary key
346 66
            $pk = reset($pks);
347 66
            foreach ($models as $i => $model) {
348 66
                if (!isset($model[$pk])) {
349
                    // do not continue if the primary key is not part of the result set
350 3
                    break;
351
                }
352 63
                $key = $model[$pk];
353 63
                if (isset($hash[$key])) {
354 24
                    unset($models[$i]);
355 63
                } elseif ($key !== null) {
356 63
                    $hash[$key] = true;
357
                }
358
            }
359
        }
360
361 69
        return array_values($models);
362
    }
363
364
    /**
365
     * Executes query and returns a single row of result.
366
     * @param Connection|null $db the DB connection used to create the DB command.
367
     * If `null`, the DB connection returned by [[modelClass]] will be used.
368
     * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]],
369
     * the query result may be either an array or an ActiveRecord object. `null` will be returned
370
     * if the query results in nothing.
371
     */
372 307
    public function one($db = null)
373
    {
374 307
        $row = parent::one($db);
375 307
        if ($row !== false) {
376 302
            $models = $this->populate([$row]);
377 302
            return reset($models) ?: null;
378
        }
379
380 29
        return null;
381
    }
382
383
    /**
384
     * Creates a DB command that can be used to execute this query.
385
     * @param Connection|null $db the DB connection used to create the DB command.
386
     * If `null`, the DB connection returned by [[modelClass]] will be used.
387
     * @return Command the created DB command instance.
388
     */
389 470
    public function createCommand($db = null)
390
    {
391
        /* @var $modelClass ActiveRecord */
392 470
        $modelClass = $this->modelClass;
393 470
        if ($db === null) {
394 459
            $db = $modelClass::getDb();
395
        }
396
397 470
        if ($this->sql === null) {
398 467
            list($sql, $params) = $db->getQueryBuilder()->build($this);
399
        } else {
400 3
            $sql = $this->sql;
401 3
            $params = $this->params;
402
        }
403
404 470
        $command = $db->createCommand($sql, $params);
405 470
        $this->setCommandCache($command);
406
407 470
        return $command;
408
    }
409
410
    /**
411
     * {@inheritdoc}
412
     */
413 65
    protected function queryScalar($selectExpression, $db)
414
    {
415
        /* @var $modelClass ActiveRecord */
416 65
        $modelClass = $this->modelClass;
417 65
        if ($db === null) {
418 64
            $db = $modelClass::getDb();
419
        }
420
421 65
        if ($this->sql === null) {
422 62
            return parent::queryScalar($selectExpression, $db);
423
        }
424
425 3
        $command = (new Query())->select([$selectExpression])
426 3
            ->from(['c' => "({$this->sql})"])
427 3
            ->params($this->params)
428 3
            ->createCommand($db);
429 3
        $this->setCommandCache($command);
430
431 3
        return $command->queryScalar();
432
    }
433
434
    /**
435
     * Joins with the specified relations.
436
     *
437
     * This method allows you to reuse existing relation definitions to perform JOIN queries.
438
     * Based on the definition of the specified relation(s), the method will append one or multiple
439
     * JOIN statements to the current query.
440
     *
441
     * If the `$eagerLoading` parameter is true, the method will also perform eager loading for the specified relations,
442
     * which is equivalent to calling [[with()]] using the specified relations.
443
     *
444
     * Note that because a JOIN query will be performed, you are responsible to disambiguate column names.
445
     *
446
     * This method differs from [[with()]] in that it will build up and execute a JOIN SQL statement
447
     * for the primary table. And when `$eagerLoading` is true, it will call [[with()]] in addition with the specified relations.
448
     *
449
     * @param string|array $with the relations to be joined. This can either be a string, representing a relation name or
450
     * an array with the following semantics:
451
     *
452
     * - Each array element represents a single relation.
453
     * - You may specify the relation name as the array key and provide an anonymous functions that
454
     *   can be used to modify the relation queries on-the-fly as the array value.
455
     * - If a relation query does not need modification, you may use the relation name as the array value.
456
     *
457
     * The relation name may optionally contain an alias for the relation table (e.g. `books b`).
458
     *
459
     * Sub-relations can also be specified, see [[with()]] for the syntax.
460
     *
461
     * In the following you find some examples:
462
     *
463
     * ```php
464
     * // find all orders that contain books, and eager loading "books"
465
     * Order::find()->joinWith('books', true, 'INNER JOIN')->all();
466
     * // find all orders, eager loading "books", and sort the orders and books by the book names.
467
     * Order::find()->joinWith([
468
     *     'books' => function (\yii\db\ActiveQuery $query) {
469
     *         $query->orderBy('item.name');
470
     *     }
471
     * ])->all();
472
     * // find all orders that contain books of the category 'Science fiction', using the alias "b" for the books table
473
     * Order::find()->joinWith(['books b'], true, 'INNER JOIN')->where(['b.category' => 'Science fiction'])->all();
474
     * ```
475
     *
476
     * The alias syntax is available since version 2.0.7.
477
     *
478
     * @param bool|array $eagerLoading whether to eager load the relations
479
     * specified in `$with`.  When this is a boolean, it applies to all
480
     * relations specified in `$with`. Use an array to explicitly list which
481
     * relations in `$with` need to be eagerly loaded.  Note, that this does
482
     * not mean, that the relations are populated from the query result. An
483
     * extra query will still be performed to bring in the related data.
484
     * Defaults to `true`.
485
     * @param string|array $joinType the join type of the relations specified in `$with`.
486
     * When this is a string, it applies to all relations specified in `$with`. Use an array
487
     * in the format of `relationName => joinType` to specify different join types for different relations.
488
     * @return $this the query object itself
489
     */
490 93
    public function joinWith($with, $eagerLoading = true, $joinType = 'LEFT JOIN')
491
    {
492 93
        $relations = [];
493 93
        foreach ((array) $with as $name => $callback) {
494 93
            if (is_int($name)) {
495 90
                $name = $callback;
496 90
                $callback = null;
497
            }
498
499 93
            if (preg_match('/^(.*?)(?:\s+AS\s+|\s+)(\w+)$/i', $name, $matches)) {
500
                // relation is defined with an alias, adjust callback to apply alias
501 18
                list(, $relation, $alias) = $matches;
502 18
                $name = $relation;
503 18
                $callback = function ($query) use ($callback, $alias) {
504
                    /* @var $query ActiveQuery */
505 18
                    $query->alias($alias);
506 18
                    if ($callback !== null) {
507 12
                        call_user_func($callback, $query);
508
                    }
509 18
                };
510
            }
511
512 93
            if ($callback === null) {
513 87
                $relations[] = $name;
514
            } else {
515 39
                $relations[$name] = $callback;
516
            }
517
        }
518 93
        $this->joinWith[] = [$relations, $eagerLoading, $joinType];
519 93
        return $this;
520
    }
521
522 87
    private function buildJoinWith()
523
    {
524 87
        $join = $this->join;
525 87
        $this->join = [];
526
527
        /* @var $modelClass ActiveRecordInterface */
528 87
        $modelClass = $this->modelClass;
529 87
        $model = $modelClass::instance();
530 87
        foreach ($this->joinWith as $config) {
531 87
            list($with, $eagerLoading, $joinType) = $config;
532 87
            $this->joinWithRelations($model, $with, $joinType);
533
534 87
            if (is_array($eagerLoading)) {
535
                foreach ($with as $name => $callback) {
536
                    if (is_int($name)) {
537
                        if (!in_array($callback, $eagerLoading, true)) {
538
                            unset($with[$name]);
539
                        }
540
                    } elseif (!in_array($name, $eagerLoading, true)) {
541
                        unset($with[$name]);
542
                    }
543
                }
544 87
            } elseif (!$eagerLoading) {
545 18
                $with = [];
546
            }
547
548 87
            $this->with($with);
549
        }
550
551
        // remove duplicated joins added by joinWithRelations that may be added
552
        // e.g. when joining a relation and a via relation at the same time
553 87
        $uniqueJoins = [];
554 87
        foreach ($this->join as $j) {
555 87
            $uniqueJoins[serialize($j)] = $j;
556
        }
557 87
        $this->join = array_values($uniqueJoins);
558
559
        // https://github.com/yiisoft/yii2/issues/16092
560 87
        $uniqueJoinsByTableName = [];
561 87
        foreach ($this->join as $config) {
562 87
            $tableName = serialize($config[1]);
563 87
            if (!array_key_exists($tableName, $uniqueJoinsByTableName)) {
564 87
                $uniqueJoinsByTableName[$tableName] = $config;
565
            }
566
        }
567 87
        $this->join = array_values($uniqueJoinsByTableName);
568
569 87
        if (!empty($join)) {
570
            // append explicit join to joinWith()
571
            // https://github.com/yiisoft/yii2/issues/2880
572
            $this->join = empty($this->join) ? $join : array_merge($this->join, $join);
573
        }
574 87
    }
575
576
    /**
577
     * Inner joins with the specified relations.
578
     * This is a shortcut method to [[joinWith()]] with the join type set as "INNER JOIN".
579
     * Please refer to [[joinWith()]] for detailed usage of this method.
580
     * @param string|array $with the relations to be joined with.
581
     * @param bool|array $eagerLoading whether to eager load the relations.
582
     * Note, that this does not mean, that the relations are populated from the
583
     * query result. An extra query will still be performed to bring in the
584
     * related data.
585
     * @return $this the query object itself
586
     * @see joinWith()
587
     */
588 48
    public function innerJoinWith($with, $eagerLoading = true)
589
    {
590 48
        return $this->joinWith($with, $eagerLoading, 'INNER JOIN');
591
    }
592
593
    /**
594
     * Modifies the current query by adding join fragments based on the given relations.
595
     * @param ActiveRecord $model the primary model
596
     * @param array $with the relations to be joined
597
     * @param string|array $joinType the join type
598
     */
599 87
    private function joinWithRelations($model, $with, $joinType)
600
    {
601 87
        $relations = [];
602
603 87
        foreach ($with as $name => $callback) {
604 87
            if (is_int($name)) {
605 81
                $name = $callback;
606 81
                $callback = null;
607
            }
608
609 87
            $primaryModel = $model;
610 87
            $parent = $this;
611 87
            $prefix = '';
612 87
            while (($pos = strpos($name, '.')) !== false) {
613 15
                $childName = substr($name, $pos + 1);
614 15
                $name = substr($name, 0, $pos);
615 15
                $fullName = $prefix === '' ? $name : "$prefix.$name";
616 15
                if (!isset($relations[$fullName])) {
617 9
                    $relations[$fullName] = $relation = $primaryModel->getRelation($name);
618 9
                    $this->joinWithRelation($parent, $relation, $this->getJoinType($joinType, $fullName));
619
                } else {
620 6
                    $relation = $relations[$fullName];
621
                }
622
                /* @var $relationModelClass ActiveRecordInterface */
623 15
                $relationModelClass = $relation->modelClass;
0 ignored issues
show
Bug introduced by
Accessing modelClass on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
624 15
                $primaryModel = $relationModelClass::instance();
625 15
                $parent = $relation;
626 15
                $prefix = $fullName;
627 15
                $name = $childName;
628
            }
629
630 87
            $fullName = $prefix === '' ? $name : "$prefix.$name";
631 87
            if (!isset($relations[$fullName])) {
632 87
                $relations[$fullName] = $relation = $primaryModel->getRelation($name);
633 87
                if ($callback !== null) {
634 39
                    call_user_func($callback, $relation);
0 ignored issues
show
Bug introduced by
$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

634
                    call_user_func(/** @scrutinizer ignore-type */ $callback, $relation);
Loading history...
635
                }
636 87
                if (!empty($relation->joinWith)) {
0 ignored issues
show
Bug introduced by
Accessing joinWith on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
637 9
                    $relation->buildJoinWith();
0 ignored issues
show
Bug introduced by
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

637
                    $relation->/** @scrutinizer ignore-call */ 
638
                               buildJoinWith();
Loading history...
638
                }
639 87
                $this->joinWithRelation($parent, $relation, $this->getJoinType($joinType, $fullName));
640
            }
641
        }
642 87
    }
643
644
    /**
645
     * Returns the join type based on the given join type parameter and the relation name.
646
     * @param string|array $joinType the given join type(s)
647
     * @param string $name relation name
648
     * @return string the real join type
649
     */
650 87
    private function getJoinType($joinType, $name)
651
    {
652 87
        if (is_array($joinType) && isset($joinType[$name])) {
653
            return $joinType[$name];
654
        }
655
656 87
        return is_string($joinType) ? $joinType : 'INNER JOIN';
657
    }
658
659
    /**
660
     * Returns the table name and the table alias for [[modelClass]].
661
     * @return array the table name and the table alias.
662
     * @since 2.0.16
663
     */
664 138
    protected function getTableNameAndAlias()
665
    {
666 138
        if (empty($this->from)) {
667 132
            $tableName = $this->getPrimaryTableName();
668
        } else {
669 87
            $tableName = '';
670
            // if the first entry in "from" is an alias-tablename-pair return it directly
671 87
            foreach ($this->from as $alias => $tableName) {
672 87
                if (is_string($alias)) {
673 24
                    return [$tableName, $alias];
674
                }
675 78
                break;
676
            }
677
        }
678
679 135
        if (preg_match('/^(.*?)\s+({{\w+}}|\w+)$/', $tableName, $matches)) {
680 6
            $alias = $matches[2];
681
        } else {
682 135
            $alias = $tableName;
683
        }
684
685 135
        return [$tableName, $alias];
686
    }
687
688
    /**
689
     * Joins a parent query with a child query.
690
     * The current query object will be modified accordingly.
691
     * @param ActiveQuery $parent
692
     * @param ActiveQuery $child
693
     * @param string $joinType
694
     */
695 87
    private function joinWithRelation($parent, $child, $joinType)
696
    {
697 87
        $via = $child->via;
698 87
        $child->via = null;
699 87
        if ($via instanceof self) {
700
            // via table
701 9
            $this->joinWithRelation($parent, $via, $joinType);
702 9
            $this->joinWithRelation($via, $child, $joinType);
703 9
            return;
704 87
        } elseif (is_array($via)) {
705
            // via relation
706 27
            $this->joinWithRelation($parent, $via[1], $joinType);
707 27
            $this->joinWithRelation($via[1], $child, $joinType);
708 27
            return;
709
        }
710
711 87
        list($parentTable, $parentAlias) = $parent->getTableNameAndAlias();
712 87
        list($childTable, $childAlias) = $child->getTableNameAndAlias();
713
714 87
        if (!empty($child->link)) {
715 87
            if (strpos($parentAlias, '{{') === false) {
716 81
                $parentAlias = '{{' . $parentAlias . '}}';
717
            }
718 87
            if (strpos($childAlias, '{{') === false) {
719 87
                $childAlias = '{{' . $childAlias . '}}';
720
            }
721
722 87
            $on = [];
723 87
            foreach ($child->link as $childColumn => $parentColumn) {
724 87
                $on[] = "$parentAlias.[[$parentColumn]] = $childAlias.[[$childColumn]]";
725
            }
726 87
            $on = implode(' AND ', $on);
727 87
            if (!empty($child->on)) {
728 87
                $on = ['and', $on, $child->on];
729
            }
730
        } else {
731
            $on = $child->on;
732
        }
733 87
        $this->join($joinType, empty($child->from) ? $childTable : $child->from, $on);
734
735 87
        if (!empty($child->where)) {
736 21
            $this->andWhere($child->where);
737
        }
738 87
        if (!empty($child->having)) {
739
            $this->andHaving($child->having);
740
        }
741 87
        if (!empty($child->orderBy)) {
742 30
            $this->addOrderBy($child->orderBy);
743
        }
744 87
        if (!empty($child->groupBy)) {
745
            $this->addGroupBy($child->groupBy);
746
        }
747 87
        if (!empty($child->params)) {
748
            $this->addParams($child->params);
749
        }
750 87
        if (!empty($child->join)) {
751 9
            foreach ($child->join as $join) {
752 9
                $this->join[] = $join;
753
            }
754
        }
755 87
        if (!empty($child->union)) {
756
            foreach ($child->union as $union) {
757
                $this->union[] = $union;
758
            }
759
        }
760 87
    }
761
762
    /**
763
     * Sets the ON condition for a relational query.
764
     * The condition will be used in the ON part when [[ActiveQuery::joinWith()]] is called.
765
     * Otherwise, the condition will be used in the WHERE part of a query.
766
     *
767
     * Use this method to specify additional conditions when declaring a relation in the [[ActiveRecord]] class:
768
     *
769
     * ```php
770
     * public function getActiveUsers()
771
     * {
772
     *     return $this->hasMany(User::class, ['id' => 'user_id'])
773
     *                 ->onCondition(['active' => true]);
774
     * }
775
     * ```
776
     *
777
     * Note that this condition is applied in case of a join as well as when fetching the related records.
778
     * Thus only fields of the related table can be used in the condition. Trying to access fields of the primary
779
     * record will cause an error in a non-join-query.
780
     *
781
     * @param string|array $condition the ON condition. Please refer to [[Query::where()]] on how to specify this parameter.
782
     * @param array $params the parameters (name => value) to be bound to the query.
783
     * @return $this the query object itself
784
     */
785 21
    public function onCondition($condition, $params = [])
786
    {
787 21
        $this->on = $condition;
788 21
        $this->addParams($params);
789 21
        return $this;
790
    }
791
792
    /**
793
     * Adds an additional ON condition to the existing one.
794
     * The new condition and the existing one will be joined using the 'AND' operator.
795
     * @param string|array $condition the new ON condition. Please refer to [[where()]]
796
     * on how to specify this parameter.
797
     * @param array $params the parameters (name => value) to be bound to the query.
798
     * @return $this the query object itself
799
     * @see onCondition()
800
     * @see orOnCondition()
801
     */
802 12
    public function andOnCondition($condition, $params = [])
803
    {
804 12
        if ($this->on === null) {
805 9
            $this->on = $condition;
806
        } else {
807 3
            $this->on = ['and', $this->on, $condition];
808
        }
809 12
        $this->addParams($params);
810 12
        return $this;
811
    }
812
813
    /**
814
     * Adds an additional ON condition to the existing one.
815
     * The new condition and the existing one will be joined using the 'OR' operator.
816
     * @param string|array $condition the new ON condition. Please refer to [[where()]]
817
     * on how to specify this parameter.
818
     * @param array $params the parameters (name => value) to be bound to the query.
819
     * @return $this the query object itself
820
     * @see onCondition()
821
     * @see andOnCondition()
822
     */
823 6
    public function orOnCondition($condition, $params = [])
824
    {
825 6
        if ($this->on === null) {
826 3
            $this->on = $condition;
827
        } else {
828 3
            $this->on = ['or', $this->on, $condition];
829
        }
830 6
        $this->addParams($params);
831 6
        return $this;
832
    }
833
834
    public function viaJoined($relationName, callable $callable = null)
835
    {
836
        $this->viaJoined = true;
837
        return $this->via($relationName, $callable);
838
    }
839
840
    /**
841
     * Specifies the junction table for a relational query.
842
     *
843
     * Use this method to specify a junction table when declaring a relation in the [[ActiveRecord]] class:
844
     *
845
     * ```php
846
     * public function getItems()
847
     * {
848
     *     return $this->hasMany(Item::class, ['id' => 'item_id'])
849
     *                 ->viaTable('order_item', ['order_id' => 'id']);
850
     * }
851
     * ```
852
     *
853
     * @param string $tableName the name of the junction table.
854
     * @param array $link the link between the junction table and the table associated with [[primaryModel]].
855
     * The keys of the array represent the columns in the junction table, and the values represent the columns
856
     * in the [[primaryModel]] table.
857
     * @param callable|null $callable a PHP callback for customizing the relation associated with the junction table.
858
     * Its signature should be `function($query)`, where `$query` is the query to be customized.
859
     * @return $this the query object itself
860
     * @throws InvalidConfigException when query is not initialized properly
861
     * @see via()
862
     */
863 24
    public function viaTable($tableName, $link, callable $callable = null)
864
    {
865 24
        $modelClass = $this->primaryModel ? get_class($this->primaryModel) : $this->modelClass;
866 24
        $relation = new self($modelClass, [
867 24
            'from' => [$tableName],
868 24
            'link' => $link,
869
            'multiple' => true,
870
            'asArray' => true,
871
        ]);
872 24
        $this->via = $relation;
873 24
        if ($callable !== null) {
874 6
            call_user_func($callable, $relation);
875
        }
876
877 24
        return $this;
878
    }
879
880
    public function viaJoinedTable($tableName, $link, callable $callable = null)
881
    {
882
        $this->viaJoined = true;
883
        return $this->viaTable($tableName, $link, $callable);
884
    }
885
886
    public function viaJoinedTables($links, callable $callable = null)
887
    {
888
        $this->viaJoined = true;
889
        $query = $this;
890
        foreach ($links as $tableName => $link) {
891
            $query->viaTable($tableName, $link, $callable);
892
            $query = $query->via;
893
        }
894
        return $this;
895
    }
896
897
    /**
898
     * Define an alias for the table defined in [[modelClass]].
899
     *
900
     * This method will adjust [[from]] so that an already defined alias will be overwritten.
901
     * If none was defined, [[from]] will be populated with the given alias.
902
     *
903
     * @param string $alias the table alias.
904
     * @return $this the query object itself
905
     * @since 2.0.7
906
     */
907 63
    public function alias($alias)
908
    {
909 63
        if (empty($this->from) || count($this->from) < 2) {
910 63
            list($tableName) = $this->getTableNameAndAlias();
911 63
            $this->from = [$alias => $tableName];
912
        } else {
913 3
            $tableName = $this->getPrimaryTableName();
914
915 3
            foreach ($this->from as $key => $table) {
916 3
                if ($table === $tableName) {
917 3
                    unset($this->from[$key]);
918 3
                    $this->from[$alias] = $tableName;
919
                }
920
            }
921
        }
922
923 63
        return $this;
924
    }
925
926
    /**
927
     * {@inheritdoc}
928
     * @since 2.0.12
929
     */
930 257
    public function getTablesUsedInFrom()
931
    {
932 257
        if (empty($this->from)) {
933 173
            return $this->cleanUpTableNames([$this->getPrimaryTableName()]);
934
        }
935
936 84
        return parent::getTablesUsedInFrom();
937
    }
938
939
    /**
940
     * @return string primary table name
941
     * @since 2.0.12
942
     */
943 544
    protected function getPrimaryTableName()
944
    {
945
        /* @var $modelClass ActiveRecord */
946 544
        $modelClass = $this->modelClass;
947 544
        return $modelClass::tableName();
948
    }
949
}
950