Passed
Push — master ( 364bec...91edba )
by Wilmer
02:56
created

ActiveRelationTrait   F

Complexity

Total Complexity 124

Size/Duplication

Total Lines 722
Duplicated Lines 0 %

Test Coverage

Coverage 86.36%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 258
dl 0
loc 722
ccs 228
cts 264
cp 0.8636
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 getPrimaryModel() 0 3 1
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

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 16
    public function __clone()
52
    {
53
        /** make a clone of "via" object so that the same query object can be reused multiple times */
54 16
        if (is_object($this->via)) {
55
            $this->via = clone $this->via;
56 16
        } elseif (is_array($this->via)) {
57 8
            $this->via = [$this->via[0], clone $this->via[1], $this->via[2]];
58
        }
59 16
    }
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 88
    public function via(string $relationName, callable $callable = null): self
90
    {
91 88
        $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 88
        $callableUsed = $callable !== null;
93 88
        $this->via = [$relationName, $relation, $callableUsed];
94
95 88
        if ($callable !== null) {
96 56
            $callable($relation);
97
        }
98
99 88
        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 92
    public function findFor(string $name, ActiveRecordInterface $model)
175
    {
176 92
        if (method_exists($model, 'get' . $name)) {
177 92
            $method = new ReflectionMethod($model, 'get' . $name);
178 92
            $realName = lcfirst(substr($method->getName(), 3));
179 92
            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 92
        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 99
    public function populateRelation(string $name, array &$primaryModels): array
238
    {
239 99
        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 99
        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 99
        } elseif (is_array($this->via)) {
253
            /**
254
             * via relation
255
             *
256
             * @var $viaQuery ActiveRelationTrait|ActiveQueryTrait
257
             */
258 52
            [$viaName, $viaQuery] = $this->via;
259
260 52
            if ($viaQuery->asArray === null) {
261
                /** inherit asArray from primary query */
262 52
                $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 52
            $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 52
            $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 52
            $this->filterByModels($viaModels);
268
        } else {
269 99
            $this->filterByModels($primaryModels);
270
        }
271
272 99
        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 87
        $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 87
        $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 87
        $models = $this->all();
297
298 87
        if (isset($viaModels, $viaQuery)) {
299 52
            $buckets = $this->buildBuckets($models, $this->link, $viaModels, $viaQuery);
300
        } else {
301 87
            $buckets = $this->buildBuckets($models, $this->link);
302
        }
303
304 87
        $this->indexBy($indexBy);
305
306 87
        if ($this->getIndexBy() !== null && $this->multiple) {
307 16
            $buckets = $this->indexBuckets($buckets, $this->getIndexBy());
308
        }
309
310 87
        $link = array_values($this->link);
311 87
        if (isset($viaQuery)) {
312 52
            $deepViaQuery = $viaQuery;
313
314 52
            while ($deepViaQuery->via) {
315 4
                $deepViaQuery = is_array($deepViaQuery->via) ? $deepViaQuery->via[1] : $deepViaQuery->via;
316
            }
317
318 52
            $link = array_values($deepViaQuery->link);
319
        }
320
321 87
        foreach ($primaryModels as $i => $primaryModel) {
322 87
            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 87
                $key = $this->getModelKey($primaryModel, $link);
339 87
                $value = $buckets[$key] ?? ($this->multiple ? [] : null);
340
            }
341
342 87
            if ($primaryModel instanceof ActiveRecordInterface) {
343 87
                $primaryModel->populateRelation($name, $value);
344
            } else {
345 12
                $primaryModels[$i][$name] = $value;
346
            }
347
        }
348 87
        if ($this->inverseOf !== null) {
349 8
            $this->populateInverseRelation($primaryModels, $models, $name, $this->inverseOf);
350
        }
351
352 87
        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 91
    private function buildBuckets(
417
        array $models,
418
        array $link,
419
        array $viaModels = null,
420
        ?self $viaQuery = null,
421
        bool $checkMultiple = true
422
    ): array {
423 91
        if ($viaModels !== null) {
424 52
            $map = [];
425 52
            $viaLink = $viaQuery->link;
426 52
            $viaLinkKeys = array_keys($viaLink);
427 52
            $linkValues = array_values($link);
428
429 52
            foreach ($viaModels as $viaModel) {
430 52
                $key1 = $this->getModelKey($viaModel, $viaLinkKeys);
431 52
                $key2 = $this->getModelKey($viaModel, $linkValues);
432 52
                $map[$key2][$key1] = true;
433
            }
434
435 52
            $viaQuery->viaMap = $map;
436
437 52
            $viaVia = $viaQuery->via;
438 52
            while ($viaVia) {
439 4
                $viaViaQuery = is_array($viaVia) ? $viaVia[1] : $viaVia;
440 4
                $map = $this->mapVia($map, $viaViaQuery->viaMap);
441
442 4
                $viaVia = $viaViaQuery->via;
443
            }
444
        }
445
446 91
        $buckets = [];
447 91
        $linkKeys = array_keys($link);
448
449 91
        if (isset($map)) {
450 52
            foreach ($models as $model) {
451 52
                $key = $this->getModelKey($model, $linkKeys);
452 52
                if (isset($map[$key])) {
453 52
                    foreach (array_keys($map[$key]) as $key2) {
454 52
                        $buckets[$key2][] = $model;
455
                    }
456
                }
457
            }
458
        } else {
459 91
            foreach ($models as $model) {
460 91
                $key = $this->getModelKey($model, $linkKeys);
461 91
                $buckets[$key][] = $model;
462
            }
463
        }
464
465 91
        if ($checkMultiple && !$this->multiple) {
466 31
            foreach ($buckets as $i => $bucket) {
467 31
                $buckets[$i] = reset($bucket);
468
            }
469
        }
470
471 91
        return $buckets;
472
    }
473
474 4
    private function mapVia(array $map, array $viaMap): array
475
    {
476 4
        $resultMap = [];
477 4
        foreach ($map as $key => $linkKeys) {
478 4
            foreach (array_keys($linkKeys) as $linkKey) {
479 4
                $resultMap[$key] = $viaMap[$linkKey];
480
            }
481
        }
482 4
        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 16
    private function indexBuckets(array $buckets, $indexBy): array
495
    {
496 16
        $result = [];
497
498 16
        foreach ($buckets as $key => $models) {
499 16
            $result[$key] = [];
500 16
            foreach ($models as $model) {
501 16
                $index = is_string($indexBy) ? $model[$indexBy] : $indexBy($model);
502 16
                $result[$key][$index] = $model;
503
            }
504
        }
505
506 16
        return $result;
507
    }
508
509
    /**
510
     * @param array $attributes the attributes to prefix.
511
     *
512
     * @return array
513
     */
514 195
    private function prefixKeyColumns(array $attributes): array
515
    {
516 195
        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 195
        return $attributes;
538
    }
539
540 195
    protected function filterByModels(array $models): void
541
    {
542 195
        $attributes = array_keys($this->link);
543
544 195
        $attributes = $this->prefixKeyColumns($attributes);
545
546 195
        $values = [];
547 195
        if (count($attributes) === 1) {
548
            /** single key */
549 191
            $attribute = reset($this->link);
550 191
            foreach ($models as $model) {
551 191
                if (($value = $model[$attribute]) !== null) {
552 187
                    if (is_array($value)) {
553
                        $values = array_merge($values, $value);
554 187
                    } 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 187
                        $values[] = $value;
558
                    }
559
                }
560
            }
561
562 191
            if (empty($values)) {
563 191
                $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 195
        if (!empty($values)) {
587 191
            $scalarValues = [];
588 191
            $nonScalarValues = [];
589 191
            foreach ($values as $value) {
590 191
                if (is_scalar($value)) {
591 187
                    $scalarValues[] = $value;
592
                } else {
593 8
                    $nonScalarValues[] = $value;
594
                }
595
            }
596
597 191
            $scalarValues = array_unique($scalarValues);
598 191
            $values = array_merge($scalarValues, $nonScalarValues);
599
        }
600
601 195
        $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 195
    }
603
604
    /**
605
     * @param ActiveRecordInterface|array $model
606
     * @param array $attributes
607
     *
608
     * @return int|string
609
     */
610 91
    private function getModelKey($model, array $attributes)
611
    {
612 91
        $key = [];
613
614 91
        foreach ($attributes as $attribute) {
615 91
            $key[] = $this->normalizeModelKey($model[$attribute]);
616
        }
617
618 91
        if (count($key) > 1) {
619
            return serialize($key);
620
        }
621
622 91
        $key = reset($key);
623
624 91
        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 91
    private function normalizeModelKey($value)
633
    {
634 91
        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 91
        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 40
    public function getMultiple(): bool
677
    {
678 40
        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
    public function getPrimaryModel(): ?ActiveRecordInterface
687
    {
688
        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 104
    public function getLink(): array
701
    {
702 104
        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 108
    public function getVia()
714
    {
715 108
        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 203
    public function multiple(bool $value): self
743
    {
744 203
        $this->multiple = $value;
745
746 203
        return $this;
747
    }
748
749 203
    public function primaryModel(ActiveRecordInterface $value): self
750
    {
751 203
        $this->primaryModel = $value;
752
753 203
        return $this;
754
    }
755
756 203
    public function link(array $value): self
757
    {
758 203
        $this->link = $value;
759
760 203
        return $this;
761
    }
762
}
763