Completed
Push — 2.1-master-merge ( 240673 )
by Alexander
13:45
created

ActiveQuery::buildJoinWith()   C

Complexity

Conditions 11
Paths 24

Size

Total Lines 42
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 14.0032

Importance

Changes 0
Metric Value
dl 0
loc 42
rs 5.2653
c 0
b 0
f 0
ccs 17
cts 24
cp 0.7083
cc 11
eloc 23
nc 24
nop 0
crap 14.0032

How to fix   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 http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://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
 * @property string[] $tablesUsedInFrom Table names indexed by aliases. This property is read-only.
70
 *
71
 * @author Qiang Xue <[email protected]>
72
 * @author Carsten Brandt <[email protected]>
73
 * @since 2.0
74
 */
75
class ActiveQuery extends Query implements ActiveQueryInterface
76
{
77
    use ActiveQueryTrait;
78
    use ActiveRelationTrait;
79
80
    /**
81
     * @event Event an event that is triggered when the query is initialized via [[init()]].
82
     */
83
    const EVENT_INIT = 'init';
84
85
    /**
86
     * @var string the SQL statement to be executed for retrieving AR records.
87
     * This is set by [[ActiveRecord::findBySql()]].
88
     */
89
    public $sql;
90
    /**
91
     * @var string|array the join condition to be used when this query is used in a relational context.
92
     * The condition will be used in the ON part when [[ActiveQuery::joinWith()]] is called.
93
     * Otherwise, the condition will be used in the WHERE part of a query.
94
     * Please refer to [[Query::where()]] on how to specify this parameter.
95
     * @see onCondition()
96
     */
97
    public $on;
98
    /**
99
     * @var array a list of relations that this query should be joined with
100
     */
101
    public $joinWith;
102
103
104
    /**
105
     * Constructor.
106
     * @param string $modelClass the model class associated with this query
107
     * @param array $config configurations to be applied to the newly created query object
108
     */
109 419
    public function __construct($modelClass, $config = [])
110
    {
111 419
        $this->modelClass = $modelClass;
112 419
        parent::__construct($config);
113 419
    }
114
115
    /**
116
     * Initializes the object.
117
     * This method is called at the end of the constructor. The default implementation will trigger
118
     * an [[EVENT_INIT]] event. If you override this method, make sure you call the parent implementation at the end
119
     * to ensure triggering of the event.
120
     */
121 419
    public function init()
122
    {
123 419
        parent::init();
124 419
        $this->trigger(self::EVENT_INIT);
125 419
    }
126
127
    /**
128
     * Executes query and returns all results as an array.
129
     * @param Connection $db the DB connection used to create the DB command.
130
     * If null, the DB connection returned by [[modelClass]] will be used.
131
     * @return array|ActiveRecord[] the query results. If the query results in nothing, an empty array will be returned.
132
     */
133 183
    public function all($db = null)
134
    {
135 183
        return parent::all($db);
136
    }
137
138
    /**
139
     * @inheritdoc
140
     */
141 342
    public function prepare($builder)
142
    {
143
        // NOTE: because the same ActiveQuery may be used to build different SQL statements
144
        // (e.g. by ActiveDataProvider, one for count query, the other for row data query,
145
        // it is important to make sure the same ActiveQuery can be used to build SQL statements
146
        // multiple times.
147 342
        if (!empty($this->joinWith)) {
148 48
            $this->buildJoinWith();
149 48
            $this->joinWith = null;    // clean it up to avoid issue https://github.com/yiisoft/yii2/issues/2687
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $joinWith.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
150
        }
151
152 342
        if (empty($this->from)) {
153 333
            $this->from = [$this->getPrimaryTableName()];
154
        }
155
156 342
        if (empty($this->select) && !empty($this->join)) {
157 39
            $alias = $this->getTableNameAndAlias()[1];
158 39
            $this->select = ["$alias.*"];
159
        }
160
161 342
        if ($this->primaryModel === null) {
162
            // eager loading
163 341
            $query = Query::create($this);
164
        } else {
165
            // lazy loading of a relation
166 85
            $where = $this->where;
167
168 85
            if ($this->via instanceof self) {
169
                // via junction table
170 15
                $viaModels = $this->via->findJunctionRows([$this->primaryModel]);
171 15
                $this->filterByModels($viaModels);
172 76
            } elseif (is_array($this->via)) {
173
                // via relation
174
                /* @var $viaQuery ActiveQuery */
175 27
                [$viaName, $viaQuery] = $this->via;
0 ignored issues
show
Bug introduced by
The variable $viaName does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $viaQuery does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
176 27
                if ($viaQuery->multiple) {
177 27
                    $viaModels = $viaQuery->all();
178 27
                    $this->primaryModel->populateRelation($viaName, $viaModels);
179
                } else {
180
                    $model = $viaQuery->one();
181
                    $this->primaryModel->populateRelation($viaName, $model);
182
                    $viaModels = $model === null ? [] : [$model];
183
                }
184 27
                $this->filterByModels($viaModels);
185
            } else {
186 76
                $this->filterByModels([$this->primaryModel]);
187
            }
188
189 85
            $query = Query::create($this);
190 85
            $this->where = $where;
191
        }
192
193 342
        if (!empty($this->on)) {
194 18
            $query->andWhere($this->on);
195
        }
196
197 342
        return $query;
198
    }
199
200
    /**
201
     * @inheritdoc
202
     */
203 286
    public function populate($rows)
204
    {
205 286
        if (empty($rows)) {
206 52
            return [];
207
        }
208
209 277
        $models = $this->createModels($rows);
210 277
        if (!empty($this->join) && $this->indexBy === null) {
211 33
            $models = $this->removeDuplicatedModels($models);
212
        }
213 277
        if (!empty($this->with)) {
214 75
            $this->findWith($this->with, $models);
215
        }
216
217 277
        if ($this->inverseOf !== null) {
218 9
            $this->addInverseRelations($models);
219
        }
220
221 277
        if (!$this->asArray) {
222 267
            foreach ($models as $model) {
223 267
                $model->afterFind();
224
            }
225
        }
226
227 277
        return $models;
228
    }
229
230
    /**
231
     * Removes duplicated models by checking their primary key values.
232
     * This method is mainly called when a join query is performed, which may cause duplicated rows being returned.
233
     * @param array $models the models to be checked
234
     * @throws InvalidConfigException if model primary key is empty
235
     * @return array the distinctive models
236
     */
237 33
    private function removeDuplicatedModels($models)
238
    {
239 33
        $hash = [];
240
        /* @var $class ActiveRecord */
241 33
        $class = $this->modelClass;
242 33
        $pks = $class::primaryKey();
243
244 33
        if (count($pks) > 1) {
245
            // composite primary key
246 6
            foreach ($models as $i => $model) {
247 6
                $key = [];
248 6
                foreach ($pks as $pk) {
249 6
                    if (!isset($model[$pk])) {
250
                        // do not continue if the primary key is not part of the result set
251 3
                        break 2;
252
                    }
253 6
                    $key[] = $model[$pk];
254
                }
255 3
                $key = serialize($key);
256 3
                if (isset($hash[$key])) {
257
                    unset($models[$i]);
258
                } else {
259 6
                    $hash[$key] = true;
260
                }
261
            }
262
        } elseif (empty($pks)) {
263
            throw new InvalidConfigException("Primary key of '{$class}' can not be empty.");
264
        } else {
265
            // single column primary key
266 30
            $pk = reset($pks);
267 30
            foreach ($models as $i => $model) {
268 30
                if (!isset($model[$pk])) {
269
                    // do not continue if the primary key is not part of the result set
270 3
                    break;
271
                }
272 27
                $key = $model[$pk];
273 27
                if (isset($hash[$key])) {
274 12
                    unset($models[$i]);
275 27
                } elseif ($key !== null) {
276 27
                    $hash[$key] = true;
277
                }
278
            }
279
        }
280
281 33
        return array_values($models);
282
    }
283
284
    /**
285
     * Executes query and returns a single row of result.
286
     * @param Connection|null $db the DB connection used to create the DB command.
287
     * If `null`, the DB connection returned by [[modelClass]] will be used.
288
     * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]],
289
     * the query result may be either an array or an ActiveRecord object. `null` will be returned
290
     * if the query results in nothing.
291
     */
292 233
    public function one($db = null)
293
    {
294 233
        $row = parent::one($db);
295 233
        if ($row !== false) {
296 230
            $models = $this->populate([$row]);
297 230
            return reset($models) ?: null;
298
        }
299
300 24
        return null;
301
    }
302
303
    /**
304
     * Creates a DB command that can be used to execute this query.
305
     * @param Connection|null $db the DB connection used to create the DB command.
306
     * If `null`, the DB connection returned by [[modelClass]] will be used.
307
     * @return Command the created DB command instance.
308
     */
309 342
    public function createCommand($db = null)
310
    {
311
        /* @var $modelClass ActiveRecord */
312 342
        $modelClass = $this->modelClass;
313 342
        if ($db === null) {
314 331
            $db = $modelClass::getDb();
315
        }
316
317 342
        if ($this->sql === null) {
318 339
            [$sql, $params] = $db->getQueryBuilder()->build($this);
0 ignored issues
show
Bug introduced by
The variable $sql seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
Bug introduced by
The variable $params seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
319
        } else {
320 3
            $sql = $this->sql;
321 3
            $params = $this->params;
322
        }
323
324 342
        return $db->createCommand($sql, $params);
0 ignored issues
show
Bug introduced by
The variable $sql does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug introduced by
The variable $params does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
325
    }
326
327
    /**
328
     * @inheritdoc
329
     */
330 64
    protected function queryScalar($selectExpression, $db)
331
    {
332
        /* @var $modelClass ActiveRecord */
333 64
        $modelClass = $this->modelClass;
334 64
        if ($db === null) {
335 63
            $db = $modelClass::getDb();
336
        }
337
338 64
        if ($this->sql === null) {
339 61
            return parent::queryScalar($selectExpression, $db);
340
        }
341
342 3
        return (new Query())->select([$selectExpression])
343 3
            ->from(['c' => "({$this->sql})"])
344 3
            ->params($this->params)
345 3
            ->createCommand($db)
346 3
            ->queryScalar();
347
    }
348
349
    /**
350
     * Joins with the specified relations.
351
     *
352
     * This method allows you to reuse existing relation definitions to perform JOIN queries.
353
     * Based on the definition of the specified relation(s), the method will append one or multiple
354
     * JOIN statements to the current query.
355
     *
356
     * If the `$eagerLoading` parameter is true, the method will also perform eager loading for the specified relations,
357
     * which is equivalent to calling [[with()]] using the specified relations.
358
     *
359
     * Note that because a JOIN query will be performed, you are responsible to disambiguate column names.
360
     *
361
     * This method differs from [[with()]] in that it will build up and execute a JOIN SQL statement
362
     * for the primary table. And when `$eagerLoading` is true, it will call [[with()]] in addition with the specified relations.
363
     *
364
     * @param string|array $with the relations to be joined. This can either be a string, representing a relation name or
365
     * an array with the following semantics:
366
     *
367
     * - Each array element represents a single relation.
368
     * - You may specify the relation name as the array key and provide an anonymous functions that
369
     *   can be used to modify the relation queries on-the-fly as the array value.
370
     * - If a relation query does not need modification, you may use the relation name as the array value.
371
     *
372
     * The relation name may optionally contain an alias for the relation table (e.g. `books b`).
373
     *
374
     * Sub-relations can also be specified, see [[with()]] for the syntax.
375
     *
376
     * In the following you find some examples:
377
     *
378
     * ```php
379
     * // find all orders that contain books, and eager loading "books"
380
     * Order::find()->joinWith('books', true, 'INNER JOIN')->all();
381
     * // find all orders, eager loading "books", and sort the orders and books by the book names.
382
     * Order::find()->joinWith([
383
     *     'books' => function (\yii\db\ActiveQuery $query) {
384
     *         $query->orderBy('item.name');
385
     *     }
386
     * ])->all();
387
     * // find all orders that contain books of the category 'Science fiction', using the alias "b" for the books table
388
     * Order::find()->joinWith(['books b'], true, 'INNER JOIN')->where(['b.category' => 'Science fiction'])->all();
389
     * ```
390
     *
391
     * The alias syntax is available since version 2.0.7.
392
     *
393
     * @param bool|array $eagerLoading whether to eager load the relations specified in `$with`.
394
     * When this is a boolean, it applies to all relations specified in `$with`. Use an array
395
     * to explicitly list which relations in `$with` need to be eagerly loaded. Defaults to `true`.
396
     * @param string|array $joinType the join type of the relations specified in `$with`.
397
     * When this is a string, it applies to all relations specified in `$with`. Use an array
398
     * in the format of `relationName => joinType` to specify different join types for different relations.
399
     * @return $this the query object itself
400
     */
401 54
    public function joinWith($with, $eagerLoading = true, $joinType = 'LEFT JOIN')
402
    {
403 54
        $relations = [];
404 54
        foreach ((array) $with as $name => $callback) {
405 54
            if (is_int($name)) {
406 54
                $name = $callback;
407 54
                $callback = null;
408
            }
409
410 54
            if (preg_match('/^(.*?)(?:\s+AS\s+|\s+)(\w+)$/i', $name, $matches)) {
411
                // relation is defined with an alias, adjust callback to apply alias
412 12
                [, $relation, $alias] = $matches;
0 ignored issues
show
Bug introduced by
The variable $relation does not exist. Did you mean $relations?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
Bug introduced by
The variable $alias does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
413 12
                $name = $relation;
0 ignored issues
show
Bug introduced by
The variable $relation does not exist. Did you mean $relations?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
414 12
                $callback = function ($query) use ($callback, $alias) {
415
                    /* @var $query ActiveQuery */
416 12
                    $query->alias($alias);
417 12
                    if ($callback !== null) {
418 9
                        call_user_func($callback, $query);
419
                    }
420 12
                };
421
            }
422
423 54
            if ($callback === null) {
424 51
                $relations[] = $name;
425
            } else {
426 54
                $relations[$name] = $callback;
427
            }
428
        }
429 54
        $this->joinWith[] = [$relations, $eagerLoading, $joinType];
430 54
        return $this;
431
    }
432
433 48
    private function buildJoinWith()
434
    {
435 48
        $join = $this->join;
436 48
        $this->join = [];
437
438
        /* @var $modelClass ActiveRecordInterface */
439 48
        $modelClass = $this->modelClass;
440 48
        $model = $modelClass::instance();
441 48
        foreach ($this->joinWith as [$with, $eagerLoading, $joinType]) {
442 48
            $this->joinWithRelations($model, $with, $joinType);
0 ignored issues
show
Bug introduced by
The variable $with does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug introduced by
The variable $joinType does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Compatibility introduced by
$model of type object<yii\db\ActiveRecordInterface> is not a sub-type of object<yii\db\ActiveRecord>. It seems like you assume a concrete implementation of the interface yii\db\ActiveRecordInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
443
444 48
            if (is_array($eagerLoading)) {
445
                foreach ($with as $name => $callback) {
446
                    if (is_int($name)) {
447
                        if (!in_array($callback, $eagerLoading, true)) {
0 ignored issues
show
Bug introduced by
The variable $eagerLoading does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
448
                            unset($with[$name]);
449
                        }
450
                    } elseif (!in_array($name, $eagerLoading, true)) {
451
                        unset($with[$name]);
452
                    }
453
                }
454 48
            } elseif (!$eagerLoading) {
455 15
                $with = [];
456
            }
457
458 48
            $this->with($with);
459
        }
460
461
        // remove duplicated joins added by joinWithRelations that may be added
462
        // e.g. when joining a relation and a via relation at the same time
463 48
        $uniqueJoins = [];
464 48
        foreach ($this->join as $j) {
465 48
            $uniqueJoins[serialize($j)] = $j;
466
        }
467 48
        $this->join = array_values($uniqueJoins);
468
469 48
        if (!empty($join)) {
470
            // append explicit join to joinWith()
471
            // https://github.com/yiisoft/yii2/issues/2880
472
            $this->join = empty($this->join) ? $join : array_merge($this->join, $join);
473
        }
474 48
    }
475
476
    /**
477
     * Inner joins with the specified relations.
478
     * This is a shortcut method to [[joinWith()]] with the join type set as "INNER JOIN".
479
     * Please refer to [[joinWith()]] for detailed usage of this method.
480
     * @param string|array $with the relations to be joined with.
481
     * @param bool|array $eagerLoading whether to eager loading the relations.
482
     * @return $this the query object itself
483
     * @see joinWith()
484
     */
485 18
    public function innerJoinWith($with, $eagerLoading = true)
486
    {
487 18
        return $this->joinWith($with, $eagerLoading, 'INNER JOIN');
488
    }
489
490
    /**
491
     * Modifies the current query by adding join fragments based on the given relations.
492
     * @param ActiveRecord $model the primary model
493
     * @param array $with the relations to be joined
494
     * @param string|array $joinType the join type
495
     */
496 48
    private function joinWithRelations($model, $with, $joinType)
497
    {
498 48
        $relations = [];
499
500 48
        foreach ($with as $name => $callback) {
501 48
            if (is_int($name)) {
502 45
                $name = $callback;
503 45
                $callback = null;
504
            }
505
506 48
            $primaryModel = $model;
507 48
            $parent = $this;
508 48
            $prefix = '';
509 48
            while (($pos = strpos($name, '.')) !== false) {
510 6
                $childName = substr($name, $pos + 1);
511 6
                $name = substr($name, 0, $pos);
512 6
                $fullName = $prefix === '' ? $name : "$prefix.$name";
513 6
                if (!isset($relations[$fullName])) {
514
                    $relations[$fullName] = $relation = $primaryModel->getRelation($name);
515
                    $this->joinWithRelation($parent, $relation, $this->getJoinType($joinType, $fullName));
0 ignored issues
show
Compatibility introduced by
$relation of type object<yii\db\ActiveQueryInterface> is not a sub-type of object<yii\db\ActiveQuery>. It seems like you assume a concrete implementation of the interface yii\db\ActiveQueryInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
516
                } else {
517 6
                    $relation = $relations[$fullName];
518
                }
519
                /* @var $relationModelClass ActiveRecordInterface */
520 6
                $relationModelClass = $relation->modelClass;
521 6
                $primaryModel = $relationModelClass::instance();
522 6
                $parent = $relation;
523 6
                $prefix = $fullName;
524 6
                $name = $childName;
525
            }
526
527 48
            $fullName = $prefix === '' ? $name : "$prefix.$name";
528 48
            if (!isset($relations[$fullName])) {
529 48
                $relations[$fullName] = $relation = $primaryModel->getRelation($name);
530 48
                if ($callback !== null) {
531 18
                    call_user_func($callback, $relation);
532
                }
533 48
                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?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
534 6
                    $relation->buildJoinWith();
535
                }
536 48
                $this->joinWithRelation($parent, $relation, $this->getJoinType($joinType, $fullName));
0 ignored issues
show
Compatibility introduced by
$relation of type object<yii\db\ActiveQueryInterface> is not a sub-type of object<yii\db\ActiveQuery>. It seems like you assume a concrete implementation of the interface yii\db\ActiveQueryInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
537
            }
538
        }
539 48
    }
540
541
    /**
542
     * Returns the join type based on the given join type parameter and the relation name.
543
     * @param string|array $joinType the given join type(s)
544
     * @param string $name relation name
545
     * @return string the real join type
546
     */
547 48
    private function getJoinType($joinType, $name)
548
    {
549 48
        if (is_array($joinType) && isset($joinType[$name])) {
550
            return $joinType[$name];
551
        }
552
553 48
        return is_string($joinType) ? $joinType : 'INNER JOIN';
554
    }
555
556
    /**
557
     * Returns the table name and the table alias for [[modelClass]].
558
     * @return array the table name and the table alias.
559
     * @internal
560
     */
561 63
    private function getTableNameAndAlias()
562
    {
563 63
        if (empty($this->from)) {
564 57
            $tableName = $this->getPrimaryTableName();
565
        } else {
566 54
            $tableName = '';
567 54
            foreach ($this->from as $alias => $tableName) {
568 54
                if (is_string($alias)) {
569 18
                    return [$tableName, $alias];
570
                }
571 48
                break;
572
            }
573
        }
574
575 60
        if (preg_match('/^(.*?)\s+({{\w+}}|\w+)$/', $tableName, $matches)) {
576 3
            $alias = $matches[2];
577
        } else {
578 60
            $alias = $tableName;
579
        }
580
581 60
        return [$tableName, $alias];
582
    }
583
584
    /**
585
     * Joins a parent query with a child query.
586
     * The current query object will be modified accordingly.
587
     * @param ActiveQuery $parent
588
     * @param ActiveQuery $child
589
     * @param string $joinType
590
     */
591 48
    private function joinWithRelation($parent, $child, $joinType)
592
    {
593 48
        $via = $child->via;
594 48
        $child->via = null;
595 48
        if ($via instanceof self) {
596
            // via table
597 9
            $this->joinWithRelation($parent, $via, $joinType);
598 9
            $this->joinWithRelation($via, $child, $joinType);
599 9
            return;
600 48
        } elseif (is_array($via)) {
601
            // via relation
602 15
            $this->joinWithRelation($parent, $via[1], $joinType);
603 15
            $this->joinWithRelation($via[1], $child, $joinType);
604 15
            return;
605
        }
606
607 48
        [$parentTable, $parentAlias] = $parent->getTableNameAndAlias();
0 ignored issues
show
Bug introduced by
The variable $parentTable does not exist. Did you mean $parent?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
Bug introduced by
The variable $parentAlias seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
608 48
        [$childTable, $childAlias] = $child->getTableNameAndAlias();
0 ignored issues
show
Bug introduced by
The variable $childTable does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $childAlias seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
609
610 48
        if (!empty($child->link)) {
611 48
            if (strpos($parentAlias, '{{') === false) {
0 ignored issues
show
Bug introduced by
The variable $parentAlias seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
612 42
                $parentAlias = '{{' . $parentAlias . '}}';
0 ignored issues
show
Bug introduced by
The variable $parentAlias seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
613
            }
614 48
            if (strpos($childAlias, '{{') === false) {
0 ignored issues
show
Bug introduced by
The variable $childAlias seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
615 48
                $childAlias = '{{' . $childAlias . '}}';
0 ignored issues
show
Bug introduced by
The variable $childAlias seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
616
            }
617
618 48
            $on = [];
619 48
            foreach ($child->link as $childColumn => $parentColumn) {
620 48
                $on[] = "$parentAlias.[[$parentColumn]] = $childAlias.[[$childColumn]]";
0 ignored issues
show
Bug introduced by
The variable $parentAlias does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug introduced by
The variable $childAlias does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
621
            }
622 48
            $on = implode(' AND ', $on);
623 48
            if (!empty($child->on)) {
624 48
                $on = ['and', $on, $child->on];
625
            }
626
        } else {
627
            $on = $child->on;
628
        }
629 48
        $this->join($joinType, empty($child->from) ? $childTable : $child->from, $on);
630
631 48
        if (!empty($child->where)) {
632 6
            $this->andWhere($child->where);
633
        }
634 48
        if (!empty($child->having)) {
635
            $this->andHaving($child->having);
636
        }
637 48
        if (!empty($child->orderBy)) {
638 15
            $this->addOrderBy($child->orderBy);
639
        }
640 48
        if (!empty($child->groupBy)) {
641
            $this->addGroupBy($child->groupBy);
642
        }
643 48
        if (!empty($child->params)) {
644
            $this->addParams($child->params);
645
        }
646 48
        if (!empty($child->join)) {
647 6
            foreach ($child->join as $join) {
648 6
                $this->join[] = $join;
649
            }
650
        }
651 48
        if (!empty($child->union)) {
652
            foreach ($child->union as $union) {
653
                $this->union[] = $union;
654
            }
655
        }
656 48
    }
657
658
    /**
659
     * Sets the ON condition for a relational query.
660
     * The condition will be used in the ON part when [[ActiveQuery::joinWith()]] is called.
661
     * Otherwise, the condition will be used in the WHERE part of a query.
662
     *
663
     * Use this method to specify additional conditions when declaring a relation in the [[ActiveRecord]] class:
664
     *
665
     * ```php
666
     * public function getActiveUsers()
667
     * {
668
     *     return $this->hasMany(User::class, ['id' => 'user_id'])
669
     *                 ->onCondition(['active' => true]);
670
     * }
671
     * ```
672
     *
673
     * Note that this condition is applied in case of a join as well as when fetching the related records.
674
     * Thus only fields of the related table can be used in the condition. Trying to access fields of the primary
675
     * record will cause an error in a non-join-query.
676
     *
677
     * @param string|array $condition the ON condition. Please refer to [[Query::where()]] on how to specify this parameter.
678
     * @param array $params the parameters (name => value) to be bound to the query.
679
     * @return $this the query object itself
680
     */
681 21
    public function onCondition($condition, $params = [])
682
    {
683 21
        $this->on = $condition;
684 21
        $this->addParams($params);
685 21
        return $this;
686
    }
687
688
    /**
689
     * Adds an additional ON condition to the existing one.
690
     * The new condition and the existing one will be joined using the 'AND' operator.
691
     * @param string|array $condition the new ON condition. Please refer to [[where()]]
692
     * on how to specify this parameter.
693
     * @param array $params the parameters (name => value) to be bound to the query.
694
     * @return $this the query object itself
695
     * @see onCondition()
696
     * @see orOnCondition()
697
     */
698 6
    public function andOnCondition($condition, $params = [])
699
    {
700 6
        if ($this->on === null) {
701 3
            $this->on = $condition;
702
        } else {
703 3
            $this->on = ['and', $this->on, $condition];
704
        }
705 6
        $this->addParams($params);
706 6
        return $this;
707
    }
708
709
    /**
710
     * Adds an additional ON condition to the existing one.
711
     * The new condition and the existing one will be joined using the 'OR' operator.
712
     * @param string|array $condition the new ON condition. Please refer to [[where()]]
713
     * on how to specify this parameter.
714
     * @param array $params the parameters (name => value) to be bound to the query.
715
     * @return $this the query object itself
716
     * @see onCondition()
717
     * @see andOnCondition()
718
     */
719 6
    public function orOnCondition($condition, $params = [])
720
    {
721 6
        if ($this->on === null) {
722 3
            $this->on = $condition;
723
        } else {
724 3
            $this->on = ['or', $this->on, $condition];
725
        }
726 6
        $this->addParams($params);
727 6
        return $this;
728
    }
729
730
    /**
731
     * Specifies the junction table for a relational query.
732
     *
733
     * Use this method to specify a junction table when declaring a relation in the [[ActiveRecord]] class:
734
     *
735
     * ```php
736
     * public function getItems()
737
     * {
738
     *     return $this->hasMany(Item::class, ['id' => 'item_id'])
739
     *                 ->viaTable('order_item', ['order_id' => 'id']);
740
     * }
741
     * ```
742
     *
743
     * @param string $tableName the name of the junction table.
744
     * @param array $link the link between the junction table and the table associated with [[primaryModel]].
745
     * The keys of the array represent the columns in the junction table, and the values represent the columns
746
     * in the [[primaryModel]] table.
747
     * @param callable $callable a PHP callback for customizing the relation associated with the junction table.
748
     * Its signature should be `function($query)`, where `$query` is the query to be customized.
749
     * @return $this the query object itself
750
     * @see via()
751
     */
752 24
    public function viaTable($tableName, $link, callable $callable = null)
753
    {
754 24
        $relation = new self(get_class($this->primaryModel), [
755 24
            'from' => [$tableName],
756 24
            'link' => $link,
757
            'multiple' => true,
758
            'asArray' => true,
759
        ]);
760 24
        $this->via = $relation;
761 24
        if ($callable !== null) {
762 6
            call_user_func($callable, $relation);
763
        }
764
765 24
        return $this;
766
    }
767
768
    /**
769
     * Define an alias for the table defined in [[modelClass]].
770
     *
771
     * This method will adjust [[from]] so that an already defined alias will be overwritten.
772
     * If none was defined, [[from]] will be populated with the given alias.
773
     *
774
     * @param string $alias the table alias.
775
     * @return $this the query object itself
776
     * @since 2.0.7
777
     */
778 21
    public function alias($alias)
779
    {
780 21
        if (empty($this->from) || count($this->from) < 2) {
781 21
            $tableName = $this->getTableNameAndAlias()[0];
782 21
            $this->from = [$alias => $tableName];
783
        } else {
784 3
            $tableName = $this->getPrimaryTableName();
785
786 3
            foreach ($this->from as $key => $table) {
787 3
                if ($table === $tableName) {
788 3
                    unset($this->from[$key]);
789 3
                    $this->from[$alias] = $tableName;
790
                }
791
            }
792
        }
793
794 21
        return $this;
795
    }
796
797
    /**
798
     * @inheritdoc
799
     * @since 2.0.12
800
     */
801 85
    public function getTablesUsedInFrom()
802
    {
803 85
        if (empty($this->from)) {
804 67
            $this->from = [$this->getPrimaryTableName()];
805
        }
806
807 85
        return parent::getTablesUsedInFrom();
808
    }
809
810
    /**
811
     * @return string primary table name
812
     * @since 2.0.12
813
     */
814 354
    protected function getPrimaryTableName()
815
    {
816
        /* @var $modelClass ActiveRecord */
817 354
        $modelClass = $this->modelClass;
818 354
        return $modelClass::tableName();
819
    }
820
}
821