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

ActiveRelationTrait::addInverseRelations()   C

Complexity

Conditions 8
Paths 8

Size

Total Lines 22
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 8.0291

Importance

Changes 0
Metric Value
dl 0
loc 22
ccs 12
cts 13
cp 0.9231
rs 6.6037
c 0
b 0
f 0
cc 8
eloc 13
nc 8
nop 1
crap 8.0291
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
use yii\base\InvalidArgumentException;
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 66
    public function via($relationName, callable $callable = null)
105
    {
106 66
        $relation = $this->primaryModel->getRelation($relationName);
107 66
        $this->via = [$relationName, $relation];
108 66
        if ($callable !== null) {
109 45
            call_user_func($callable, $relation);
110
        }
111
112 66
        return $this;
113
    }
114
115
    /**
116
     * Sets the name of the relation that is the inverse of this relation.
117
     * For example, an order has a customer, which means the inverse of the "customer" relation
118
     * is the "orders", and the inverse of the "orders" relation is the "customer".
119
     * If this property is set, the primary record(s) will be referenced through the specified relation.
120
     * For example, `$customer->orders[0]->customer` and `$customer` will be the same object,
121
     * and accessing the customer of an order will not trigger a new DB query.
122
     *
123
     * Use this method when declaring a relation in the [[ActiveRecord]] class:
124
     *
125
     * ```php
126
     * public function getOrders()
127
     * {
128
     *     return $this->hasMany(Order::class, ['customer_id' => 'id'])->inverseOf('customer');
129
     * }
130
     * ```
131
     *
132
     * @param string $relationName the name of the relation that is the inverse of this relation.
133
     * @return $this the relation object itself.
134
     */
135 9
    public function inverseOf($relationName)
136
    {
137 9
        $this->inverseOf = $relationName;
138 9
        return $this;
139
    }
140
141
    /**
142
     * Finds the related records for the specified primary record.
143
     * This method is invoked when a relation of an ActiveRecord is being accessed in a lazy fashion.
144
     * @param string $name the relation name
145
     * @param ActiveRecordInterface|BaseActiveRecord $model the primary model
146
     * @return mixed the related record(s)
147
     * @throws InvalidArgumentException if the relation is invalid
148
     */
149 55
    public function findFor($name, $model)
150
    {
151 55
        if (method_exists($model, 'get' . $name)) {
152 55
            $method = new \ReflectionMethod($model, 'get' . $name);
153 55
            $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...
154 55
            if ($realName !== $name) {
155
                throw new InvalidArgumentException('Relation names are case sensitive. ' . get_class($model)
156
                    . " has a relation named \"$realName\" instead of \"$name\".");
157
            }
158
        }
159
160 55
        return $this->multiple ? $this->all() : $this->one();
161
    }
162
163
    /**
164
     * If applicable, populate the query's primary model into the related records' inverse relationship.
165
     * @param array $result the array of related records as generated by [[populate()]]
166
     * @since 2.0.9
167
     */
168 9
    private function addInverseRelations(&$result)
169
    {
170 9
        if ($this->inverseOf === null) {
171
            return;
172
        }
173
174 9
        foreach ($result as $i => $relatedModel) {
175 9
            if ($relatedModel instanceof ActiveRecordInterface) {
176 9
                if (!isset($inverseRelation)) {
177 9
                    $inverseRelation = $relatedModel->getRelation($this->inverseOf);
178
                }
179 9
                $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...
180
            } else {
181 6
                if (!isset($inverseRelation)) {
182
                    /* @var $modelClass ActiveRecordInterface */
183 6
                    $modelClass = $this->modelClass;
184 6
                    $inverseRelation = $modelClass::instance()->getRelation($this->inverseOf);
185
                }
186 9
                $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...
187
            }
188
        }
189 9
    }
190
191
    /**
192
     * Finds the related records and populates them into the primary models.
193
     * @param string $name the relation name
194
     * @param array $primaryModels primary models
195
     * @return array the related models
196
     * @throws InvalidConfigException if [[link]] is invalid
197
     */
198 75
    public function populateRelation($name, &$primaryModels)
199
    {
200 75
        if (!is_array($this->link)) {
201
            throw new InvalidConfigException('Invalid link: it must be an array of key-value pairs.');
202
        }
203
204 75
        if ($this->via instanceof self) {
205
            // via junction table
206
            /* @var $viaQuery ActiveRelationTrait */
207 9
            $viaQuery = $this->via;
208 9
            $viaModels = $viaQuery->findJunctionRows($primaryModels);
209 9
            $this->filterByModels($viaModels);
210 75
        } elseif (is_array($this->via)) {
211
            // via relation
212
            /* @var $viaQuery ActiveRelationTrait|ActiveQueryTrait */
213 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...
214 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...
215
                // inherit asArray from primary query
216 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...
217
            }
218 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...
219 36
            $viaModels = $viaQuery->populateRelation($viaName, $primaryModels);
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...
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...
220 36
            $this->filterByModels($viaModels);
221
        } else {
222 75
            $this->filterByModels($primaryModels);
223
        }
224
225 75
        if (!$this->multiple && count($primaryModels) === 1) {
226 24
            $model = $this->one();
227 24
            $primaryModel = reset($primaryModels);
228 24
            if ($primaryModel instanceof ActiveRecordInterface) {
229 24
                $primaryModel->populateRelation($name, $model);
230
            } else {
231 3
                $primaryModels[key($primaryModels)][$name] = $model;
232
            }
233 24
            if ($this->inverseOf !== null) {
234 6
                $this->populateInverseRelation($primaryModels, [$model], $name, $this->inverseOf);
235
            }
236
237 24
            return [$model];
238
        }
239
240
        // https://github.com/yiisoft/yii2/issues/3197
241
        // delay indexing related models after buckets are built
242 63
        $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...
243 63
        $this->indexBy = null;
244 63
        $models = $this->all();
245
246 63
        if (isset($viaModels, $viaQuery)) {
247 36
            $buckets = $this->buildBuckets($models, $this->link, $viaModels, $viaQuery->link);
248
        } else {
249 63
            $buckets = $this->buildBuckets($models, $this->link);
250
        }
251
252 63
        $this->indexBy = $indexBy;
253 63
        if ($this->indexBy !== null && $this->multiple) {
254 15
            $buckets = $this->indexBuckets($buckets, $this->indexBy);
255
        }
256
257 63
        $link = array_values(isset($viaQuery) ? $viaQuery->link : $this->link);
258 63
        foreach ($primaryModels as $i => $primaryModel) {
259 63
            if ($this->multiple && count($link) === 1 && is_array($keys = $primaryModel[reset($link)])) {
260
                $value = [];
261
                foreach ($keys as $key) {
262
                    $key = $this->normalizeModelKey($key);
263
                    if (isset($buckets[$key])) {
264
                        if ($this->indexBy !== null) {
265
                            // if indexBy is set, array_merge will cause renumbering of numeric array
266
                            foreach ($buckets[$key] as $bucketKey => $bucketValue) {
267
                                $value[$bucketKey] = $bucketValue;
268
                            }
269
                        } else {
270
                            $value = array_merge($value, $buckets[$key]);
271
                        }
272
                    }
273
                }
274
            } else {
275 63
                $key = $this->getModelKey($primaryModel, $link);
276 63
                $value = isset($buckets[$key]) ? $buckets[$key] : ($this->multiple ? [] : null);
277
            }
278 63
            if ($primaryModel instanceof ActiveRecordInterface) {
279 63
                $primaryModel->populateRelation($name, $value);
280
            } else {
281 63
                $primaryModels[$i][$name] = $value;
282
            }
283
        }
284 63
        if ($this->inverseOf !== null) {
285 6
            $this->populateInverseRelation($primaryModels, $models, $name, $this->inverseOf);
286
        }
287
288 63
        return $models;
289
    }
290
291
    /**
292
     * @param ActiveRecordInterface[] $primaryModels primary models
293
     * @param ActiveRecordInterface[] $models models
294
     * @param string $primaryName the primary relation name
295
     * @param string $name the relation name
296
     */
297 9
    private function populateInverseRelation(&$primaryModels, $models, $primaryName, $name)
298
    {
299 9
        if (empty($models) || empty($primaryModels)) {
300
            return;
301
        }
302 9
        $model = reset($models);
303
        /* @var $relation ActiveQueryInterface|ActiveQuery */
304 9
        if ($model instanceof ActiveRecordInterface) {
305 9
            $relation = $model->getRelation($name);
306
        } else {
307
            /* @var $modelClass ActiveRecordInterface */
308 3
            $modelClass = $this->modelClass;
309 3
            $relation = $modelClass::instance()->getRelation($name);
310
        }
311
312 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...
313 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...
314 6
            if ($model instanceof ActiveRecordInterface) {
315 6
                foreach ($models as $model) {
316 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...
317 6
                    $model->populateRelation($name, isset($buckets[$key]) ? $buckets[$key] : []);
318
                }
319
            } else {
320 3
                foreach ($primaryModels as $i => $primaryModel) {
321 3
                    if ($this->multiple) {
322
                        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...
323
                            $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...
324
                            $primaryModels[$i][$j][$name] = isset($buckets[$key]) ? $buckets[$key] : [];
325
                        }
326 3
                    } elseif (!empty($primaryModel[$primaryName])) {
327 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...
328 6
                        $primaryModels[$i][$primaryName][$name] = isset($buckets[$key]) ? $buckets[$key] : [];
329
                    }
330
                }
331
            }
332
        } else {
333 6
            if ($this->multiple) {
334 6
                foreach ($primaryModels as $i => $primaryModel) {
335 6
                    foreach ($primaryModel[$primaryName] as $j => $m) {
336 6
                        if ($m instanceof ActiveRecordInterface) {
337 6
                            $m->populateRelation($name, $primaryModel);
338
                        } else {
339 6
                            $primaryModels[$i][$primaryName][$j][$name] = $primaryModel;
340
                        }
341
                    }
342
                }
343
            } else {
344
                foreach ($primaryModels as $i => $primaryModel) {
345
                    if ($primaryModels[$i][$primaryName] instanceof ActiveRecordInterface) {
346
                        $primaryModels[$i][$primaryName]->populateRelation($name, $primaryModel);
347
                    } elseif (!empty($primaryModels[$i][$primaryName])) {
348
                        $primaryModels[$i][$primaryName][$name] = $primaryModel;
349
                    }
350
                }
351
            }
352
        }
353 9
    }
354
355
    /**
356
     * @param array $models
357
     * @param array $link
358
     * @param array $viaModels
359
     * @param array $viaLink
360
     * @param bool $checkMultiple
361
     * @return array
362
     */
363 66
    private function buildBuckets($models, $link, $viaModels = null, $viaLink = null, $checkMultiple = true)
364
    {
365 66
        if ($viaModels !== null) {
366 36
            $map = [];
367 36
            $viaLinkKeys = array_keys($viaLink);
368 36
            $linkValues = array_values($link);
369 36
            foreach ($viaModels as $viaModel) {
370 36
                $key1 = $this->getModelKey($viaModel, $viaLinkKeys);
371 36
                $key2 = $this->getModelKey($viaModel, $linkValues);
372 36
                $map[$key2][$key1] = true;
373
            }
374
        }
375
376 66
        $buckets = [];
377 66
        $linkKeys = array_keys($link);
378
379 66
        if (isset($map)) {
380 36
            foreach ($models as $model) {
381 36
                $key = $this->getModelKey($model, $linkKeys);
382 36
                if (isset($map[$key])) {
383 36
                    foreach (array_keys($map[$key]) as $key2) {
384 36
                        $buckets[$key2][] = $model;
385
                    }
386
                }
387
            }
388
        } else {
389 66
            foreach ($models as $model) {
390 66
                $key = $this->getModelKey($model, $linkKeys);
391 66
                $buckets[$key][] = $model;
392
            }
393
        }
394
395 66
        if ($checkMultiple && !$this->multiple) {
396 18
            foreach ($buckets as $i => $bucket) {
397 18
                $buckets[$i] = reset($bucket);
398
            }
399
        }
400
401 66
        return $buckets;
402
    }
403
404
405
    /**
406
     * Indexes buckets by column name.
407
     *
408
     * @param array $buckets
409
     * @param string|callable $indexBy the name of the column by which the query results should be indexed by.
410
     * This can also be a callable (e.g. anonymous function) that returns the index value based on the given row data.
411
     * @return array
412
     */
413 15
    private function indexBuckets($buckets, $indexBy)
414
    {
415 15
        $result = [];
416 15
        foreach ($buckets as $key => $models) {
417 15
            $result[$key] = [];
418 15
            foreach ($models as $model) {
419 15
                $index = is_string($indexBy) ? $model[$indexBy] : call_user_func($indexBy, $model);
420 15
                $result[$key][$index] = $model;
421
            }
422
        }
423
424 15
        return $result;
425
    }
426
427
    /**
428
     * @param array $attributes the attributes to prefix
429
     * @return array
430
     */
431 136
    private function prefixKeyColumns($attributes)
432
    {
433 136
        if ($this instanceof ActiveQuery && (!empty($this->join) || !empty($this->joinWith))) {
434 27
            if (empty($this->from)) {
435
                /* @var $modelClass ActiveRecord */
436 6
                $modelClass = $this->modelClass;
437 6
                $alias = $modelClass::tableName();
438
            } else {
439 27
                foreach ($this->from as $alias => $table) {
440 27
                    if (!is_string($alias)) {
441 27
                        $alias = $table;
442
                    }
443 27
                    break;
444
                }
445
            }
446 27
            if (isset($alias)) {
447 27
                foreach ($attributes as $i => $attribute) {
448 27
                    $attributes[$i] = "$alias.$attribute";
449
                }
450
            }
451
        }
452
453 136
        return $attributes;
454
    }
455
456
    /**
457
     * @param array $models
458
     */
459 136
    private function filterByModels($models)
460
    {
461 136
        $attributes = array_keys($this->link);
462
463 136
        $attributes = $this->prefixKeyColumns($attributes);
464
465 136
        $values = [];
466 136
        if (count($attributes) === 1) {
467
            // single key
468 136
            $attribute = reset($this->link);
469 136
            foreach ($models as $model) {
470 136
                if (($value = $model[$attribute]) !== null) {
471 136
                    if (is_array($value)) {
472
                        $values = array_merge($values, $value);
473
                    } else {
474 136
                        $values[] = $value;
475
                    }
476
                }
477
            }
478 136
            if (empty($values)) {
479 136
                $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...
480
            }
481
        } else {
482
            // composite keys
483
484
            // ensure keys of $this->link are prefixed the same way as $attributes
485 3
            $prefixedLink = array_combine(
486 3
                $attributes,
487 3
                array_values($this->link)
488
            );
489 3
            foreach ($models as $model) {
490 3
                $v = [];
491 3
                foreach ($prefixedLink as $attribute => $link) {
492 3
                    $v[$attribute] = $model[$link];
493
                }
494 3
                $values[] = $v;
495 3
                if (empty($v)) {
496 3
                    $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...
497
                }
498
            }
499
        }
500 136
        $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...
501 136
    }
502
503
    /**
504
     * @param ActiveRecordInterface|array $model
505
     * @param array $attributes
506
     * @return string
507
     */
508 66
    private function getModelKey($model, $attributes)
509
    {
510 66
        $key = [];
511 66
        foreach ($attributes as $attribute) {
512 66
            $key[] = $this->normalizeModelKey($model[$attribute]);
513
        }
514 66
        if (count($key) > 1) {
515
            return serialize($key);
516
        }
517 66
        $key = reset($key);
518 66
        return is_scalar($key) ? $key : serialize($key);
519
    }
520
521
    /**
522
     * @param mixed $value raw key value.
523
     * @return string normalized key value.
524
     */
525 66
    private function normalizeModelKey($value)
526
    {
527 66
        if (is_object($value) && method_exists($value, '__toString')) {
528
            // ensure matching to special objects, which are convertable to string, for cross-DBMS relations, for example: `|MongoId`
529
            $value = $value->__toString();
530
        }
531
532 66
        return $value;
533
    }
534
535
    /**
536
     * @param array $primaryModels either array of AR instances or arrays
537
     * @return array
538
     */
539 21
    private function findJunctionRows($primaryModels)
540
    {
541 21
        if (empty($primaryModels)) {
542
            return [];
543
        }
544 21
        $this->filterByModels($primaryModels);
545
        /* @var $primaryModel ActiveRecord */
546 21
        $primaryModel = reset($primaryModels);
547 21
        if (!$primaryModel instanceof ActiveRecordInterface) {
548
            // when primaryModels are array of arrays (asArray case)
549
            $primaryModel = $this->modelClass;
550
        }
551
552 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...
553
    }
554
}
555