Passed
Push — master ( 91edba...d01bf5 )
by Wilmer
02:58
created

ActiveRelationTrait   F

Complexity

Total Complexity 124

Size/Duplication

Total Lines 722
Duplicated Lines 0 %

Test Coverage

Coverage 87.12%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 258
dl 0
loc 722
ccs 230
cts 264
cp 0.8712
rs 2
c 1
b 0
f 0
wmc 124

24 Methods

Rating   Name   Duplication   Size   Complexity  
A getLink() 0 3 1
A inverseOf() 0 5 1
A via() 0 11 2
C filterByModels() 0 62 14
A link() 0 5 1
A getModelKey() 0 15 4
A getVia() 0 3 1
A findFor() 0 14 4
A primaryModel() 0 5 1
A mapVia() 0 9 3
A getInverseOf() 0 3 1
F populateRelation() 0 116 26
A getMultiple() 0 3 1
A findJunctionRows() 0 17 3
C buildBuckets() 0 56 13
D populateInverseRelation() 0 56 18
B prefixKeyColumns() 0 24 9
A __clone() 0 7 3
B addInverseRelations() 0 22 8
A indexBuckets() 0 13 4
A normalizeModelKey() 0 11 3
A multiple() 0 5 1
A getViaMap() 0 3 1
A getPrimaryModel() 0 3 1

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
declare(strict_types=1);
4
5
namespace Yiisoft\ActiveRecord;
6
7
use ReflectionException;
8
use ReflectionMethod;
9
use Yiisoft\Db\Exception\Exception;
10
use Yiisoft\Db\Exception\NotSupportedException;
11
use Yiisoft\Db\Expression\ArrayExpression;
12
use Yiisoft\Db\Exception\InvalidArgumentException;
13
use Yiisoft\Db\Exception\InvalidConfigException;
14
15
use function array_combine;
16
use function array_keys;
17
use function array_merge;
18
use function array_unique;
19
use function array_values;
20
use function count;
21
use function get_class;
22
use function is_array;
23
use function is_object;
24
use function is_scalar;
25
use function is_string;
26
use function lcfirst;
27
use function method_exists;
28
use function reset;
29
use function serialize;
30
31
/**
32
 * ActiveRelationTrait implements the common methods and properties for active record relational queries.
33
 *
34
 * @method ActiveRecordInterface one()
35
 * @method ActiveRecordInterface[] all()
36
 *
37
 * @property ActiveRecord $modelClass
38
 */
39
trait ActiveRelationTrait
40
{
41
    private bool $multiple = false;
42
    private ?ActiveRecordInterface $primaryModel = null;
43
    private array $link = [];
44
    private $via;
45
    private ?string $inverseOf = null;
46
    private $viaMap;
47
48
    /**
49
     * Clones internal objects.
50
     */
51 20
    public function __clone()
52
    {
53
        /** make a clone of "via" object so that the same query object can be reused multiple times */
54 20
        if (is_object($this->via)) {
55
            $this->via = clone $this->via;
56 20
        } elseif (is_array($this->via)) {
57 10
            $this->via = [$this->via[0], clone $this->via[1], $this->via[2]];
58
        }
59 20
    }
60
61
    /**
62
     * Specifies the relation associated with the junction table.
63
     *
64
     * Use this method to specify a pivot record/table when declaring a relation in the {@see ActiveRecord} class:
65
     *
66
     * ```php
67
     * class Order extends ActiveRecord
68
     * {
69
     *    public function getOrderItems() {
70
     *        return $this->hasMany(OrderItem::class, ['order_id' => 'id']);
71
     *    }
72
     *
73
     *    public function getItems() {
74
     *        return $this->hasMany(Item::class, ['id' => 'item_id'])
75
     *                    ->via('orderItems');
76
     *    }
77
     * }
78
     * ```
79
     *
80
     * @param string $relationName the relation name. This refers to a relation declared in {@see primaryModel}.
81
     * @param callable $callable a PHP callback for customizing the relation associated with the junction table.
82
     * Its signature should be `function($query)`, where `$query` is the query to be customized.
83
     *
84
     * @throws InvalidArgumentException
85
     * @throws ReflectionException
86
     *
87
     * @return self the relation object itself.
88
     */
89 105
    public function via(string $relationName, callable $callable = null): self
90
    {
91 105
        $relation = $this->primaryModel->getRelation($relationName);
0 ignored issues
show
Bug introduced by
The method getRelation() does not exist on null. ( Ignorable by Annotation )

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

91
        /** @scrutinizer ignore-call */ 
92
        $relation = $this->primaryModel->getRelation($relationName);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
92 105
        $callableUsed = $callable !== null;
93 105
        $this->via = [$relationName, $relation, $callableUsed];
94
95 105
        if ($callable !== null) {
96 66
            $callable($relation);
97
        }
98
99 105
        return $this;
100
    }
101
102
    /**
103
     * Sets the name of the relation that is the inverse of this relation.
104
     *
105
     * For example, a customer has orders, which means the inverse of the "orders" relation is the "customer".
106
     *
107
     * If this property is set, the primary record(s) will be referenced through the specified relation.
108
     *
109
     * For example, `$customer->orders[0]->customer` and `$customer` will be the same object, and accessing the customer
110
     * of an order will not trigger a new DB query.
111
     *
112
     * Use this method when declaring a relation in the {@see ActiveRecord} class, e.g. in Customer model:
113
     *
114
     * ```php
115
     * public function getOrders()
116
     * {
117
     *     return $this->hasMany(Order::class, ['customer_id' => 'id'])->inverseOf('customer');
118
     * }
119
     * ```
120
     *
121
     * This also may be used for Order model, but with caution:
122
     *
123
     * ```php
124
     * public function getCustomer()
125
     * {
126
     *     return $this->hasOne(Customer::class, ['id' => 'customer_id'])->inverseOf('orders');
127
     * }
128
     * ```
129
     *
130
     * in this case result will depend on how order(s) was loaded.
131
     * Let's suppose customer has several orders. If only one order was loaded:
132
     *
133
     * ```php
134
     * $orders = Order::find()->where(['id' => 1])->all();
135
     * $customerOrders = $orders[0]->customer->orders;
136
     * ```
137
     *
138
     * variable `$customerOrders` will contain only one order. If orders was loaded like this:
139
     *
140
     * ```php
141
     * $orders = Order::find()->with('customer')->where(['customer_id' => 1])->all();
142
     * $customerOrders = $orders[0]->customer->orders;
143
     * ```
144
     *
145
     * variable `$customerOrders` will contain all orders of the customer.
146
     *
147
     * @param string $relationName the name of the relation that is the inverse of this relation.
148
     *
149
     * @return self the relation object itself.
150
     */
151 16
    public function inverseOf(string $relationName): self
152
    {
153 16
        $this->inverseOf = $relationName;
154
155 16
        return $this;
156
    }
157
158
    /**
159
     * Finds the related records for the specified primary record.
160
     *
161
     * This method is invoked when a relation of an ActiveRecord is being accessed in a lazy fashion.
162
     *
163
     * @param string $name the relation name.
164
     * @param ActiveRecordInterface $model the primary model.
165
     *
166
     * @throws ReflectionException
167
     * @throws Exception
168
     * @throws InvalidArgumentException if the relation is invalid.
169
     * @throws InvalidConfigException
170
     * @throws NotSupportedException
171
     *
172
     * @return mixed the related record(s).
173
     */
174 104
    public function findFor(string $name, ActiveRecordInterface $model)
175
    {
176 104
        if (method_exists($model, 'get' . $name)) {
177 104
            $method = new ReflectionMethod($model, 'get' . $name);
178 104
            $realName = lcfirst(substr($method->getName(), 3));
179 104
            if ($realName !== $name) {
180
                throw new InvalidArgumentException(
181
                    'Relation names are case sensitive. ' . get_class($model)
182
                    . " has a relation named \"$realName\" instead of \"$name\"."
183
                );
184
            }
185
        }
186
187 104
        return $this->multiple ? $this->all() : $this->one();
188
    }
189
190
    /**
191
     * If applicable, populate the query's primary model into the related records' inverse relationship.
192
     *
193
     * @param array $result the array of related records as generated by {@see populate()}
194
     *
195
     * @throws InvalidArgumentException
196
     * @throws ReflectionException
197
     */
198 16
    private function addInverseRelations(array &$result): void
199
    {
200 16
        if ($this->inverseOf === null) {
201
            return;
202
        }
203
204 16
        foreach ($result as $i => $relatedModel) {
205 16
            if ($relatedModel instanceof ActiveRecordInterface) {
206 16
                if (!isset($inverseRelation)) {
207 16
                    $inverseRelation = $relatedModel->getRelation($this->inverseOf);
208
                }
209 16
                $relatedModel->populateRelation(
210 16
                    $this->inverseOf,
211 16
                    $inverseRelation->multiple ? [$this->primaryModel] : $this->primaryModel
212
                );
213
            } else {
214 12
                if (!isset($inverseRelation)) {
215 12
                    $inverseRelation = $this->modelClass::instance()->getRelation($this->inverseOf);
0 ignored issues
show
Bug introduced by
It seems like getRelation() 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

215
                    $inverseRelation = $this->modelClass::instance()->/** @scrutinizer ignore-call */ getRelation($this->inverseOf);
Loading history...
216
                }
217
218 12
                $result[$i][$this->inverseOf] = $inverseRelation->multiple
219 12
                    ? [$this->primaryModel] : $this->primaryModel;
220
            }
221
        }
222 16
    }
223
224
    /**
225
     * Finds the related records and populates them into the primary models.
226
     *
227
     * @param string $name the relation name
228
     * @param array $primaryModels primary models
229
     *
230
     * @throws Exception
231
     * @throws InvalidArgumentException
232
     * @throws InvalidConfigException if {@see link()} is invalid
233
     * @throws NotSupportedException
234
     *
235
     * @return array the related models
236
     */
237 107
    public function populateRelation(string $name, array &$primaryModels): array
238
    {
239 107
        if (!is_array($this->link)) {
0 ignored issues
show
introduced by
The condition is_array($this->link) is always true.
Loading history...
240
            throw new InvalidConfigException('Invalid link: it must be an array of key-value pairs.');
241
        }
242
243 107
        if ($this->via instanceof self) {
244
            /**
245
             * via junction table
246
             *
247
             * @var $viaQuery ActiveRelationTrait
248
             */
249 12
            $viaQuery = $this->via;
250 12
            $viaModels = $viaQuery->findJunctionRows($primaryModels);
251 12
            $this->filterByModels($viaModels);
252 107
        } elseif (is_array($this->via)) {
253
            /**
254
             * via relation
255
             *
256
             * @var $viaQuery ActiveRelationTrait|ActiveQueryTrait
257
             */
258 60
            [$viaName, $viaQuery] = $this->via;
259
260 60
            if ($viaQuery->asArray === null) {
261
                /** inherit asArray from primary query */
262 60
                $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

262
                $viaQuery->/** @scrutinizer ignore-call */ 
263
                           asArray($this->asArray);
Loading history...
263
            }
264
265 60
            $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...
266 60
            $viaModels = $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

266
            /** @scrutinizer ignore-call */ 
267
            $viaModels = $viaQuery->populateRelation($viaName, $primaryModels);
Loading history...
267 60
            $this->filterByModels($viaModels);
268
        } else {
269 107
            $this->filterByModels($primaryModels);
270
        }
271
272 107
        if (!$this->multiple && count($primaryModels) === 1) {
273 28
            $model = $this->one();
274 28
            $primaryModel = reset($primaryModels);
275
276 28
            if ($primaryModel instanceof ActiveRecordInterface) {
277 28
                $primaryModel->populateRelation($name, $model);
278
            } else {
279 4
                $primaryModels[\key($primaryModels)][$name] = $model;
280
            }
281
282 28
            if ($this->inverseOf !== null) {
283 8
                $this->populateInverseRelation($primaryModels, [$model], $name, $this->inverseOf);
284
            }
285
286 28
            return [$model];
287
        }
288
289
        /**
290
         * {@see https://github.com/yiisoft/yii2/issues/3197}
291
         *
292
         * delay indexing related models after buckets are built.
293
         */
294 95
        $indexBy = $this->getIndexBy();
0 ignored issues
show
Bug introduced by
It seems like getIndexBy() 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

294
        /** @scrutinizer ignore-call */ 
295
        $indexBy = $this->getIndexBy();
Loading history...
295 95
        $this->indexBy(null);
0 ignored issues
show
Bug introduced by
It seems like indexBy() 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

295
        $this->/** @scrutinizer ignore-call */ 
296
               indexBy(null);
Loading history...
296 95
        $models = $this->all();
297
298 95
        if (isset($viaModels, $viaQuery)) {
299 60
            $buckets = $this->buildBuckets($models, $this->link, $viaModels, $viaQuery);
300
        } else {
301 95
            $buckets = $this->buildBuckets($models, $this->link);
302
        }
303
304 95
        $this->indexBy($indexBy);
305
306 95
        if ($this->getIndexBy() !== null && $this->multiple) {
307 17
            $buckets = $this->indexBuckets($buckets, $this->getIndexBy());
308
        }
309
310 95
        $link = array_values($this->link);
311 95
        if (isset($viaQuery)) {
312 60
            $deepViaQuery = $viaQuery;
313
314 60
            while ($deepViaQuery->via) {
315 5
                $deepViaQuery = is_array($deepViaQuery->via) ? $deepViaQuery->via[1] : $deepViaQuery->via;
316
            }
317
318 60
            $link = array_values($deepViaQuery->link);
319
        }
320
321 95
        foreach ($primaryModels as $i => $primaryModel) {
322 95
            if ($this->multiple && count($link) === 1 && is_array($keys = $primaryModel[reset($link)])) {
323
                $value = [];
324
                foreach ($keys as $key) {
325
                    $key = $this->normalizeModelKey($key);
326
                    if (isset($buckets[$key])) {
327
                        if ($this->getIndexBy() !== null) {
328
                            /** if indexBy is set, array_merge will cause renumbering of numeric array */
329
                            foreach ($buckets[$key] as $bucketKey => $bucketValue) {
330
                                $value[$bucketKey] = $bucketValue;
331
                            }
332
                        } else {
333
                            $value = array_merge($value, $buckets[$key]);
334
                        }
335
                    }
336
                }
337
            } else {
338 95
                $key = $this->getModelKey($primaryModel, $link);
339 95
                $value = $buckets[$key] ?? ($this->multiple ? [] : null);
340
            }
341
342 95
            if ($primaryModel instanceof ActiveRecordInterface) {
343 95
                $primaryModel->populateRelation($name, $value);
344
            } else {
345 13
                $primaryModels[$i][$name] = $value;
346
            }
347
        }
348 95
        if ($this->inverseOf !== null) {
349 8
            $this->populateInverseRelation($primaryModels, $models, $name, $this->inverseOf);
350
        }
351
352 95
        return $models;
353
    }
354
355 12
    private function populateInverseRelation(
356
        array &$primaryModels,
357
        array $models,
358
        string $primaryName,
359
        string $name
360
    ): void {
361 12
        if (empty($models) || empty($primaryModels)) {
362
            return;
363
        }
364 12
        $model = reset($models);
365
366
        /** @var $relation ActiveQueryInterface|ActiveQuery */
367 12
        if ($model instanceof ActiveRecordInterface) {
368 12
            $relation = $model->getRelation($name);
369
        } else {
370
            /** @var $modelClass ActiveRecordInterface */
371 8
            $modelClass = $this->modelClass;
372 8
            $relation = $modelClass::instance()->getRelation($name);
373
        }
374
375 12
        if ($relation->multiple) {
376 8
            $buckets = $this->buildBuckets($primaryModels, $relation->link, null, null, false);
377 8
            if ($model instanceof ActiveRecordInterface) {
378 8
                foreach ($models as $model) {
379 8
                    $key = $this->getModelKey($model, $relation->link);
380 8
                    $model->populateRelation($name, $buckets[$key] ?? []);
381
                }
382
            } else {
383 8
                foreach ($primaryModels as $i => $primaryModel) {
384 4
                    if ($this->multiple) {
385
                        foreach ($primaryModel as $j => $m) {
386
                            $key = $this->getModelKey($m, $relation->link);
387
                            $primaryModels[$i][$j][$name] = $buckets[$key] ?? [];
388
                        }
389 4
                    } elseif (!empty($primaryModel[$primaryName])) {
390 4
                        $key = $this->getModelKey($primaryModel[$primaryName], $relation->link);
391 4
                        $primaryModels[$i][$primaryName][$name] = $buckets[$key] ?? [];
392
                    }
393
                }
394
            }
395 8
        } elseif ($this->multiple) {
396 8
            foreach ($primaryModels as $i => $primaryModel) {
397 8
                foreach ($primaryModel[$primaryName] as $j => $m) {
398 8
                    if ($m instanceof ActiveRecordInterface) {
399 8
                        $m->populateRelation($name, $primaryModel);
400
                    } else {
401 8
                        $primaryModels[$i][$primaryName][$j][$name] = $primaryModel;
402
                    }
403
                }
404
            }
405
        } else {
406
            foreach ($primaryModels as $i => $primaryModel) {
407
                if ($primaryModels[$i][$primaryName] instanceof ActiveRecordInterface) {
408
                    $primaryModels[$i][$primaryName]->populateRelation($name, $primaryModel);
409
                } elseif (!empty($primaryModels[$i][$primaryName])) {
410
                    $primaryModels[$i][$primaryName][$name] = $primaryModel;
411
                }
412
            }
413
        }
414 12
    }
415
416 99
    private function buildBuckets(
417
        array $models,
418
        array $link,
419
        array $viaModels = null,
420
        ?self $viaQuery = null,
421
        bool $checkMultiple = true
422
    ): array {
423 99
        if ($viaModels !== null) {
424 60
            $map = [];
425 60
            $viaLink = $viaQuery->link;
426 60
            $viaLinkKeys = array_keys($viaLink);
427 60
            $linkValues = array_values($link);
428
429 60
            foreach ($viaModels as $viaModel) {
430 59
                $key1 = $this->getModelKey($viaModel, $viaLinkKeys);
431 59
                $key2 = $this->getModelKey($viaModel, $linkValues);
432 59
                $map[$key2][$key1] = true;
433
            }
434
435 60
            $viaQuery->viaMap = $map;
436
437 60
            $viaVia = $viaQuery->via;
438 60
            while ($viaVia) {
439 5
                $viaViaQuery = is_array($viaVia) ? $viaVia[1] : $viaVia;
440 5
                $map = $this->mapVia($map, $viaViaQuery->viaMap);
441
442 5
                $viaVia = $viaViaQuery->via;
443
            }
444
        }
445
446 99
        $buckets = [];
447 99
        $linkKeys = array_keys($link);
448
449 99
        if (isset($map)) {
450 60
            foreach ($models as $model) {
451 59
                $key = $this->getModelKey($model, $linkKeys);
452 59
                if (isset($map[$key])) {
453 59
                    foreach (array_keys($map[$key]) as $key2) {
454 59
                        $buckets[$key2][] = $model;
455
                    }
456
                }
457
            }
458
        } else {
459 99
            foreach ($models as $model) {
460 99
                $key = $this->getModelKey($model, $linkKeys);
461 99
                $buckets[$key][] = $model;
462
            }
463
        }
464
465 99
        if ($checkMultiple && !$this->multiple) {
466 32
            foreach ($buckets as $i => $bucket) {
467 32
                $buckets[$i] = reset($bucket);
468
            }
469
        }
470
471 99
        return $buckets;
472
    }
473
474 5
    private function mapVia(array $map, array $viaMap): array
475
    {
476 5
        $resultMap = [];
477 5
        foreach ($map as $key => $linkKeys) {
478 5
            foreach (array_keys($linkKeys) as $linkKey) {
479 5
                $resultMap[$key] = $viaMap[$linkKey];
480
            }
481
        }
482 5
        return $resultMap;
483
    }
484
485
    /**
486
     * Indexes buckets by column name.
487
     *
488
     * @param array $buckets
489
     * @param string|callable $indexBy the name of the column by which the query results should be indexed by. This can
490
     * also be a callable (e.g. anonymous function) that returns the index value based on the given row data.
491
     *
492
     * @return array
493
     */
494 17
    private function indexBuckets(array $buckets, $indexBy): array
495
    {
496 17
        $result = [];
497
498 17
        foreach ($buckets as $key => $models) {
499 17
            $result[$key] = [];
500 17
            foreach ($models as $model) {
501 17
                $index = is_string($indexBy) ? $model[$indexBy] : $indexBy($model);
502 17
                $result[$key][$index] = $model;
503
            }
504
        }
505
506 17
        return $result;
507
    }
508
509
    /**
510
     * @param array $attributes the attributes to prefix.
511
     *
512
     * @return array
513
     */
514 218
    private function prefixKeyColumns(array $attributes): array
515
    {
516 218
        if ($this instanceof ActiveQuery && (!empty($this->join) || !empty($this->joinWith))) {
0 ignored issues
show
Bug introduced by
The property joinWith is declared private in Yiisoft\ActiveRecord\ActiveQuery and cannot be accessed from this context.
Loading history...
Bug introduced by
The property join is declared protected in Yiisoft\Db\Query\Query and cannot be accessed from this context.
Loading history...
517 36
            if (empty($this->from)) {
0 ignored issues
show
Bug introduced by
The property from is declared protected in Yiisoft\Db\Query\Query and cannot be accessed from this context.
Loading history...
518
                /** @var $modelClass ActiveRecord */
519 8
                $modelClass = $this->modelClass;
0 ignored issues
show
Bug introduced by
The property modelClass is declared protected in Yiisoft\ActiveRecord\ActiveQuery and cannot be accessed from this context.
Loading history...
520 8
                $alias = $modelClass::tableName();
521
            } else {
522 36
                foreach ($this->from as $alias => $table) {
523 36
                    if (!is_string($alias)) {
524 36
                        $alias = $table;
525
                    }
526 36
                    break;
527
                }
528
            }
529
530 36
            if (isset($alias)) {
531 36
                foreach ($attributes as $i => $attribute) {
532 36
                    $attributes[$i] = "$alias.$attribute";
533
                }
534
            }
535
        }
536
537 218
        return $attributes;
538
    }
539
540 218
    protected function filterByModels(array $models): void
541
    {
542 218
        $attributes = array_keys($this->link);
543
544 218
        $attributes = $this->prefixKeyColumns($attributes);
545
546 218
        $values = [];
547 218
        if (count($attributes) === 1) {
548
            /** single key */
549 214
            $attribute = reset($this->link);
550 214
            foreach ($models as $model) {
551 214
                if (($value = $model[$attribute]) !== null) {
552 210
                    if (is_array($value)) {
553
                        $values = array_merge($values, $value);
554 210
                    } elseif ($value instanceof ArrayExpression && $value->getDimension() === 1) {
555
                        $values = array_merge($values, $value->getValue());
0 ignored issues
show
Bug introduced by
It seems like $value->getValue() can also be of type Yiisoft\Db\Query\QueryInterface; however, parameter $array2 of array_merge() does only seem to accept array|null, 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

555
                        $values = array_merge($values, /** @scrutinizer ignore-type */ $value->getValue());
Loading history...
556
                    } else {
557 210
                        $values[] = $value;
558
                    }
559
                }
560
            }
561
562 214
            if (empty($values)) {
563 214
                $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

563
                $this->/** @scrutinizer ignore-call */ 
564
                       emulateExecution();
Loading history...
564
            }
565
        } else {
566
            /**
567
             * composite keys ensure keys of $this->link are prefixed the same way as $attributes.
568
             */
569 8
            $prefixedLink = array_combine($attributes, $this->link);
570
571 8
            foreach ($models as $model) {
572 8
                $v = [];
573
574 8
                foreach ($prefixedLink as $attribute => $link) {
575 8
                    $v[$attribute] = $model[$link];
576
                }
577
578 8
                $values[] = $v;
579
580 8
                if (empty($v)) {
581
                    $this->emulateExecution();
582
                }
583
            }
584
        }
585
586 218
        if (!empty($values)) {
587 214
            $scalarValues = [];
588 214
            $nonScalarValues = [];
589 214
            foreach ($values as $value) {
590 214
                if (is_scalar($value)) {
591 210
                    $scalarValues[] = $value;
592
                } else {
593 8
                    $nonScalarValues[] = $value;
594
                }
595
            }
596
597 214
            $scalarValues = array_unique($scalarValues);
598 214
            $values = array_merge($scalarValues, $nonScalarValues);
599
        }
600
601 218
        $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

601
        $this->/** @scrutinizer ignore-call */ 
602
               andWhere(['in', $attributes, $values]);
Loading history...
602 218
    }
603
604
    /**
605
     * @param ActiveRecordInterface|array $model
606
     * @param array $attributes
607
     *
608
     * @return int|string
609
     */
610 99
    private function getModelKey($model, array $attributes)
611
    {
612 99
        $key = [];
613
614 99
        foreach ($attributes as $attribute) {
615 99
            $key[] = $this->normalizeModelKey($model[$attribute]);
616
        }
617
618 99
        if (count($key) > 1) {
619
            return serialize($key);
620
        }
621
622 99
        $key = reset($key);
623
624 99
        return is_scalar($key) ? $key : serialize($key);
625
    }
626
627
    /**
628
     * @param mixed $value raw key value.
629
     *
630
     * @return int|string normalized key value.
631
     */
632 99
    private function normalizeModelKey($value)
633
    {
634 99
        if (is_object($value) && method_exists($value, '__toString')) {
635
            /**
636
             * ensure matching to special objects, which are convertible to string, for cross-DBMS relations,
637
             * for example: `|MongoId`
638
             */
639
            $value = $value->__toString();
640
        }
641
642 99
        return $value;
643
    }
644
645
    /**
646
     * @param array $primaryModels either array of AR instances or arrays.
647
     *
648
     * @return array
649
     */
650 28
    private function findJunctionRows(array $primaryModels): array
651
    {
652 28
        if (empty($primaryModels)) {
653
            return [];
654
        }
655
656 28
        $this->filterByModels($primaryModels);
657
658
        /* @var $primaryModel ActiveRecord */
659 28
        $primaryModel = reset($primaryModels);
660
661 28
        if (!$primaryModel instanceof ActiveRecordInterface) {
0 ignored issues
show
introduced by
$primaryModel is always a sub-type of Yiisoft\ActiveRecord\ActiveRecordInterface.
Loading history...
662
            /** when primaryModels are array of arrays (asArray case) */
663
            $primaryModel = $this->modelClass;
0 ignored issues
show
Unused Code introduced by
The assignment to $primaryModel is dead and can be removed.
Loading history...
664
        }
665
666 28
        return $this->asArray()->all();
667
    }
668
669
    /**
670
     * @return bool whether this query represents a relation to more than one record.
671
     *
672
     * This property is only used in relational context. If true, this relation will populate all query results into AR
673
     * instances using {@see Query::all()|all()}. If false, only the first row of the results will be retrieved using
674
     * {@see Query::one()|one()}.
675
     */
676 49
    public function getMultiple(): bool
677
    {
678 49
        return $this->multiple;
679
    }
680
681
    /**
682
     * @return ActiveRecordInterface the primary model of a relational query.
683
     *
684
     * This is used only in lazy loading with dynamic query options.
685
     */
686 68
    public function getPrimaryModel(): ?ActiveRecordInterface
687
    {
688 68
        return $this->primaryModel;
689
    }
690
691
    /**
692
     * @return array the columns of the primary and foreign tables that establish a relation.
693
     *
694
     * The array keys must be columns of the table for this relation, and the array values must be the corresponding
695
     * columns from the primary table.
696
     *
697
     * Do not prefix or quote the column names as this will be done automatically by Yii. This property is only used in
698
     * relational context.
699
     */
700 116
    public function getLink(): array
701
    {
702 116
        return $this->link;
703
    }
704
705
    /**
706
     * @return array|object the query associated with the junction table. Please call {@see via()} to set this property
707
     * instead of directly setting it.
708
     *
709
     * This property is only used in relational context.
710
     *
711
     * {@see via()}
712
     */
713 123
    public function getVia()
714
    {
715 123
        return $this->via;
716
    }
717
718
    /**
719
     * @return string the name of the relation that is the inverse of this relation.
720
     *
721
     * For example, an order has a customer, which means the inverse of the "customer" relation is the "orders", and the
722
     * inverse of the "orders" relation is the "customer". If this property is set, the primary record(s) will be
723
     * referenced through the specified relation.
724
     *
725
     * For example, `$customer->orders[0]->customer` and `$customer` will be the same object, and accessing the customer
726
     * of an order will not trigger new DB query.
727
     *
728
     * This property is only used in relational context.
729
     *
730
     * {@see inverseOf()}
731
     */
732
    public function getInverseOf(): string
733
    {
734
        return $this->inverseOf;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->inverseOf could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
735
    }
736
737
    public function getViaMap()
738
    {
739
        return $this->viaMap;
740
    }
741
742 226
    public function multiple(bool $value): self
743
    {
744 226
        $this->multiple = $value;
745
746 226
        return $this;
747
    }
748
749 226
    public function primaryModel(ActiveRecordInterface $value): self
750
    {
751 226
        $this->primaryModel = $value;
752
753 226
        return $this;
754
    }
755
756 226
    public function link(array $value): self
757
    {
758 226
        $this->link = $value;
759
760 226
        return $this;
761
    }
762
}
763