Passed
Push — php82 ( 3aff1c...33963f )
by Alexander
08:23
created

ActiveRelationTrait   F

Complexity

Total Complexity 125

Size/Duplication

Total Lines 610
Duplicated Lines 0 %

Test Coverage

Coverage 87.9%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 253
dl 0
loc 610
ccs 218
cts 248
cp 0.879
rs 2
c 1
b 0
f 0
wmc 125

15 Methods

Rating   Name   Duplication   Size   Complexity  
A __clone() 0 8 3
A findFor() 0 11 4
A via() 0 10 2
A inverseOf() 0 4 1
B addInverseRelations() 0 19 8
A indexBuckets() 0 12 4
B prefixKeyColumns() 0 23 9
F populateRelation() 0 103 28
C buildBuckets() 0 50 13
D populateInverseRelation() 0 51 21
A normalizeModelKey() 0 8 3
C filterByModels() 0 58 17
A findJunctionRows() 0 14 3
A getModelKey() 0 12 6
A mapVia() 0 9 3

How to fix   Complexity   

Complex Class

Complex classes like ActiveRelationTrait often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ActiveRelationTrait, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @link https://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license https://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|array|null one($db = null) See [[ActiveQueryInterface::one()]] for more info.
21
 * @method ActiveRecordInterface[] all($db = null) See [[ActiveQueryInterface::all()]] for more info.
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 13
    public function __clone()
71
    {
72 13
        parent::__clone();
73
        // make a clone of "via" object so that the same query object can be reused multiple times
74 13
        if (is_object($this->via)) {
75
            $this->via = clone $this->via;
76 13
        } elseif (is_array($this->via)) {
0 ignored issues
show
introduced by
The condition is_array($this->via) is always true.
Loading history...
77 6
            $this->via = [$this->via[0], clone $this->via[1], $this->via[2]];
78
        }
79 13
    }
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::class, ['order_id' => 'id']);
91
     *    }
92
     *
93
     *    public function getItems() {
94
     *        return $this->hasMany(Item::class, ['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|null $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 93
    public function via($relationName, callable $callable = null)
106
    {
107 93
        $relation = $this->primaryModel->getRelation($relationName);
108 93
        $callableUsed = $callable !== null;
109 93
        $this->via = [$relationName, $relation, $callableUsed];
110 93
        if ($callable !== null) {
111 60
            call_user_func($callable, $relation);
112
        }
113
114 93
        return $this;
115
    }
116
117
    /**
118
     * Sets the name of the relation that is the inverse of this relation.
119
     * For example, a customer has orders, which means the inverse of the "orders" relation is the "customer".
120
     * If this property is set, the primary record(s) will be referenced through the specified relation.
121
     * For example, `$customer->orders[0]->customer` and `$customer` will be the same object,
122
     * and accessing the customer of an order will not trigger a new DB query.
123
     *
124
     * Use this method when declaring a relation in the [[ActiveRecord]] class, e.g. in Customer model:
125
     *
126
     * ```php
127
     * public function getOrders()
128
     * {
129
     *     return $this->hasMany(Order::class, ['customer_id' => 'id'])->inverseOf('customer');
130
     * }
131
     * ```
132
     *
133
     * This also may be used for Order model, but with caution:
134
     *
135
     * ```php
136
     * public function getCustomer()
137
     * {
138
     *     return $this->hasOne(Customer::class, ['id' => 'customer_id'])->inverseOf('orders');
139
     * }
140
     * ```
141
     *
142
     * in this case result will depend on how order(s) was loaded.
143
     * Let's suppose customer has several orders. If only one order was loaded:
144
     *
145
     * ```php
146
     * $orders = Order::find()->where(['id' => 1])->all();
147
     * $customerOrders = $orders[0]->customer->orders;
148
     * ```
149
     *
150
     * variable `$customerOrders` will contain only one order. If orders was loaded like this:
151
     *
152
     * ```php
153
     * $orders = Order::find()->with('customer')->where(['customer_id' => 1])->all();
154
     * $customerOrders = $orders[0]->customer->orders;
155
     * ```
156
     *
157
     * variable `$customerOrders` will contain all orders of the customer.
158
     *
159
     * @param string $relationName the name of the relation that is the inverse of this relation.
160
     * @return $this the relation object itself.
161
     */
162 12
    public function inverseOf($relationName)
163
    {
164 12
        $this->inverseOf = $relationName;
165 12
        return $this;
166
    }
167
168
    /**
169
     * Finds the related records for the specified primary record.
170
     * This method is invoked when a relation of an ActiveRecord is being accessed lazily.
171
     * @param string $name the relation name
172
     * @param ActiveRecordInterface|BaseActiveRecord $model the primary model
173
     * @return mixed the related record(s)
174
     * @throws InvalidArgumentException if the relation is invalid
175
     */
176 88
    public function findFor($name, $model)
177
    {
178 88
        if (method_exists($model, 'get' . $name)) {
179 88
            $method = new \ReflectionMethod($model, 'get' . $name);
180 88
            $realName = lcfirst(substr($method->getName(), 3));
181 88
            if ($realName !== $name) {
182
                throw new InvalidArgumentException('Relation names are case sensitive. ' . get_class($model) . " has a relation named \"$realName\" instead of \"$name\".");
183
            }
184
        }
185
186 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

186
        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

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

242
                $viaQuery->/** @scrutinizer ignore-call */ 
243
                           asArray($this->asArray);
Loading history...
243
            }
244 51
            $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...
245 51
            $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

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

537
                        $values = array_merge($values, /** @scrutinizer ignore-type */ $value->getValue());
Loading history...
538
                    } else {
539 214
                        $values[] = $value;
540
                    }
541
                }
542
            }
543 220
            if (empty($values)) {
544 220
                $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

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

578
        $this->/** @scrutinizer ignore-call */ 
579
               andWhere(['in', $attributes, $values]);
Loading history...
579 226
    }
580
581
    /**
582
     * @param ActiveRecordInterface|array $model
583
     * @param array $attributes
584
     * @return string|false
585
     */
586 105
    private function getModelKey($model, $attributes)
587
    {
588 105
        $key = [];
589 105
        foreach ($attributes as $attribute) {
590 105
            if (isset($model[$attribute]) || (is_object($model) && property_exists($model, $attribute))) {
591 102
                $key[] = $this->normalizeModelKey($model[$attribute]);
592
            }
593
        }
594 105
        if (count($key) > 1) {
595 3
            return serialize($key);
596
        }
597 102
        return reset($key);
598
    }
599
600
    /**
601
     * @param mixed $value raw key value. Since 2.0.40 non-string values must be convertible to string (like special
602
     * objects for cross-DBMS relations, for example: `|MongoId`).
603
     * @return string normalized key value.
604
     */
605 102
    private function normalizeModelKey($value)
606
    {
607
        try {
608 102
            return (string)$value;
609
        } 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...
610
            throw new InvalidConfigException('Value must be convertable to string.');
611
        } catch (\Throwable $e) {
612
            throw new InvalidConfigException('Value must be convertable to string.');
613
        }
614
    }
615
616
    /**
617
     * @param array $primaryModels either array of AR instances or arrays
618
     * @return array
619
     */
620 21
    private function findJunctionRows($primaryModels)
621
    {
622 21
        if (empty($primaryModels)) {
623
            return [];
624
        }
625 21
        $this->filterByModels($primaryModels);
626
        /* @var $primaryModel ActiveRecord */
627 21
        $primaryModel = reset($primaryModels);
628 21
        if (!$primaryModel instanceof ActiveRecordInterface) {
0 ignored issues
show
introduced by
$primaryModel is always a sub-type of yii\db\ActiveRecordInterface.
Loading history...
629
            // when primaryModels are array of arrays (asArray case)
630
            $primaryModel = $this->modelClass;
631
        }
632
633 21
        return $this->asArray()->all($primaryModel::getDb());
634
    }
635
}
636