ActiveRelationTrait::populateRelation()   F
last analyzed

Complexity

Conditions 28
Paths 2001

Size

Total Lines 103
Code Lines 66

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 52
CRAP Score 30.5158

Importance

Changes 0
Metric Value
cc 28
eloc 66
nc 2001
nop 2
dl 0
loc 103
ccs 52
cts 61
cp 0.8525
crap 30.5158
rs 0
c 0
b 0
f 0

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
/**
4
 * @link https://www.yiiframework.com/
5
 * @copyright Copyright (c) 2008 Yii Software LLC
6
 * @license https://www.yiiframework.com/license/
7
 */
8
9
namespace yii\db;
10
11
use yii\base\InvalidArgumentException;
12
use yii\base\InvalidConfigException;
13
14
/**
15
 * ActiveRelationTrait implements the common methods and properties for active record relational queries.
16
 *
17
 * @author Qiang Xue <[email protected]>
18
 * @author Carsten Brandt <[email protected]>
19
 * @since 2.0
20
 * @phpcs:disable Squiz.NamingConventions.ValidVariableName.PrivateNoUnderscore
21
 *
22
 * @method ActiveRecordInterface|array|null one($db = null) See [[ActiveQueryInterface::one()]] for more info.
23
 * @method ActiveRecordInterface[] all($db = null) See [[ActiveQueryInterface::all()]] for more info.
24
 * @property ActiveRecord $modelClass
25
 */
26
trait ActiveRelationTrait
27
{
28
    /**
29
     * @var bool whether this query represents a relation to more than one record.
30
     * This property is only used in relational context. If true, this relation will
31
     * populate all query results into AR instances using [[Query::all()|all()]].
32
     * If false, only the first row of the results will be retrieved using [[Query::one()|one()]].
33
     */
34
    public $multiple;
35
    /**
36
     * @var ActiveRecord the primary model of a relational query.
37
     * This is used only in lazy loading with dynamic query options.
38
     */
39
    public $primaryModel;
40
    /**
41
     * @var array the columns of the primary and foreign tables that establish a relation.
42
     * The array keys must be columns of the table for this relation, and the array values
43
     * must be the corresponding columns from the primary table.
44
     * Do not prefix or quote the column names as this will be done automatically by Yii.
45
     * This property is only used in relational context.
46
     */
47
    public $link;
48
    /**
49
     * @var array|object the query associated with the junction table. Please call [[via()]]
50
     * to set this property instead of directly setting it.
51
     * This property is only used in relational context.
52
     * @see via()
53
     */
54
    public $via;
55
    /**
56
     * @var string the name of the relation that is the inverse of this relation.
57
     * For example, an order has a customer, which means the inverse of the "customer" relation
58
     * is the "orders", and the inverse of the "orders" relation is the "customer".
59
     * If this property is set, the primary record(s) will be referenced through the specified relation.
60
     * For example, `$customer->orders[0]->customer` and `$customer` will be the same object,
61
     * and accessing the customer of an order will not trigger new DB query.
62
     * This property is only used in relational context.
63
     * @see inverseOf()
64
     */
65
    public $inverseOf;
66
67
    private $viaMap;
68
69
    /**
70
     * Clones internal objects.
71
     */
72 13
    public function __clone()
73
    {
74 13
        parent::__clone();
75
        // make a clone of "via" object so that the same query object can be reused multiple times
76 13
        if (is_object($this->via)) {
77
            $this->via = clone $this->via;
78 13
        } elseif (is_array($this->via)) {
0 ignored issues
show
introduced by
The condition is_array($this->via) is always true.
Loading history...
79 6
            $this->via = [$this->via[0], clone $this->via[1], $this->via[2]];
80
        }
81
    }
82
83
    /**
84
     * Specifies the relation associated with the junction table.
85
     *
86
     * Use this method to specify a pivot record/table when declaring a relation in the [[ActiveRecord]] class:
87
     *
88
     * ```php
89
     * class Order extends ActiveRecord
90
     * {
91
     *    public function getOrderItems() {
92
     *        return $this->hasMany(OrderItem::class, ['order_id' => 'id']);
93
     *    }
94
     *
95
     *    public function getItems() {
96
     *        return $this->hasMany(Item::class, ['id' => 'item_id'])
97
     *                    ->via('orderItems');
98
     *    }
99
     * }
100
     * ```
101
     *
102
     * @param string $relationName the relation name. This refers to a relation declared in [[primaryModel]].
103
     * @param callable|null $callable a PHP callback for customizing the relation associated with the junction table.
104
     * Its signature should be `function($query)`, where `$query` is the query to be customized.
105
     * @return $this the relation object itself.
106
     */
107 96
    public function via($relationName, ?callable $callable = null)
108
    {
109 96
        $relation = $this->primaryModel->getRelation($relationName);
110 96
        $callableUsed = $callable !== null;
111 96
        $this->via = [$relationName, $relation, $callableUsed];
112 96
        if ($callable !== null) {
113 63
            call_user_func($callable, $relation);
114
        }
115
116 96
        return $this;
117
    }
118
119
    /**
120
     * Sets the name of the relation that is the inverse of this relation.
121
     * For example, a customer has orders, which means the inverse of the "orders" relation is the "customer".
122
     * If this property is set, the primary record(s) will be referenced through the specified relation.
123
     * For example, `$customer->orders[0]->customer` and `$customer` will be the same object,
124
     * and accessing the customer of an order will not trigger a new DB query.
125
     *
126
     * Use this method when declaring a relation in the [[ActiveRecord]] class, e.g. in Customer model:
127
     *
128
     * ```php
129
     * public function getOrders()
130
     * {
131
     *     return $this->hasMany(Order::class, ['customer_id' => 'id'])->inverseOf('customer');
132
     * }
133
     * ```
134
     *
135
     * This also may be used for Order model, but with caution:
136
     *
137
     * ```php
138
     * public function getCustomer()
139
     * {
140
     *     return $this->hasOne(Customer::class, ['id' => 'customer_id'])->inverseOf('orders');
141
     * }
142
     * ```
143
     *
144
     * in this case result will depend on how order(s) was loaded.
145
     * Let's suppose customer has several orders. If only one order was loaded:
146
     *
147
     * ```php
148
     * $orders = Order::find()->where(['id' => 1])->all();
149
     * $customerOrders = $orders[0]->customer->orders;
150
     * ```
151
     *
152
     * variable `$customerOrders` will contain only one order. If orders was loaded like this:
153
     *
154
     * ```php
155
     * $orders = Order::find()->with('customer')->where(['customer_id' => 1])->all();
156
     * $customerOrders = $orders[0]->customer->orders;
157
     * ```
158
     *
159
     * variable `$customerOrders` will contain all orders of the customer.
160
     *
161
     * @param string $relationName the name of the relation that is the inverse of this relation.
162
     * @return $this the relation object itself.
163
     */
164 12
    public function inverseOf($relationName)
165
    {
166 12
        $this->inverseOf = $relationName;
167 12
        return $this;
168
    }
169
170
    /**
171
     * Finds the related records for the specified primary record.
172
     * This method is invoked when a relation of an ActiveRecord is being accessed lazily.
173
     * @param string $name the relation name
174
     * @param ActiveRecordInterface|BaseActiveRecord $model the primary model
175
     * @return mixed the related record(s)
176
     * @throws InvalidArgumentException if the relation is invalid
177
     */
178 88
    public function findFor($name, $model)
179
    {
180 88
        if (method_exists($model, 'get' . $name)) {
181 88
            $method = new \ReflectionMethod($model, 'get' . $name);
182 88
            $realName = lcfirst(substr($method->getName(), 3));
183 88
            if ($realName !== $name) {
184
                throw new InvalidArgumentException('Relation names are case sensitive. ' . get_class($model) . " has a relation named \"$realName\" instead of \"$name\".");
185
            }
186
        }
187
188 88
        return $this->multiple ? $this->all() : $this->one();
0 ignored issues
show
Bug introduced by
It seems like one() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

188
        return $this->multiple ? $this->all() : $this->/** @scrutinizer ignore-call */ one();
Loading history...
Bug introduced by
It seems like all() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

188
        return $this->multiple ? $this->/** @scrutinizer ignore-call */ all() : $this->one();
Loading history...
189
    }
190
191
    /**
192
     * If applicable, populate the query's primary model into the related records' inverse relationship.
193
     * @param array $result the array of related records as generated by [[populate()]]
194
     * @since 2.0.9
195
     */
196 12
    private function addInverseRelations(&$result)
197
    {
198 12
        if ($this->inverseOf === null) {
199
            return;
200
        }
201
202 12
        foreach ($result as $i => $relatedModel) {
203 12
            if ($relatedModel instanceof ActiveRecordInterface) {
204 12
                if (!isset($inverseRelation)) {
205 12
                    $inverseRelation = $relatedModel->getRelation($this->inverseOf);
206
                }
207 12
                $relatedModel->populateRelation($this->inverseOf, $inverseRelation->multiple ? [$this->primaryModel] : $this->primaryModel);
208
            } else {
209 9
                if (!isset($inverseRelation)) {
210
                    /* @var $modelClass ActiveRecordInterface */
211 9
                    $modelClass = $this->modelClass;
212 9
                    $inverseRelation = $modelClass::instance()->getRelation($this->inverseOf);
213
                }
214 9
                $result[$i][$this->inverseOf] = $inverseRelation->multiple ? [$this->primaryModel] : $this->primaryModel;
215
            }
216
        }
217
    }
218
219
    /**
220
     * Finds the related records and populates them into the primary models.
221
     * @param string $name the relation name
222
     * @param array $primaryModels primary models
223
     * @return array the related models
224
     * @throws InvalidConfigException if [[link]] is invalid
225
     */
226 132
    public function populateRelation($name, &$primaryModels)
227
    {
228 132
        if (!is_array($this->link)) {
0 ignored issues
show
introduced by
The condition is_array($this->link) is always true.
Loading history...
229
            throw new InvalidConfigException('Invalid link: it must be an array of key-value pairs.');
230
        }
231
232 132
        if ($this->via instanceof self) {
233
            // via junction table
234
            /* @var $viaQuery ActiveRelationTrait */
235 9
            $viaQuery = $this->via;
236 9
            $viaModels = $viaQuery->findJunctionRows($primaryModels);
237 9
            $this->filterByModels($viaModels);
238 132
        } elseif (is_array($this->via)) {
239
            // via relation
240
            /* @var $viaQuery ActiveRelationTrait|ActiveQueryTrait */
241 54
            list($viaName, $viaQuery) = $this->via;
242 54
            if ($viaQuery->asArray === null) {
243
                // inherit asArray from primary query
244 54
                $viaQuery->asArray($this->asArray);
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? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

244
                $viaQuery->/** @scrutinizer ignore-call */ 
245
                           asArray($this->asArray);
Loading history...
245
            }
246 54
            $viaQuery->primaryModel = null;
0 ignored issues
show
Bug Best Practice introduced by
The property primaryModel does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
247 54
            $viaModels = array_filter($viaQuery->populateRelation($viaName, $primaryModels));
0 ignored issues
show
Bug introduced by
It seems like populateRelation() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

247
            $viaModels = array_filter($viaQuery->/** @scrutinizer ignore-call */ populateRelation($viaName, $primaryModels));
Loading history...
248 54
            $this->filterByModels($viaModels);
249
        } else {
250 132
            $this->filterByModels($primaryModels);
251
        }
252
253 132
        if (!$this->multiple && count($primaryModels) === 1) {
254 39
            $model = $this->one();
255 39
            $primaryModel = reset($primaryModels);
256 39
            if ($primaryModel instanceof ActiveRecordInterface) {
257 39
                $primaryModel->populateRelation($name, $model);
258
            } else {
259 3
                $primaryModels[key($primaryModels)][$name] = $model;
260
            }
261 39
            if ($this->inverseOf !== null) {
262 6
                $this->populateInverseRelation($primaryModels, [$model], $name, $this->inverseOf);
263
            }
264
265 39
            return [$model];
266
        }
267
268
        // https://github.com/yiisoft/yii2/issues/3197
269
        // delay indexing related models after buckets are built
270 105
        $indexBy = $this->indexBy;
271 105
        $this->indexBy = null;
0 ignored issues
show
Bug Best Practice introduced by
The property indexBy does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
272 105
        $models = $this->all();
273
274 105
        if (isset($viaModels, $viaQuery)) {
275 54
            $buckets = $this->buildBuckets($models, $this->link, $viaModels, $viaQuery);
276
        } else {
277 105
            $buckets = $this->buildBuckets($models, $this->link);
278
        }
279
280 105
        $this->indexBy = $indexBy;
281 105
        if ($this->indexBy !== null && $this->multiple) {
282 15
            $buckets = $this->indexBuckets($buckets, $this->indexBy);
283
        }
284
285 105
        $link = array_values($this->link);
286 105
        if (isset($viaQuery)) {
287 54
            $deepViaQuery = $viaQuery;
288 54
            while ($deepViaQuery->via) {
289 6
                $deepViaQuery = is_array($deepViaQuery->via) ? $deepViaQuery->via[1] : $deepViaQuery->via;
290
            };
291 54
            $link = array_values($deepViaQuery->link);
292
        }
293 105
        foreach ($primaryModels as $i => $primaryModel) {
294 105
            $keys = null;
295 105
            if ($this->multiple && count($link) === 1) {
296 84
                $primaryModelKey = reset($link);
297 84
                $keys = isset($primaryModel[$primaryModelKey]) ? $primaryModel[$primaryModelKey] : null;
298
            }
299 105
            if (is_array($keys)) {
300
                $value = [];
301
                foreach ($keys as $key) {
302
                    $key = $this->normalizeModelKey($key);
303
                    if (isset($buckets[$key])) {
304
                        if ($this->indexBy !== null) {
305
                            // if indexBy is set, array_merge will cause renumbering of numeric array
306
                            foreach ($buckets[$key] as $bucketKey => $bucketValue) {
307
                                $value[$bucketKey] = $bucketValue;
308
                            }
309
                        } else {
310
                            $value = array_merge($value, $buckets[$key]);
311
                        }
312
                    }
313
                }
314
            } else {
315 105
                $key = $this->getModelKey($primaryModel, $link);
316 105
                $value = isset($buckets[$key]) ? $buckets[$key] : ($this->multiple ? [] : null);
317
            }
318 105
            if ($primaryModel instanceof ActiveRecordInterface) {
319 102
                $primaryModel->populateRelation($name, $value);
320
            } else {
321 15
                $primaryModels[$i][$name] = $value;
322
            }
323
        }
324 105
        if ($this->inverseOf !== null) {
325 6
            $this->populateInverseRelation($primaryModels, $models, $name, $this->inverseOf);
326
        }
327
328 105
        return $models;
329
    }
330
331
    /**
332
     * @param ActiveRecordInterface[] $primaryModels primary models
333
     * @param ActiveRecordInterface[] $models models
334
     * @param string $primaryName the primary relation name
335
     * @param string $name the relation name
336
     */
337 9
    private function populateInverseRelation(&$primaryModels, $models, $primaryName, $name)
338
    {
339 9
        if (empty($models) || empty($primaryModels)) {
340
            return;
341
        }
342 9
        $model = reset($models);
343
        /* @var $relation ActiveQueryInterface|ActiveQuery */
344 9
        if ($model instanceof ActiveRecordInterface) {
345 9
            $relation = $model->getRelation($name);
346
        } else {
347
            /* @var $modelClass ActiveRecordInterface */
348 6
            $modelClass = $this->modelClass;
349 6
            $relation = $modelClass::instance()->getRelation($name);
350
        }
351
352 9
        if ($relation->multiple) {
353 6
            $buckets = $this->buildBuckets($primaryModels, $relation->link, null, null, false);
354 6
            if ($model instanceof ActiveRecordInterface) {
355 6
                foreach ($models as $model) {
356 6
                    $key = $this->getModelKey($model, $relation->link);
357 6
                    $model->populateRelation($name, isset($buckets[$key]) ? $buckets[$key] : []);
358
                }
359
            } else {
360 6
                foreach ($primaryModels as $i => $primaryModel) {
361 3
                    if ($this->multiple) {
362
                        foreach ($primaryModel as $j => $m) {
363
                            $key = $this->getModelKey($m, $relation->link);
364
                            $primaryModels[$i][$j][$name] = isset($buckets[$key]) ? $buckets[$key] : [];
365
                        }
366 3
                    } elseif (!empty($primaryModel[$primaryName])) {
367 3
                        $key = $this->getModelKey($primaryModel[$primaryName], $relation->link);
368 3
                        $primaryModels[$i][$primaryName][$name] = isset($buckets[$key]) ? $buckets[$key] : [];
369
                    }
370
                }
371
            }
372 6
        } elseif ($this->multiple) {
373 6
            foreach ($primaryModels as $i => $primaryModel) {
374 6
                foreach ($primaryModel[$primaryName] as $j => $m) {
375 6
                    if ($m instanceof ActiveRecordInterface) {
376 6
                        $m->populateRelation($name, $primaryModel);
377
                    } else {
378 6
                        $primaryModels[$i][$primaryName][$j][$name] = $primaryModel;
379
                    }
380
                }
381
            }
382
        } else {
383
            foreach ($primaryModels as $i => $primaryModel) {
384
                if ($primaryModels[$i][$primaryName] instanceof ActiveRecordInterface) {
385
                    $primaryModels[$i][$primaryName]->populateRelation($name, $primaryModel);
386
                } elseif (!empty($primaryModels[$i][$primaryName])) {
387
                    $primaryModels[$i][$primaryName][$name] = $primaryModel;
388
                }
389
            }
390
        }
391
    }
392
393
    /**
394
     * @param array $models
395
     * @param array $link
396
     * @param array|null $viaModels
397
     * @param self|null $viaQuery
398
     * @param bool $checkMultiple
399
     * @return array
400
     */
401 108
    private function buildBuckets($models, $link, $viaModels = null, $viaQuery = null, $checkMultiple = true)
402
    {
403 108
        if ($viaModels !== null) {
404 54
            $map = [];
405 54
            $viaLink = $viaQuery->link;
406 54
            $viaLinkKeys = array_keys($viaLink);
407 54
            $linkValues = array_values($link);
408 54
            foreach ($viaModels as $viaModel) {
409 54
                $key1 = $this->getModelKey($viaModel, $viaLinkKeys);
410 54
                $key2 = $this->getModelKey($viaModel, $linkValues);
411 54
                $map[$key2][$key1] = true;
412
            }
413
414 54
            $viaQuery->viaMap = $map;
415
416 54
            $viaVia = $viaQuery->via;
417 54
            while ($viaVia) {
418 6
                $viaViaQuery = is_array($viaVia) ? $viaVia[1] : $viaVia;
419 6
                $map = $this->mapVia($map, $viaViaQuery->viaMap);
420
421 6
                $viaVia = $viaViaQuery->via;
422
            };
423
        }
424
425 108
        $buckets = [];
426 108
        $linkKeys = array_keys($link);
427
428 108
        if (isset($map)) {
429 54
            foreach ($models as $model) {
430 54
                $key = $this->getModelKey($model, $linkKeys);
431 54
                if (isset($map[$key])) {
432 54
                    foreach (array_keys($map[$key]) as $key2) {
433 54
                        $buckets[$key2][] = $model;
434
                    }
435
                }
436
            }
437
        } else {
438 108
            foreach ($models as $model) {
439 105
                $key = $this->getModelKey($model, $linkKeys);
440 105
                $buckets[$key][] = $model;
441
            }
442
        }
443
444 108
        if ($checkMultiple && !$this->multiple) {
445 45
            foreach ($buckets as $i => $bucket) {
446 45
                $buckets[$i] = reset($bucket);
447
            }
448
        }
449
450 108
        return $buckets;
451
    }
452
453
    /**
454
     * @param array $map
455
     * @param array $viaMap
456
     * @return array
457
     */
458 6
    private function mapVia($map, $viaMap)
459
    {
460 6
        $resultMap = [];
461 6
        foreach ($map as $key => $linkKeys) {
462 6
            $resultMap[$key] = [];
463 6
            foreach (array_keys($linkKeys) as $linkKey) {
464 6
                $resultMap[$key] += $viaMap[$linkKey];
465
            }
466
        }
467 6
        return $resultMap;
468
    }
469
470
    /**
471
     * Indexes buckets by column name.
472
     *
473
     * @param array $buckets
474
     * @param string|callable $indexBy the name of the column by which the query results should be indexed by.
475
     * This can also be a callable (e.g. anonymous function) that returns the index value based on the given row data.
476
     * @return array
477
     */
478 15
    private function indexBuckets($buckets, $indexBy)
479
    {
480 15
        $result = [];
481 15
        foreach ($buckets as $key => $models) {
482 15
            $result[$key] = [];
483 15
            foreach ($models as $model) {
484 15
                $index = is_string($indexBy) ? $model[$indexBy] : call_user_func($indexBy, $model);
485 15
                $result[$key][$index] = $model;
486
            }
487
        }
488
489 15
        return $result;
490
    }
491
492
    /**
493
     * @param array $attributes the attributes to prefix
494
     * @return array
495
     */
496 229
    private function prefixKeyColumns($attributes)
497
    {
498 229
        if ($this instanceof ActiveQuery && (!empty($this->join) || !empty($this->joinWith))) {
499 30
            if (empty($this->from)) {
500
                /* @var $modelClass ActiveRecord */
501 9
                $modelClass = $this->modelClass;
502 9
                $alias = $modelClass::tableName();
503
            } else {
504 27
                foreach ($this->from as $alias => $table) {
505 27
                    if (!is_string($alias)) {
506 27
                        $alias = $table;
507
                    }
508 27
                    break;
509
                }
510
            }
511 30
            if (isset($alias)) {
512 30
                foreach ($attributes as $i => $attribute) {
513 30
                    $attributes[$i] = "$alias.$attribute";
514
                }
515
            }
516
        }
517
518 229
        return $attributes;
519
    }
520
521
    /**
522
     * @param array $models
523
     */
524 229
    private function filterByModels($models)
525
    {
526 229
        $attributes = array_keys($this->link);
527
528 229
        $attributes = $this->prefixKeyColumns($attributes);
529
530 229
        $values = [];
531 229
        if (count($attributes) === 1) {
532
            // single key
533 223
            $attribute = reset($this->link);
534 223
            foreach ($models as $model) {
535 223
                $value = isset($model[$attribute]) || (is_object($model) && property_exists($model, $attribute)) ? $model[$attribute] : null;
536 223
                if ($value !== null) {
537 217
                    if (is_array($value)) {
538
                        $values = array_merge($values, $value);
539 217
                    } elseif ($value instanceof ArrayExpression && $value->getDimension() === 1) {
540
                        $values = array_merge($values, $value->getValue());
0 ignored issues
show
Bug introduced by
It seems like $value->getValue() can also be of type yii\db\QueryInterface; however, parameter $arrays of array_merge() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

540
                        $values = array_merge($values, /** @scrutinizer ignore-type */ $value->getValue());
Loading history...
541
                    } else {
542 217
                        $values[] = $value;
543
                    }
544
                }
545
            }
546 223
            if (empty($values)) {
547 223
                $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? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

547
                $this->/** @scrutinizer ignore-call */ 
548
                       emulateExecution();
Loading history...
548
            }
549
        } else {
550
            // composite keys
551
552
            // ensure keys of $this->link are prefixed the same way as $attributes
553 9
            $prefixedLink = array_combine($attributes, $this->link);
554 9
            foreach ($models as $model) {
555 9
                $v = [];
556 9
                foreach ($prefixedLink as $attribute => $link) {
557 9
                    $v[$attribute] = $model[$link];
558
                }
559 9
                $values[] = $v;
560 9
                if (empty($v)) {
561
                    $this->emulateExecution();
562
                }
563
            }
564
        }
565
566 229
        if (!empty($values)) {
567 223
            $scalarValues = [];
568 223
            $nonScalarValues = [];
569 223
            foreach ($values as $value) {
570 223
                if (is_scalar($value)) {
571 217
                    $scalarValues[] = $value;
572
                } else {
573 9
                    $nonScalarValues[] = $value;
574
                }
575
            }
576
577 223
            $scalarValues = array_unique($scalarValues);
578 223
            $values = array_merge($scalarValues, $nonScalarValues);
579
        }
580
581 229
        $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? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

581
        $this->/** @scrutinizer ignore-call */ 
582
               andWhere(['in', $attributes, $values]);
Loading history...
582
    }
583
584
    /**
585
     * @param ActiveRecordInterface|array $model
586
     * @param array $attributes
587
     * @return string|false
588
     */
589 108
    private function getModelKey($model, $attributes)
590
    {
591 108
        $key = [];
592 108
        foreach ($attributes as $attribute) {
593 108
            if (isset($model[$attribute]) || (is_object($model) && property_exists($model, $attribute))) {
594 105
                $key[] = $this->normalizeModelKey($model[$attribute]);
595
            }
596
        }
597 108
        if (count($key) > 1) {
598 3
            return serialize($key);
599
        }
600 105
        return reset($key);
601
    }
602
603
    /**
604
     * @param mixed $value raw key value. Since 2.0.40 non-string values must be convertible to string (like special
605
     * objects for cross-DBMS relations, for example: `|MongoId`).
606
     * @return string normalized key value.
607
     */
608 105
    private function normalizeModelKey($value)
609
    {
610
        try {
611 105
            return (string)$value;
612
        } catch (\Exception $e) {
0 ignored issues
show
Unused Code introduced by
catch (\Exception $e) is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
613
            throw new InvalidConfigException('Value must be convertable to string.');
614
        } catch (\Throwable $e) {
615
            throw new InvalidConfigException('Value must be convertable to string.');
616
        }
617
    }
618
619
    /**
620
     * @param array $primaryModels either array of AR instances or arrays
621
     * @return array
622
     */
623 21
    private function findJunctionRows($primaryModels)
624
    {
625 21
        if (empty($primaryModels)) {
626
            return [];
627
        }
628 21
        $this->filterByModels($primaryModels);
629
        /* @var $primaryModel ActiveRecord */
630 21
        $primaryModel = reset($primaryModels);
631 21
        if (!$primaryModel instanceof ActiveRecordInterface) {
0 ignored issues
show
introduced by
$primaryModel is always a sub-type of yii\db\ActiveRecordInterface.
Loading history...
632
            // when primaryModels are array of arrays (asArray case)
633
            $primaryModel = $this->modelClass;
634
        }
635
636 21
        return $this->asArray()->all($primaryModel::getDb());
637
    }
638
}
639