Completed
Push — 2.1 ( c952e8...98ed49 )
by Carsten
10:00
created

ActiveRelationTrait::populateRelation()   F

Complexity

Conditions 26
Paths 373

Size

Total Lines 93
Code Lines 59

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 53
CRAP Score 32.1714

Importance

Changes 0
Metric Value
dl 0
loc 93
rs 3.5303
c 0
b 0
f 0
ccs 53
cts 67
cp 0.791
cc 26
eloc 59
nc 373
nop 2
crap 32.1714

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\InvalidConfigException;
11
use yii\base\InvalidParamException;
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 boolean 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 6
        }
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
     * public function getOrders()
87
     * {
88
     *     return $this->hasOne(Order::class, ['id' => 'order_id']);
89
     * }
90
     *
91
     * public function getOrderItems()
92
     * {
93
     *     return $this->hasMany(Item::class, ['id' => 'item_id'])
94
     *                 ->via('orders');
95
     * }
96
     * ```
97
     *
98
     * @param string $relationName the relation name. This refers to a relation declared in [[primaryModel]].
99
     * @param callable $callable a PHP callback for customizing the relation associated with the junction table.
100
     * Its signature should be `function($query)`, where `$query` is the query to be customized.
101
     * @return $this the relation object itself.
102
     */
103 60
    public function via($relationName, callable $callable = null)
104
    {
105 60
        $relation = $this->primaryModel->getRelation($relationName);
106 60
        $this->via = [$relationName, $relation];
107 60
        if ($callable !== null) {
108 42
            call_user_func($callable, $relation);
109 42
        }
110 60
        return $this;
111
    }
112
113
    /**
114
     * Sets the name of the relation that is the inverse of this relation.
115
     * For example, an order has a customer, which means the inverse of the "customer" relation
116
     * is the "orders", and the inverse of the "orders" relation is the "customer".
117
     * If this property is set, the primary record(s) will be referenced through the specified relation.
118
     * For example, `$customer->orders[0]->customer` and `$customer` will be the same object,
119
     * and accessing the customer of an order will not trigger a new DB query.
120
     *
121
     * Use this method when declaring a relation in the [[ActiveRecord]] class:
122
     *
123
     * ```php
124
     * public function getOrders()
125
     * {
126
     *     return $this->hasMany(Order::class, ['customer_id' => 'id'])->inverseOf('customer');
127
     * }
128
     * ```
129
     *
130
     * @param string $relationName the name of the relation that is the inverse of this relation.
131
     * @return $this the relation object itself.
132
     */
133 3
    public function inverseOf($relationName)
134
    {
135 3
        $this->inverseOf = $relationName;
136 3
        return $this;
137
    }
138
139
    /**
140
     * Finds the related records for the specified primary record.
141
     * This method is invoked when a relation of an ActiveRecord is being accessed in a lazy fashion.
142
     * @param string $name the relation name
143
     * @param ActiveRecordInterface|BaseActiveRecord $model the primary model
144
     * @return mixed the related record(s)
145
     * @throws InvalidParamException if the relation is invalid
146
     */
147 48
    public function findFor($name, $model)
148
    {
149 48
        if (method_exists($model, 'get' . $name)) {
150 48
            $method = new \ReflectionMethod($model, 'get' . $name);
151 48
            $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...
152 48
            if ($realName !== $name) {
153
                throw new InvalidParamException('Relation names are case sensitive. ' . get_class($model) . " has a relation named \"$realName\" instead of \"$name\".");
154
            }
155 48
        }
156
157 48
        $related = $this->multiple ? $this->all() : $this->one();
158
159 48
        if ($this->inverseOf === null || empty($related)) {
160 45
            return $related;
161
        }
162
163 3
        if ($this->multiple) {
164 3
            foreach ($related as $i => $relatedModel) {
0 ignored issues
show
Bug introduced by
The expression $related of type array<integer,object<yii...\ActiveRecordInterface> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
165 3
                if ($relatedModel instanceof ActiveRecordInterface) {
166 3
                    if (!isset($inverseRelation)) {
167 3
                        $inverseRelation = $relatedModel->getRelation($this->inverseOf);
168 3
                    }
169 3
                    $relatedModel->populateRelation($this->inverseOf, $inverseRelation->multiple ? [$model] : $model);
170 3
                } else {
171
                    if (!isset($inverseRelation)) {
172
                        $inverseRelation = (new $this->modelClass)->getRelation($this->inverseOf);
173
                    }
174
                    $related[$i][$this->inverseOf] = $inverseRelation->multiple ? [$model] : $model;
175
                }
176 3
            }
177 3
        } else {
178
            if ($related instanceof ActiveRecordInterface) {
179
                $inverseRelation = $related->getRelation($this->inverseOf);
180
                $related->populateRelation($this->inverseOf, $inverseRelation->multiple ? [$model] : $model);
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...
181
            } else {
182
                $inverseRelation = (new $this->modelClass)->getRelation($this->inverseOf);
183
                $related[$this->inverseOf] = $inverseRelation->multiple ? [$model] : $model;
184
            }
185
        }
186
187 3
        return $related;
188
    }
189
190
    /**
191
     * Finds the related records and populates them into the primary models.
192
     * @param string $name the relation name
193
     * @param array $primaryModels primary models
194
     * @return array the related models
195
     * @throws InvalidConfigException if [[link]] is invalid
196
     */
197 58
    public function populateRelation($name, &$primaryModels)
198
    {
199 58
        if (!is_array($this->link)) {
200
            throw new InvalidConfigException('Invalid link: it must be an array of key-value pairs.');
201
        }
202
203 58
        if ($this->via instanceof self) {
204
            // via junction table
205
            /* @var $viaQuery ActiveRelationTrait */
206 9
            $viaQuery = $this->via;
207 9
            $viaModels = $viaQuery->findJunctionRows($primaryModels);
208 9
            $this->filterByModels($viaModels);
209 58
        } elseif (is_array($this->via)) {
210
            // via relation
211
            /* @var $viaQuery ActiveRelationTrait|ActiveQueryTrait */
212 33
            list($viaName, $viaQuery) = $this->via;
213 33
            if ($viaQuery->asArray === null) {
214
                // inherit asArray from primary query
215 33
                $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...
216 33
            }
217 33
            $viaQuery->primaryModel = null;
218 33
            $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...
219 33
            $this->filterByModels($viaModels);
220 33
        } else {
221 58
            $this->filterByModels($primaryModels);
222
        }
223
224 58
        if (!$this->multiple && count($primaryModels) === 1) {
225 15
            $model = $this->one();
226 15
            foreach ($primaryModels as $i => $primaryModel) {
227 15
                if ($primaryModel instanceof ActiveRecordInterface) {
228 15
                    $primaryModel->populateRelation($name, $model);
229 15
                } else {
230 3
                    $primaryModels[$i][$name] = $model;
231
                }
232 15
                if ($this->inverseOf !== null) {
233 3
                    $this->populateInverseRelation($primaryModels, [$model], $name, $this->inverseOf);
234 3
                }
235 15
            }
236
237 15
            return [$model];
238
        } else {
239
            // https://github.com/yiisoft/yii2/issues/3197
240
            // delay indexing related models after buckets are built
241 52
            $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...
242 52
            $this->indexBy = null;
243 52
            $models = $this->all();
244
245 52
            if (isset($viaModels, $viaQuery)) {
246 33
                $buckets = $this->buildBuckets($models, $this->link, $viaModels, $viaQuery->link);
247 33
            } else {
248 52
                $buckets = $this->buildBuckets($models, $this->link);
249
            }
250
251 52
            $this->indexBy = $indexBy;
252 52
            if ($this->indexBy !== null && $this->multiple) {
253 15
                $buckets = $this->indexBuckets($buckets, $this->indexBy);
254 15
            }
255
256 52
            $link = array_values(isset($viaQuery) ? $viaQuery->link : $this->link);
257 52
            foreach ($primaryModels as $i => $primaryModel) {
258 52
                if ($this->multiple && count($link) === 1 && is_array($keys = $primaryModel[reset($link)])) {
259
                    $value = [];
260
                    foreach ($keys as $key) {
261
                        $key = $this->normalizeModelKey($key);
262
                        if (isset($buckets[$key])) {
263
                            if ($this->indexBy !== null) {
264
                                // if indexBy is set, array_merge will cause renumbering of numeric array
265
                                foreach ($buckets[$key] as $bucketKey => $bucketValue) {
266
                                    $value[$bucketKey] = $bucketValue;
267
                                }
268
                            } else {
269
                                $value = array_merge($value, $buckets[$key]);
270
                            }
271
                        }
272
                    }
273
                } else {
274 52
                    $key = $this->getModelKey($primaryModel, $link);
275 52
                    $value = isset($buckets[$key]) ? $buckets[$key] : ($this->multiple ? [] : null);
276
                }
277 52
                if ($primaryModel instanceof ActiveRecordInterface) {
278 52
                    $primaryModel->populateRelation($name, $value);
279 52
                } else {
280 6
                    $primaryModels[$i][$name] = $value;
281
                }
282 52
            }
283 52
            if ($this->inverseOf !== null) {
284 3
                $this->populateInverseRelation($primaryModels, $models, $name, $this->inverseOf);
285 3
            }
286
287 52
            return $models;
288
        }
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 3
    private function populateInverseRelation(&$primaryModels, $models, $primaryName, $name)
298
    {
299 3
        if (empty($models) || empty($primaryModels)) {
300
            return;
301
        }
302 3
        $model = reset($models);
303
        /* @var $relation ActiveQueryInterface|ActiveQuery */
304 3
        $relation = $model instanceof ActiveRecordInterface ? $model->getRelation($name) : (new $this->modelClass)->getRelation($name);
305
306 3
        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...
307 3
            $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...
308 3
            if ($model instanceof ActiveRecordInterface) {
309 3
                foreach ($models as $model) {
310 3
                    $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...
311 3
                    $model->populateRelation($name, isset($buckets[$key]) ? $buckets[$key] : []);
312 3
                }
313 3
            } else {
314 3
                foreach ($primaryModels as $i => $primaryModel) {
315 3
                    if ($this->multiple) {
316
                        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...
317
                            $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...
318
                            $primaryModels[$i][$j][$name] = isset($buckets[$key]) ? $buckets[$key] : [];
319
                        }
320 3
                    } elseif (!empty($primaryModel[$primaryName])) {
321 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...
322 3
                        $primaryModels[$i][$primaryName][$name] = isset($buckets[$key]) ? $buckets[$key] : [];
323 3
                    }
324 3
                }
325
            }
326 3
        } else {
327 3
            if ($this->multiple) {
328 3
                foreach ($primaryModels as $i => $primaryModel) {
329 3
                    foreach ($primaryModel[$primaryName] as $j => $m) {
330 3
                        if ($m instanceof ActiveRecordInterface) {
331 3
                            $m->populateRelation($name, $primaryModel);
332 3
                        } else {
333 3
                            $primaryModels[$i][$primaryName][$j][$name] = $primaryModel;
334
                        }
335 3
                    }
336 3
                }
337 3
            } else {
338
                foreach ($primaryModels as $i => $primaryModel) {
339
                    if ($primaryModels[$i][$primaryName] instanceof ActiveRecordInterface) {
340
                        $primaryModels[$i][$primaryName]->populateRelation($name, $primaryModel);
341
                    } elseif (!empty($primaryModels[$i][$primaryName])) {
342
                        $primaryModels[$i][$primaryName][$name] = $primaryModel;
343
                    }
344
                }
345
            }
346
        }
347 3
    }
348
349
    /**
350
     * @param array $models
351
     * @param array $link
352
     * @param array $viaModels
353
     * @param array $viaLink
354
     * @param boolean $checkMultiple
355
     * @return array
356
     */
357 52
    private function buildBuckets($models, $link, $viaModels = null, $viaLink = null, $checkMultiple = true)
358
    {
359 52
        if ($viaModels !== null) {
360 33
            $map = [];
361 33
            $viaLinkKeys = array_keys($viaLink);
362 33
            $linkValues = array_values($link);
363 33
            foreach ($viaModels as $viaModel) {
364 33
                $key1 = $this->getModelKey($viaModel, $viaLinkKeys);
365 33
                $key2 = $this->getModelKey($viaModel, $linkValues);
366 33
                $map[$key2][$key1] = true;
367 33
            }
368 33
        }
369
370 52
        $buckets = [];
371 52
        $linkKeys = array_keys($link);
372
373 52
        if (isset($map)) {
374 33
            foreach ($models as $model) {
375 33
                $key = $this->getModelKey($model, $linkKeys);
376 33
                if (isset($map[$key])) {
377 33
                    foreach (array_keys($map[$key]) as $key2) {
378 33
                        $buckets[$key2][] = $model;
379 33
                    }
380 33
                }
381 33
            }
382 33
        } else {
383 52
            foreach ($models as $model) {
384 52
                $key = $this->getModelKey($model, $linkKeys);
385 52
                $buckets[$key][] = $model;
386 52
            }
387
        }
388
389 52
        if ($checkMultiple && !$this->multiple) {
390 15
            foreach ($buckets as $i => $bucket) {
391 15
                $buckets[$i] = reset($bucket);
392 15
            }
393 15
        }
394
395 52
        return $buckets;
396
    }
397
398
399
    /**
400
     * Indexes buckets by column name.
401
     *
402
     * @param array $buckets
403
     * @var string|callable $column the name of the column by which the query results should be indexed by.
404
     * This can also be a callable (e.g. anonymous function) that returns the index value based on the given row data.
405
     * @return array
406
     */
407 15
    private function indexBuckets($buckets, $indexBy)
408
    {
409 15
        $result = [];
410 15
        foreach ($buckets as $key => $models) {
411 15
            $result[$key] = [];
412 15
            foreach ($models as $model) {
413 15
                $index = is_string($indexBy) ? $model[$indexBy] : call_user_func($indexBy, $model);
414 15
                $result[$key][$index] = $model;
415 15
            }
416 15
        }
417 15
        return $result;
418
    }
419
420
    /**
421
     * @param array $attributes the attributes to prefix
422
     * @return array
423
     */
424 115
    private function prefixKeyColumns($attributes)
425
    {
426 115
        if ($this instanceof ActiveQuery && (!empty($this->join) || !empty($this->joinWith))) {
427 24
            if (empty($this->from)) {
428
                /* @var $modelClass ActiveRecord */
429 6
                $modelClass = $this->modelClass;
430 6
                $alias = $modelClass::tableName();
431 6
            } else {
432 24
                foreach ($this->from as $alias => $table) {
433 24
                    if (!is_string($alias)) {
434 24
                        $alias = $table;
435 24
                    }
436 24
                    break;
437 24
                }
438
            }
439 24
            if (isset($alias)) {
440 24
                foreach ($attributes as $i => $attribute) {
441 24
                    $attributes[$i] = "$alias.$attribute";
442 24
                }
443 24
            }
444 24
        }
445 115
        return $attributes;
446
    }
447
448
    /**
449
     * @param array $models
450
     */
451 115
    private function filterByModels($models)
452
    {
453 115
        $attributes = array_keys($this->link);
454
455 115
        $attributes = $this->prefixKeyColumns($attributes);
456
457 115
        $values = [];
458 115
        if (count($attributes) === 1) {
459
            // single key
460 115
            $attribute = reset($this->link);
461 115
            foreach ($models as $model) {
462 115
                if (($value = $model[$attribute]) !== null) {
463 115
                    if (is_array($value)) {
464
                        $values = array_merge($values, $value);
465
                    } else {
466 115
                        $values[] = $value;
467
                    }
468 115
                }
469 115
            }
470 115
        } else {
471
            // composite keys
472
473
            // ensure keys of $this->link are prefixed the same way as $attributes
474 3
            $prefixedLink = array_combine(
475 3
                $attributes,
476 3
                array_values($this->link)
477 3
            );
478 3
            foreach ($models as $model) {
479 3
                $v = [];
480 3
                foreach ($prefixedLink as $attribute => $link) {
481 3
                    $v[$attribute] = $model[$link];
482 3
                }
483 3
                $values[] = $v;
484 3
            }
485
        }
486 115
        $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...
487 115
    }
488
489
    /**
490
     * @param ActiveRecordInterface|array $model
491
     * @param array $attributes
492
     * @return string
493
     */
494 52
    private function getModelKey($model, $attributes)
495
    {
496 52
        $key = [];
497 52
        foreach ($attributes as $attribute) {
498 52
            $key[] = $this->normalizeModelKey($model[$attribute]);
499 52
        }
500 52
        if (count($key) > 1) {
501
            return serialize($key);
502
        }
503 52
        $key = reset($key);
504 52
        return is_scalar($key) ? $key : serialize($key);
505
    }
506
507
    /**
508
     * @param mixed $value raw key value.
509
     * @return string normalized key value.
510
     */
511 52
    private function normalizeModelKey($value)
512
    {
513 52
        if (is_object($value) && method_exists($value, '__toString')) {
514
            // ensure matching to special objects, which are convertable to string, for cross-DBMS relations, for example: `|MongoId`
515
            $value = $value->__toString();
516
        }
517 52
        return $value;
518
    }
519
520
    /**
521
     * @param array $primaryModels either array of AR instances or arrays
522
     * @return array
523
     */
524 18
    private function findJunctionRows($primaryModels)
525
    {
526 18
        if (empty($primaryModels)) {
527
            return [];
528
        }
529 18
        $this->filterByModels($primaryModels);
530
        /* @var $primaryModel ActiveRecord */
531 18
        $primaryModel = reset($primaryModels);
532 18
        if (!$primaryModel instanceof ActiveRecordInterface) {
533
            // when primaryModels are array of arrays (asArray case)
534
            $primaryModel = $this->modelClass;
535
        }
536
537 18
        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...
538
    }
539
}
540