Completed
Push — master ( 63faf5...4333b9 )
by Alexander
93:38 queued 90:36
created

ActiveRelationTrait::filterByModels()   C

Complexity

Conditions 14
Paths 26

Size

Total Lines 58

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 32
CRAP Score 14.0398

Importance

Changes 0
Metric Value
dl 0
loc 58
ccs 32
cts 34
cp 0.9412
rs 6.2666
c 0
b 0
f 0
cc 14
nc 26
nop 1
crap 14.0398

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @link 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
    private $viaMap;
66
67
    /**
68
     * Clones internal objects.
69
     */
70 12
    public function __clone()
71
    {
72 12
        parent::__clone();
73
        // make a clone of "via" object so that the same query object can be reused multiple times
74 12
        if (is_object($this->via)) {
75
            $this->via = clone $this->via;
76 12
        } elseif (is_array($this->via)) {
77 6
            $this->via = [$this->via[0], clone $this->via[1]];
78
        }
79 12
    }
80
81
    /**
82
     * Specifies the relation associated with the junction table.
83
     *
84
     * Use this method to specify a pivot record/table when declaring a relation in the [[ActiveRecord]] class:
85
     *
86
     * ```php
87
     * class Order extends ActiveRecord
88
     * {
89
     *    public function getOrderItems() {
90
     *        return $this->hasMany(OrderItem::className(), ['order_id' => 'id']);
91
     *    }
92
     *
93
     *    public function getItems() {
94
     *        return $this->hasMany(Item::className(), ['id' => 'item_id'])
95
     *                    ->via('orderItems');
96
     *    }
97
     * }
98
     * ```
99
     *
100
     * @param string $relationName the relation name. This refers to a relation declared in [[primaryModel]].
101
     * @param callable $callable a PHP callback for customizing the relation associated with the junction table.
102
     * Its signature should be `function($query)`, where `$query` is the query to be customized.
103
     * @return $this the relation object itself.
104
     */
105 72
    public function via($relationName, callable $callable = null)
106
    {
107 72
        $relation = $this->primaryModel->getRelation($relationName);
108 72
        $this->via = [$relationName, $relation];
109 72
        if ($callable !== null) {
110 48
            call_user_func($callable, $relation);
111
        }
112
113 72
        return $this;
114
    }
115
116
    /**
117
     * Sets the name of the relation that is the inverse of this relation.
118
     * For example, a customer has orders, which means 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, e.g. in Customer model:
124
     *
125
     * ```php
126
     * public function getOrders()
127
     * {
128
     *     return $this->hasMany(Order::className(), ['customer_id' => 'id'])->inverseOf('customer');
129
     * }
130
     * ```
131
     *
132
     * This also may be used for Order model, but with caution:
133
     *
134
     * ```php
135
     * public function getCustomer()
136
     * {
137
     *     return $this->hasOne(Customer::className(), ['id' => 'customer_id'])->inverseOf('orders');
138
     * }
139
     * ```
140
     *
141
     * in this case result will depend on how order(s) was loaded.
142
     * Let's suppose customer has several orders. If only one order was loaded:
143
     *
144
     * ```php
145
     * $orders = Order::find()->where(['id' => 1])->all();
146
     * $customerOrders = $orders[0]->customer->orders;
147
     * ```
148
     *
149
     * variable `$customerOrders` will contain only one order. If orders was loaded like this:
150
     *
151
     * ```php
152
     * $orders = Order::find()->with('customer')->where(['customer_id' => 1])->all();
153
     * $customerOrders = $orders[0]->customer->orders;
154
     * ```
155
     *
156
     * variable `$customerOrders` will contain all orders of the customer.
157
     *
158
     * @param string $relationName the name of the relation that is the inverse of this relation.
159
     * @return $this the relation object itself.
160
     */
161 12
    public function inverseOf($relationName)
162
    {
163 12
        $this->inverseOf = $relationName;
164 12
        return $this;
165
    }
166
167
    /**
168
     * Finds the related records for the specified primary record.
169
     * This method is invoked when a relation of an ActiveRecord is being accessed in a lazy fashion.
170
     * @param string $name the relation name
171
     * @param ActiveRecordInterface|BaseActiveRecord $model the primary model
172
     * @return mixed the related record(s)
173
     * @throws InvalidArgumentException if the relation is invalid
174
     */
175 73
    public function findFor($name, $model)
176
    {
177 73
        if (method_exists($model, 'get' . $name)) {
178 73
            $method = new \ReflectionMethod($model, 'get' . $name);
179 73
            $realName = lcfirst(substr($method->getName(), 3));
180 73
            if ($realName !== $name) {
181
                throw new InvalidArgumentException('Relation names are case sensitive. ' . get_class($model) . " has a relation named \"$realName\" instead of \"$name\".");
182
            }
183
        }
184
185 73
        return $this->multiple ? $this->all() : $this->one();
186
    }
187
188
    /**
189
     * If applicable, populate the query's primary model into the related records' inverse relationship.
190
     * @param array $result the array of related records as generated by [[populate()]]
191
     * @since 2.0.9
192
     */
193 12
    private function addInverseRelations(&$result)
194
    {
195 12
        if ($this->inverseOf === null) {
196
            return;
197
        }
198
199 12
        foreach ($result as $i => $relatedModel) {
200 12
            if ($relatedModel instanceof ActiveRecordInterface) {
201 12
                if (!isset($inverseRelation)) {
202 12
                    $inverseRelation = $relatedModel->getRelation($this->inverseOf);
203
                }
204 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...
205
            } else {
206 9
                if (!isset($inverseRelation)) {
207
                    /* @var $modelClass ActiveRecordInterface */
208 9
                    $modelClass = $this->modelClass;
209 9
                    $inverseRelation = $modelClass::instance()->getRelation($this->inverseOf);
210
                }
211 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...
212
            }
213
        }
214 12
    }
215
216
    /**
217
     * Finds the related records and populates them into the primary models.
218
     * @param string $name the relation name
219
     * @param array $primaryModels primary models
220
     * @return array the related models
221
     * @throws InvalidConfigException if [[link]] is invalid
222
     */
223 93
    public function populateRelation($name, &$primaryModels)
224
    {
225 93
        if (!is_array($this->link)) {
226
            throw new InvalidConfigException('Invalid link: it must be an array of key-value pairs.');
227
        }
228
229 93
        if ($this->via instanceof self) {
230
            // via junction table
231
            /* @var $viaQuery ActiveRelationTrait */
232 9
            $viaQuery = $this->via;
233 9
            $viaModels = $viaQuery->findJunctionRows($primaryModels);
234 9
            $this->filterByModels($viaModels);
235 93
        } elseif (is_array($this->via)) {
236
            // via relation
237
            /* @var $viaQuery ActiveRelationTrait|ActiveQueryTrait */
238 39
            list($viaName, $viaQuery) = $this->via;
239 39
            if ($viaQuery->asArray === null) {
240
                // inherit asArray from primary query
241 39
                $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 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...
242
            }
243 39
            $viaQuery->primaryModel = null;
244 39
            $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...
245 39
            $this->filterByModels($viaModels);
246
        } else {
247 93
            $this->filterByModels($primaryModels);
248
        }
249
250 93
        if (!$this->multiple && count($primaryModels) === 1) {
251 33
            $model = $this->one();
252 33
            $primaryModel = reset($primaryModels);
253 33
            if ($primaryModel instanceof ActiveRecordInterface) {
254 33
                $primaryModel->populateRelation($name, $model);
255
            } else {
256 3
                $primaryModels[key($primaryModels)][$name] = $model;
257
            }
258 33
            if ($this->inverseOf !== null) {
259 6
                $this->populateInverseRelation($primaryModels, [$model], $name, $this->inverseOf);
260
            }
261
262 33
            return [$model];
263
        }
264
265
        // https://github.com/yiisoft/yii2/issues/3197
266
        // delay indexing related models after buckets are built
267 72
        $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...
268 72
        $this->indexBy = null;
269 72
        $models = $this->all();
270
271 72
        if (isset($viaModels, $viaQuery)) {
272 39
            $buckets = $this->buildBuckets($models, $this->link, $viaModels, $viaQuery);
273
        } else {
274 72
            $buckets = $this->buildBuckets($models, $this->link);
275
        }
276
277 72
        $this->indexBy = $indexBy;
278 72
        if ($this->indexBy !== null && $this->multiple) {
279 15
            $buckets = $this->indexBuckets($buckets, $this->indexBy);
280
        }
281
282 72
        $link = array_values($this->link);
283 72
        if (isset($viaQuery)) {
284 39
            $deepViaQuery = $viaQuery;
285 39
            while ($deepViaQuery->via) {
286 3
                $deepViaQuery = is_array($deepViaQuery->via) ? $deepViaQuery->via[1] : $deepViaQuery->via;
287
            };
288 39
            $link = array_values($deepViaQuery->link);
289
        }
290 72
        foreach ($primaryModels as $i => $primaryModel) {
291 72
            if ($this->multiple && count($link) === 1 && is_array($keys = $primaryModel[reset($link)])) {
292
                $value = [];
293
                foreach ($keys as $key) {
294
                    $key = $this->normalizeModelKey($key);
295
                    if (isset($buckets[$key])) {
296
                        if ($this->indexBy !== null) {
297
                            // if indexBy is set, array_merge will cause renumbering of numeric array
298
                            foreach ($buckets[$key] as $bucketKey => $bucketValue) {
299
                                $value[$bucketKey] = $bucketValue;
300
                            }
301
                        } else {
302
                            $value = array_merge($value, $buckets[$key]);
303
                        }
304
                    }
305
                }
306
            } else {
307 72
                $key = $this->getModelKey($primaryModel, $link);
308 72
                $value = isset($buckets[$key]) ? $buckets[$key] : ($this->multiple ? [] : null);
309
            }
310 72
            if ($primaryModel instanceof ActiveRecordInterface) {
311 72
                $primaryModel->populateRelation($name, $value);
312
            } else {
313 72
                $primaryModels[$i][$name] = $value;
314
            }
315
        }
316 72
        if ($this->inverseOf !== null) {
317 6
            $this->populateInverseRelation($primaryModels, $models, $name, $this->inverseOf);
318
        }
319
320 72
        return $models;
321
    }
322
323
    /**
324
     * @param ActiveRecordInterface[] $primaryModels primary models
325
     * @param ActiveRecordInterface[] $models models
326
     * @param string $primaryName the primary relation name
327
     * @param string $name the relation name
328
     */
329 9
    private function populateInverseRelation(&$primaryModels, $models, $primaryName, $name)
330
    {
331 9
        if (empty($models) || empty($primaryModels)) {
332
            return;
333
        }
334 9
        $model = reset($models);
335
        /* @var $relation ActiveQueryInterface|ActiveQuery */
336 9
        if ($model instanceof ActiveRecordInterface) {
337 9
            $relation = $model->getRelation($name);
338
        } else {
339
            /* @var $modelClass ActiveRecordInterface */
340 6
            $modelClass = $this->modelClass;
341 6
            $relation = $modelClass::instance()->getRelation($name);
342
        }
343
344 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...
345 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...
346 6
            if ($model instanceof ActiveRecordInterface) {
347 6
                foreach ($models as $model) {
348 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...
349 6
                    $model->populateRelation($name, isset($buckets[$key]) ? $buckets[$key] : []);
350
                }
351
            } else {
352 3
                foreach ($primaryModels as $i => $primaryModel) {
353 3
                    if ($this->multiple) {
354
                        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...
355
                            $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...
356
                            $primaryModels[$i][$j][$name] = isset($buckets[$key]) ? $buckets[$key] : [];
357
                        }
358 3
                    } elseif (!empty($primaryModel[$primaryName])) {
359 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...
360 6
                        $primaryModels[$i][$primaryName][$name] = isset($buckets[$key]) ? $buckets[$key] : [];
361
                    }
362
                }
363
            }
364 6
        } elseif ($this->multiple) {
365 6
            foreach ($primaryModels as $i => $primaryModel) {
366 6
                foreach ($primaryModel[$primaryName] as $j => $m) {
367 6
                    if ($m instanceof ActiveRecordInterface) {
368 6
                        $m->populateRelation($name, $primaryModel);
369
                    } else {
370 6
                        $primaryModels[$i][$primaryName][$j][$name] = $primaryModel;
371
                    }
372
                }
373
            }
374
        } else {
375
            foreach ($primaryModels as $i => $primaryModel) {
376
                if ($primaryModels[$i][$primaryName] instanceof ActiveRecordInterface) {
377
                    $primaryModels[$i][$primaryName]->populateRelation($name, $primaryModel);
378
                } elseif (!empty($primaryModels[$i][$primaryName])) {
379
                    $primaryModels[$i][$primaryName][$name] = $primaryModel;
380
                }
381
            }
382
        }
383 9
    }
384
385
    /**
386
     * @param array $models
387
     * @param array $link
388
     * @param array $viaModels
389
     * @param null|self $viaQuery
390
     * @param bool $checkMultiple
391
     * @return array
392
     */
393 75
    private function buildBuckets($models, $link, $viaModels = null, $viaQuery = null, $checkMultiple = true)
394
    {
395 75
        if ($viaModels !== null) {
396 39
            $map = [];
397 39
            $viaLink = $viaQuery->link;
398 39
            $viaLinkKeys = array_keys($viaLink);
399 39
            $linkValues = array_values($link);
400 39
            foreach ($viaModels as $viaModel) {
401 39
                $key1 = $this->getModelKey($viaModel, $viaLinkKeys);
402 39
                $key2 = $this->getModelKey($viaModel, $linkValues);
403 39
                $map[$key2][$key1] = true;
404
            }
405
406 39
            $viaQuery->viaMap = $map;
407
408 39
            $viaVia = $viaQuery->via;
409 39
            while ($viaVia) {
410 3
                $viaViaQuery = is_array($viaVia) ? $viaVia[1] : $viaVia;
411 3
                $map = $this->mapVia($map, $viaViaQuery->viaMap);
412
413 3
                $viaVia = $viaViaQuery->via;
414
            };
415
        }
416
417 75
        $buckets = [];
418 75
        $linkKeys = array_keys($link);
419
420 75
        if (isset($map)) {
421 39
            foreach ($models as $model) {
422 39
                $key = $this->getModelKey($model, $linkKeys);
423 39
                if (isset($map[$key])) {
424 39
                    foreach (array_keys($map[$key]) as $key2) {
425 39
                        $buckets[$key2][] = $model;
426
                    }
427
                }
428
            }
429
        } else {
430 75
            foreach ($models as $model) {
431 75
                $key = $this->getModelKey($model, $linkKeys);
432 75
                $buckets[$key][] = $model;
433
            }
434
        }
435
436 75
        if ($checkMultiple && !$this->multiple) {
437 24
            foreach ($buckets as $i => $bucket) {
438 24
                $buckets[$i] = reset($bucket);
439
            }
440
        }
441
442 75
        return $buckets;
443
    }
444
445
    /**
446
     * @param array $map
447
     * @param array $viaMap
448
     * @return array
449
     */
450 3
    private function mapVia($map, $viaMap) {
451 3
        $resultMap = [];
452 3
        foreach ($map as $key => $linkKeys) {
453 3
            foreach (array_keys($linkKeys) as $linkKey) {
454 3
                $resultMap[$key] = $viaMap[$linkKey];
455
            }
456
        }
457 3
        return $resultMap;
458
    }
459
460
    /**
461
     * Indexes buckets by column name.
462
     *
463
     * @param array $buckets
464
     * @param string|callable $indexBy the name of the column by which the query results should be indexed by.
465
     * This can also be a callable (e.g. anonymous function) that returns the index value based on the given row data.
466
     * @return array
467
     */
468 15
    private function indexBuckets($buckets, $indexBy)
469
    {
470 15
        $result = [];
471 15
        foreach ($buckets as $key => $models) {
472 15
            $result[$key] = [];
473 15
            foreach ($models as $model) {
474 15
                $index = is_string($indexBy) ? $model[$indexBy] : call_user_func($indexBy, $model);
475 15
                $result[$key][$index] = $model;
476
            }
477
        }
478
479 15
        return $result;
480
    }
481
482
    /**
483
     * @param array $attributes the attributes to prefix
484
     * @return array
485
     */
486 178
    private function prefixKeyColumns($attributes)
487
    {
488 178
        if ($this instanceof ActiveQuery && (!empty($this->join) || !empty($this->joinWith))) {
489 27
            if (empty($this->from)) {
490
                /* @var $modelClass ActiveRecord */
491 6
                $modelClass = $this->modelClass;
492 6
                $alias = $modelClass::tableName();
493
            } else {
494 27
                foreach ($this->from as $alias => $table) {
495 27
                    if (!is_string($alias)) {
496 27
                        $alias = $table;
497
                    }
498 27
                    break;
499
                }
500
            }
501 27
            if (isset($alias)) {
502 27
                foreach ($attributes as $i => $attribute) {
503 27
                    $attributes[$i] = "$alias.$attribute";
504
                }
505
            }
506
        }
507
508 178
        return $attributes;
509
    }
510
511
    /**
512
     * @param array $models
513
     */
514 178
    private function filterByModels($models)
515
    {
516 178
        $attributes = array_keys($this->link);
517
518 178
        $attributes = $this->prefixKeyColumns($attributes);
519
520 178
        $values = [];
521 178
        if (count($attributes) === 1) {
522
            // single key
523 175
            $attribute = reset($this->link);
524 175
            foreach ($models as $model) {
525 175
                if (($value = $model[$attribute]) !== null) {
526 172
                    if (is_array($value)) {
527
                        $values = array_merge($values, $value);
528 172
                    } elseif ($value instanceof ArrayExpression && $value->getDimension() === 1) {
529
                        $values = array_merge($values, $value->getValue());
530
                    } else {
531 175
                        $values[] = $value;
532
                    }
533
                }
534
            }
535 175
            if (empty($values)) {
536 175
                $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...
537
            }
538
        } else {
539
            // composite keys
540
541
            // ensure keys of $this->link are prefixed the same way as $attributes
542 6
            $prefixedLink = array_combine($attributes, $this->link);
543 6
            foreach ($models as $model) {
544 6
                $v = [];
545 6
                foreach ($prefixedLink as $attribute => $link) {
546 6
                    $v[$attribute] = $model[$link];
547
                }
548 6
                $values[] = $v;
549 6
                if (empty($v)) {
550 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...
551
                }
552
            }
553
        }
554
555 178
        if (!empty($values)) {
556 175
            $scalarValues = [];
557 175
            $nonScalarValues = [];
558 175
            foreach ($values as $value) {
559 175
                if (is_scalar($value)) {
560 172
                    $scalarValues[] = $value;
561
                } else {
562 175
                    $nonScalarValues[] = $value;
563
                }
564
            }
565
566 175
            $scalarValues = array_unique($scalarValues);
567 175
            $values = array_merge($scalarValues, $nonScalarValues);
568
        }
569
570 178
        $this->andWhere(['in', $attributes, $values]);
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...
571 178
    }
572
573
    /**
574
     * @param ActiveRecordInterface|array $model
575
     * @param array $attributes
576
     * @return string
577
     */
578 75
    private function getModelKey($model, $attributes)
579
    {
580 75
        $key = [];
581 75
        foreach ($attributes as $attribute) {
582 75
            $key[] = $this->normalizeModelKey($model[$attribute]);
583
        }
584 75
        if (count($key) > 1) {
585
            return serialize($key);
586
        }
587 75
        $key = reset($key);
588 75
        return is_scalar($key) ? $key : serialize($key);
589
    }
590
591
    /**
592
     * @param mixed $value raw key value.
593
     * @return string normalized key value.
594
     */
595 75
    private function normalizeModelKey($value)
596
    {
597 75
        if (is_object($value) && method_exists($value, '__toString')) {
598
            // ensure matching to special objects, which are convertable to string, for cross-DBMS relations, for example: `|MongoId`
599
            $value = $value->__toString();
600
        }
601
602 75
        return $value;
603
    }
604
605
    /**
606
     * @param array $primaryModels either array of AR instances or arrays
607
     * @return array
608
     */
609 21
    private function findJunctionRows($primaryModels)
610
    {
611 21
        if (empty($primaryModels)) {
612
            return [];
613
        }
614 21
        $this->filterByModels($primaryModels);
615
        /* @var $primaryModel ActiveRecord */
616 21
        $primaryModel = reset($primaryModels);
617 21
        if (!$primaryModel instanceof ActiveRecordInterface) {
618
            // when primaryModels are array of arrays (asArray case)
619
            $primaryModel = $this->modelClass;
620
        }
621
622 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...
623
    }
624
}
625