Completed
Push — 2.1 ( b44a46...4c2160 )
by
unknown
12:30
created

ActiveRelationTrait::__clone()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3.0261

Importance

Changes 0
Metric Value
dl 0
loc 10
ccs 6
cts 7
cp 0.8571
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 6
nc 3
nop 0
crap 3.0261
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\InvalidArgumentException;
11
use yii\base\InvalidConfigException;
12
13
/**
14
 * ActiveRelationTrait implements the common methods and properties for active record relational queries.
15
 *
16
 * @author Qiang Xue <[email protected]>
17
 * @author Carsten Brandt <[email protected]>
18
 * @since 2.0
19
 *
20
 * @method ActiveRecordInterface one()
21
 * @method ActiveRecordInterface[] all()
22
 * @property ActiveRecord $modelClass
23
 */
24
trait ActiveRelationTrait
25
{
26
    /**
27
     * @var bool whether this query represents a relation to more than one record.
28
     * This property is only used in relational context. If true, this relation will
29
     * populate all query results into AR instances using [[Query::all()|all()]].
30
     * If false, only the first row of the results will be retrieved using [[Query::one()|one()]].
31
     */
32
    public $multiple;
33
    /**
34
     * @var ActiveRecord the primary model of a relational query.
35
     * This is used only in lazy loading with dynamic query options.
36
     */
37
    public $primaryModel;
38
    /**
39
     * @var array the columns of the primary and foreign tables that establish a relation.
40
     * The array keys must be columns of the table for this relation, and the array values
41
     * must be the corresponding columns from the primary table.
42
     * Do not prefix or quote the column names as this will be done automatically by Yii.
43
     * This property is only used in relational context.
44
     */
45
    public $link;
46
    /**
47
     * @var array|object the query associated with the junction table. Please call [[via()]]
48
     * to set this property instead of directly setting it.
49
     * This property is only used in relational context.
50
     * @see via()
51
     */
52
    public $via;
53
    /**
54
     * @var string the name of the relation that is the inverse of this relation.
55
     * For example, an order has a customer, which means the inverse of the "customer" relation
56
     * is the "orders", and the inverse of the "orders" relation is the "customer".
57
     * If this property is set, the primary record(s) will be referenced through the specified relation.
58
     * For example, `$customer->orders[0]->customer` and `$customer` will be the same object,
59
     * and accessing the customer of an order will not trigger new DB query.
60
     * This property is only used in relational context.
61
     * @see inverseOf()
62
     */
63
    public $inverseOf;
64
65
66
    /**
67
     * Clones internal objects.
68
     */
69 12
    public function __clone()
70
    {
71 12
        parent::__clone();
72
        // make a clone of "via" object so that the same query object can be reused multiple times
73 12
        if (is_object($this->via)) {
74
            $this->via = clone $this->via;
75 12
        } elseif (is_array($this->via)) {
76 6
            $this->via = [$this->via[0], clone $this->via[1]];
77
        }
78 12
    }
79
80
    /**
81
     * Specifies the relation associated with the junction table.
82
     *
83
     * Use this method to specify a pivot record/table when declaring a relation in the [[ActiveRecord]] class:
84
     *
85
     * ```php
86
     * class Order extends ActiveRecord
87
     * {
88
     *    public function getOrderItems() {
89
     *        return $this->hasMany(OrderItem::class, ['order_id' => 'id']);
90
     *    }
91
     *
92
     *    public function getItems() {
93
     *        return $this->hasMany(Item::class, ['id' => 'item_id'])
94
     *                    ->via('orderItems');
95
     *    }
96
     * }
97
     * ```
98
     *
99
     * @param string $relationName the relation name. This refers to a relation declared in [[primaryModel]].
100
     * @param callable $callable a PHP callback for customizing the relation associated with the junction table.
101
     * Its signature should be `function($query)`, where `$query` is the query to be customized.
102
     * @return $this the relation object itself.
103
     */
104 69
    public function via($relationName, callable $callable = null)
105
    {
106 69
        $relation = $this->primaryModel->getRelation($relationName);
107 69
        $this->via = [$relationName, $relation];
108 69
        if ($callable !== null) {
109 48
            call_user_func($callable, $relation);
110
        }
111
112 69
        return $this;
113
    }
114
115
    /**
116
     * Sets the name of the relation that is the inverse of this relation.
117
     * For example, a customer has orders, which means the inverse of the "orders" relation is the "customer".
118
     * If this property is set, the primary record(s) will be referenced through the specified relation.
119
     * For example, `$customer->orders[0]->customer` and `$customer` will be the same object,
120
     * and accessing the customer of an order will not trigger a new DB query.
121
     *
122
     * Use this method when declaring a relation in the [[ActiveRecord]] class, e.g. in Customer model:
123
     *
124
     * ```php
125
     * public function getOrders()
126
     * {
127
     *     return $this->hasMany(Order::class, ['customer_id' => 'id'])->inverseOf('customer');
128
     * }
129
     * ```
130
     *
131
     * This also may be used for Order model, but with caution:
132
     *
133
     * ```php
134
     * public function getCustomer()
135
     * {
136
     *     return $this->hasOne(Customer::class, ['id' => 'customer_id'])->inverseOf('orders');
137
     * }
138
     * ```
139
     *
140
     * in this case result will depend on how order(s) was loaded.
141
     * Let's suppose customer has several orders. If only one order was loaded:
142
     *
143
     * ```php
144
     * $orders = Order::find()->where(['id' => 1])->all();
145
     * $customerOrders = $orders[0]->customer->orders;
146
     * ```
147
     *
148
     * variable `$customerOrders` will contain only one order. If orders was loaded like this:
149
     *
150
     * ```php
151
     * $orders = Order::find()->with('customer')->where(['customer_id' => 1])->all();
152
     * $customerOrders = $orders[0]->customer->orders;
153
     * ```
154
     *
155
     * variable `$customerOrders` will contain all orders of the customer.
156
     *
157
     * @param string $relationName the name of the relation that is the inverse of this relation.
158
     * @return $this the relation object itself.
159
     */
160 12
    public function inverseOf($relationName)
161
    {
162 12
        $this->inverseOf = $relationName;
163 12
        return $this;
164
    }
165
166
    /**
167
     * Finds the related records for the specified primary record.
168
     * This method is invoked when a relation of an ActiveRecord is being accessed in a lazy fashion.
169
     * @param string $name the relation name
170
     * @param ActiveRecordInterface|BaseActiveRecord $model the primary model
171
     * @return mixed the related record(s)
172
     * @throws InvalidArgumentException if the relation is invalid
173
     */
174 67
    public function findFor($name, $model)
175
    {
176 67
        if (method_exists($model, 'get' . $name)) {
177 67
            $method = new \ReflectionMethod($model, 'get' . $name);
178 67
            $realName = lcfirst(substr($method->getName(), 3));
0 ignored issues
show
Bug introduced by
Consider using $method->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
179 67
            if ($realName !== $name) {
180
                throw new InvalidArgumentException('Relation names are case sensitive. ' . get_class($model) . " has a relation named \"$realName\" instead of \"$name\".");
181
            }
182
        }
183
184 67
        return $this->multiple ? $this->all() : $this->one();
185
    }
186
187
    /**
188
     * If applicable, populate the query's primary model into the related records' inverse relationship.
189
     * @param array $result the array of related records as generated by [[populate()]]
190
     * @since 2.0.9
191
     */
192 12
    private function addInverseRelations(&$result)
193
    {
194 12
        if ($this->inverseOf === null) {
195
            return;
196
        }
197
198 12
        foreach ($result as $i => $relatedModel) {
199 12
            if ($relatedModel instanceof ActiveRecordInterface) {
200 12
                if (!isset($inverseRelation)) {
201 12
                    $inverseRelation = $relatedModel->getRelation($this->inverseOf);
202
                }
203 12
                $relatedModel->populateRelation($this->inverseOf, $inverseRelation->multiple ? [$this->primaryModel] : $this->primaryModel);
0 ignored issues
show
Bug introduced by
Accessing multiple 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...
204
            } else {
205 9
                if (!isset($inverseRelation)) {
206
                    /* @var $modelClass ActiveRecordInterface */
207 9
                    $modelClass = $this->modelClass;
208 9
                    $inverseRelation = $modelClass::instance()->getRelation($this->inverseOf);
209
                }
210 12
                $result[$i][$this->inverseOf] = $inverseRelation->multiple ? [$this->primaryModel] : $this->primaryModel;
0 ignored issues
show
Bug introduced by
Accessing multiple 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...
211
            }
212
        }
213 12
    }
214
215
    /**
216
     * Finds the related records and populates them into the primary models.
217
     * @param string $name the relation name
218
     * @param array $primaryModels primary models
219
     * @return array the related models
220
     * @throws InvalidConfigException if [[link]] is invalid
221
     */
222 84
    public function populateRelation($name, &$primaryModels)
223
    {
224 84
        if (!is_array($this->link)) {
225
            throw new InvalidConfigException('Invalid link: it must be an array of key-value pairs.');
226
        }
227
228 84
        if ($this->via instanceof self) {
229
            // via junction table
230
            /* @var $viaQuery ActiveRelationTrait */
231 9
            $viaQuery = $this->via;
232 9
            $viaModels = $viaQuery->findJunctionRows($primaryModels);
233 9
            $this->filterByModels($viaModels);
234 84
        } elseif (is_array($this->via)) {
235
            // via relation
236
            /* @var $viaQuery ActiveRelationTrait|ActiveQueryTrait */
237 36
            [$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 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...
238 36
            if ($viaQuery->asArray === null) {
0 ignored issues
show
Bug introduced by
The variable $viaQuery 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...
239
                // inherit asArray from primary query
240 36
                $viaQuery->asArray($this->asArray);
0 ignored issues
show
Bug introduced by
The property asArray does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
Bug introduced by
The variable $viaQuery 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 method asArray does only exist in yii\db\ActiveQueryTrait, but not in yii\db\ActiveRelationTrait.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
241
            }
242 36
            $viaQuery->primaryModel = null;
0 ignored issues
show
Bug introduced by
The variable $viaQuery 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...
243 36
            $viaModels = $viaQuery->populateRelation($viaName, $primaryModels);
0 ignored issues
show
Bug introduced by
The method populateRelation does only exist in yii\db\ActiveRelationTrait, but not in yii\db\ActiveQueryTrait.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
Bug introduced by
The variable $viaQuery 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...
244 36
            $this->filterByModels($viaModels);
245
        } else {
246 84
            $this->filterByModels($primaryModels);
247
        }
248
249 84
        if (!$this->multiple && count($primaryModels) === 1) {
250 30
            $model = $this->one();
251 30
            $primaryModel = reset($primaryModels);
252 30
            if ($primaryModel instanceof ActiveRecordInterface) {
253 30
                $primaryModel->populateRelation($name, $model);
254
            } else {
255 3
                $primaryModels[key($primaryModels)][$name] = $model;
256
            }
257 30
            if ($this->inverseOf !== null) {
258 6
                $this->populateInverseRelation($primaryModels, [$model], $name, $this->inverseOf);
259
            }
260
261 30
            return [$model];
262
        }
263
264
        // https://github.com/yiisoft/yii2/issues/3197
265
        // delay indexing related models after buckets are built
266 66
        $indexBy = $this->indexBy;
0 ignored issues
show
Bug introduced by
The property indexBy does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
267 66
        $this->indexBy = null;
268 66
        $models = $this->all();
269
270 66
        if (isset($viaModels, $viaQuery)) {
271 36
            $buckets = $this->buildBuckets($models, $this->link, $viaModels, $viaQuery->link);
272
        } else {
273 66
            $buckets = $this->buildBuckets($models, $this->link);
274
        }
275
276 66
        $this->indexBy = $indexBy;
277 66
        if ($this->indexBy !== null && $this->multiple) {
278 15
            $buckets = $this->indexBuckets($buckets, $this->indexBy);
279
        }
280
281 66
        $link = array_values(isset($viaQuery) ? $viaQuery->link : $this->link);
282 66
        foreach ($primaryModels as $i => $primaryModel) {
283 66
            if ($this->multiple && count($link) === 1 && is_array($keys = $primaryModel[reset($link)])) {
284
                $value = [];
285
                foreach ($keys as $key) {
286
                    $key = $this->normalizeModelKey($key);
287
                    if (isset($buckets[$key])) {
288
                        if ($this->indexBy !== null) {
289
                            // if indexBy is set, array_merge will cause renumbering of numeric array
290
                            foreach ($buckets[$key] as $bucketKey => $bucketValue) {
291
                                $value[$bucketKey] = $bucketValue;
292
                            }
293
                        } else {
294
                            $value = array_merge($value, $buckets[$key]);
295
                        }
296
                    }
297
                }
298
            } else {
299 66
                $key = $this->getModelKey($primaryModel, $link);
300 66
                $value = isset($buckets[$key]) ? $buckets[$key] : ($this->multiple ? [] : null);
301
            }
302 66
            if ($primaryModel instanceof ActiveRecordInterface) {
303 66
                $primaryModel->populateRelation($name, $value);
304
            } else {
305 66
                $primaryModels[$i][$name] = $value;
306
            }
307
        }
308 66
        if ($this->inverseOf !== null) {
309 6
            $this->populateInverseRelation($primaryModels, $models, $name, $this->inverseOf);
310
        }
311
312 66
        return $models;
313
    }
314
315
    /**
316
     * @param ActiveRecordInterface[] $primaryModels primary models
317
     * @param ActiveRecordInterface[] $models models
318
     * @param string $primaryName the primary relation name
319
     * @param string $name the relation name
320
     */
321 9
    private function populateInverseRelation(&$primaryModels, $models, $primaryName, $name)
322
    {
323 9
        if (empty($models) || empty($primaryModels)) {
324
            return;
325
        }
326 9
        $model = reset($models);
327
        /* @var $relation ActiveQueryInterface|ActiveQuery */
328 9
        if ($model instanceof ActiveRecordInterface) {
329 9
            $relation = $model->getRelation($name);
330
        } else {
331
            /* @var $modelClass ActiveRecordInterface */
332 6
            $modelClass = $this->modelClass;
333 6
            $relation = $modelClass::instance()->getRelation($name);
334
        }
335
336 9
        if ($relation->multiple) {
0 ignored issues
show
Bug introduced by
Accessing multiple 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...
337 6
            $buckets = $this->buildBuckets($primaryModels, $relation->link, null, null, false);
0 ignored issues
show
Bug introduced by
Accessing link 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...
338 6
            if ($model instanceof ActiveRecordInterface) {
339 6
                foreach ($models as $model) {
340 6
                    $key = $this->getModelKey($model, $relation->link);
0 ignored issues
show
Bug introduced by
Accessing link 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...
341 6
                    $model->populateRelation($name, isset($buckets[$key]) ? $buckets[$key] : []);
342
                }
343
            } else {
344 3
                foreach ($primaryModels as $i => $primaryModel) {
345 3
                    if ($this->multiple) {
346
                        foreach ($primaryModel as $j => $m) {
0 ignored issues
show
Bug introduced by
The expression $primaryModel of type object<yii\db\ActiveRecordInterface> is not traversable.
Loading history...
347
                            $key = $this->getModelKey($m, $relation->link);
0 ignored issues
show
Bug introduced by
Accessing link 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...
348
                            $primaryModels[$i][$j][$name] = isset($buckets[$key]) ? $buckets[$key] : [];
349
                        }
350 3
                    } elseif (!empty($primaryModel[$primaryName])) {
351 3
                        $key = $this->getModelKey($primaryModel[$primaryName], $relation->link);
0 ignored issues
show
Bug introduced by
Accessing link 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...
352 6
                        $primaryModels[$i][$primaryName][$name] = isset($buckets[$key]) ? $buckets[$key] : [];
353
                    }
354
                }
355
            }
356
        } else {
357 6
            if ($this->multiple) {
358 6
                foreach ($primaryModels as $i => $primaryModel) {
359 6
                    foreach ($primaryModel[$primaryName] as $j => $m) {
360 6
                        if ($m instanceof ActiveRecordInterface) {
361 6
                            $m->populateRelation($name, $primaryModel);
362
                        } else {
363 6
                            $primaryModels[$i][$primaryName][$j][$name] = $primaryModel;
364
                        }
365
                    }
366
                }
367
            } else {
368
                foreach ($primaryModels as $i => $primaryModel) {
369
                    if ($primaryModels[$i][$primaryName] instanceof ActiveRecordInterface) {
370
                        $primaryModels[$i][$primaryName]->populateRelation($name, $primaryModel);
371
                    } elseif (!empty($primaryModels[$i][$primaryName])) {
372
                        $primaryModels[$i][$primaryName][$name] = $primaryModel;
373
                    }
374
                }
375
            }
376
        }
377 9
    }
378
379
    /**
380
     * @param array $models
381
     * @param array $link
382
     * @param array $viaModels
383
     * @param array $viaLink
384
     * @param bool $checkMultiple
385
     * @return array
386
     */
387 69
    private function buildBuckets($models, $link, $viaModels = null, $viaLink = null, $checkMultiple = true)
388
    {
389 69
        if ($viaModels !== null) {
390 36
            $map = [];
391 36
            $viaLinkKeys = array_keys($viaLink);
392 36
            $linkValues = array_values($link);
393 36
            foreach ($viaModels as $viaModel) {
394 36
                $key1 = $this->getModelKey($viaModel, $viaLinkKeys);
395 36
                $key2 = $this->getModelKey($viaModel, $linkValues);
396 36
                $map[$key2][$key1] = true;
397
            }
398
        }
399
400 69
        $buckets = [];
401 69
        $linkKeys = array_keys($link);
402
403 69
        if (isset($map)) {
404 36
            foreach ($models as $model) {
405 36
                $key = $this->getModelKey($model, $linkKeys);
406 36
                if (isset($map[$key])) {
407 36
                    foreach (array_keys($map[$key]) as $key2) {
408 36
                        $buckets[$key2][] = $model;
409
                    }
410
                }
411
            }
412
        } else {
413 69
            foreach ($models as $model) {
414 69
                $key = $this->getModelKey($model, $linkKeys);
415 69
                $buckets[$key][] = $model;
416
            }
417
        }
418
419 69
        if ($checkMultiple && !$this->multiple) {
420 21
            foreach ($buckets as $i => $bucket) {
421 21
                $buckets[$i] = reset($bucket);
422
            }
423
        }
424
425 69
        return $buckets;
426
    }
427
428
429
    /**
430
     * Indexes buckets by column name.
431
     *
432
     * @param array $buckets
433
     * @param string|callable $indexBy the name of the column by which the query results should be indexed by.
434
     * This can also be a callable (e.g. anonymous function) that returns the index value based on the given row data.
435
     * @return array
436
     */
437 15
    private function indexBuckets($buckets, $indexBy)
438
    {
439 15
        $result = [];
440 15
        foreach ($buckets as $key => $models) {
441 15
            $result[$key] = [];
442 15
            foreach ($models as $model) {
443 15
                $index = is_string($indexBy) ? $model[$indexBy] : call_user_func($indexBy, $model);
444 15
                $result[$key][$index] = $model;
445
            }
446
        }
447
448 15
        return $result;
449
    }
450
451
    /**
452
     * @param array $attributes the attributes to prefix
453
     * @return array
454
     */
455 163
    private function prefixKeyColumns($attributes)
456
    {
457 163
        if ($this instanceof ActiveQuery && (!empty($this->join) || !empty($this->joinWith))) {
458 27
            if (empty($this->from)) {
459
                /* @var $modelClass ActiveRecord */
460 6
                $modelClass = $this->modelClass;
461 6
                $alias = $modelClass::tableName();
462
            } else {
463 27
                foreach ($this->from as $alias => $table) {
464 27
                    if (!is_string($alias)) {
465 27
                        $alias = $table;
466
                    }
467 27
                    break;
468
                }
469
            }
470 27
            if (isset($alias)) {
471 27
                foreach ($attributes as $i => $attribute) {
472 27
                    $attributes[$i] = "$alias.$attribute";
473
                }
474
            }
475
        }
476
477 163
        return $attributes;
478
    }
479
480
    /**
481
     * @param array $models
482
     */
483 163
    private function filterByModels($models)
484
    {
485 163
        $attributes = array_keys($this->link);
486
487 163
        $attributes = $this->prefixKeyColumns($attributes);
488
489 163
        $values = [];
490 163
        if (count($attributes) === 1) {
491
            // single key
492 160
            $attribute = reset($this->link);
493 160
            foreach ($models as $model) {
494 160
                if (($value = $model[$attribute]) !== null) {
495 160
                    if (is_array($value)) {
496
                        $values = array_merge($values, $value);
497
                    } else {
498 160
                        $values[] = $value;
499
                    }
500
                }
501
            }
502 160
            if (empty($values)) {
503 160
                $this->emulateExecution();
0 ignored issues
show
Bug introduced by
It seems like emulateExecution() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
504
            }
505
        } else {
506
            // composite keys
507
508
            // ensure keys of $this->link are prefixed the same way as $attributes
509 6
            $prefixedLink = array_combine(
510 6
                $attributes,
511 6
                array_values($this->link)
512
            );
513 6
            foreach ($models as $model) {
514 6
                $v = [];
515 6
                foreach ($prefixedLink as $attribute => $link) {
516 6
                    $v[$attribute] = $model[$link];
517
                }
518 6
                $values[] = $v;
519 6
                if (empty($v)) {
520 6
                    $this->emulateExecution();
0 ignored issues
show
Bug introduced by
It seems like emulateExecution() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
521
                }
522
            }
523
        }
524 163
        $this->andWhere(['in', $attributes, array_unique($values, SORT_REGULAR)]);
0 ignored issues
show
Bug introduced by
It seems like andWhere() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
525 163
    }
526
527
    /**
528
     * @param ActiveRecordInterface|array $model
529
     * @param array $attributes
530
     * @return string
531
     */
532 69
    private function getModelKey($model, $attributes)
533
    {
534 69
        $key = [];
535 69
        foreach ($attributes as $attribute) {
536 69
            $key[] = $this->normalizeModelKey($model[$attribute]);
537
        }
538 69
        if (count($key) > 1) {
539
            return serialize($key);
540
        }
541 69
        $key = reset($key);
542 69
        return is_scalar($key) ? $key : serialize($key);
543
    }
544
545
    /**
546
     * @param mixed $value raw key value.
547
     * @return string normalized key value.
548
     */
549 69
    private function normalizeModelKey($value)
550
    {
551 69
        if (is_object($value) && method_exists($value, '__toString')) {
552
            // ensure matching to special objects, which are convertable to string, for cross-DBMS relations, for example: `|MongoId`
553
            $value = $value->__toString();
554
        }
555
556 69
        return $value;
557
    }
558
559
    /**
560
     * @param array $primaryModels either array of AR instances or arrays
561
     * @return array
562
     */
563 21
    private function findJunctionRows($primaryModels)
564
    {
565 21
        if (empty($primaryModels)) {
566
            return [];
567
        }
568 21
        $this->filterByModels($primaryModels);
569
        /* @var $primaryModel ActiveRecord */
570 21
        $primaryModel = reset($primaryModels);
571 21
        if (!$primaryModel instanceof ActiveRecordInterface) {
572
            // when primaryModels are array of arrays (asArray case)
573
            $primaryModel = $this->modelClass;
574
        }
575
576 21
        return $this->asArray()->all($primaryModel::getDb());
0 ignored issues
show
Bug introduced by
It seems like asArray() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
577
    }
578
}
579