Passed
Pull Request — master (#224)
by Def
02:42
created

ActiveRelationTrait::populateInverseRelation()   D

Complexity

Conditions 18
Paths 29

Size

Total Lines 56
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 25.9265

Importance

Changes 0
Metric Value
cc 18
eloc 35
c 0
b 0
f 0
nc 29
nop 4
dl 0
loc 56
rs 4.8666
ccs 22
cts 31
cp 0.7097
crap 25.9265

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\ActiveRecord;
6
7
use ReflectionException;
8
use ReflectionMethod;
9
use Stringable;
10
use Throwable;
11
use Yiisoft\Db\Exception\Exception;
12
use Yiisoft\Db\Exception\InvalidArgumentException;
13
use Yiisoft\Db\Exception\InvalidConfigException;
14
use Yiisoft\Db\Exception\NotSupportedException;
15
use Yiisoft\Db\Expression\ArrayExpression;
16
17
use function array_combine;
18
use function array_keys;
19
use function array_merge;
20
use function array_unique;
21
use function array_values;
22
use function count;
23
use function is_array;
24
use function is_object;
25
use function is_scalar;
26
use function is_string;
27
use function key;
28
use function lcfirst;
29
use function method_exists;
30
use function reset;
31
use function serialize;
32
33
/**
34
 * ActiveRelationTrait implements the common methods and properties for active record relational queries.
35
 *
36
 * @method ActiveRecordInterface one()
37
 * @method ActiveRecordInterface[] all()
38
 *
39
 * @property ActiveRecord $modelClass
40
 */
41
trait ActiveRelationTrait
42
{
43
    private bool $multiple = false;
44
    private ActiveRecordInterface|null $primaryModel = null;
45
    private array $link = [];
46
    /**
47
     * @var string|null the name of the relation that is the inverse of this relation.
48
     *
49
     * For example, an order has a customer, which means the inverse of the "customer" relation is the "orders", and the
50
     * inverse of the "orders" relation is the "customer". If this property is set, the primary record(s) will be
51
     * referenced through the specified relation.
52
     *
53
     * For example, `$customer->orders[0]->customer` and `$customer` will be the same object, and accessing the customer
54
     * of an order will not trigger new DB query.
55
     *
56
     * This property is only used in relational context.
57
     *
58
     * {@see inverseOf()}
59
     */
60
    private string|null $inverseOf = null;
61
    private array|ActiveQuery|null $via = null;
62
    private array $viaMap = [];
63
64
    /**
65
     * Clones internal objects.
66
     */
67
    public function __clone()
68
    {
69
        /** make a clone of "via" object so that the same query object can be reused multiple times */
70
        if (is_object($this->via)) {
71
            $this->via = clone $this->via;
72
        } elseif (is_array($this->via)) {
73
            $this->via = [$this->via[0], clone $this->via[1], $this->via[2]];
74
        }
75
    }
76
77
    /**
78
     * Specifies the relation associated with the junction table.
79
     *
80
     * Use this method to specify a pivot record/table when declaring a relation in the {@see ActiveRecord} class:
81
     *
82
     * ```php
83
     * class Order extends ActiveRecord
84
     * {
85
     *    public function getOrderItems() {
86
     *        return $this->hasMany(OrderItem::class, ['order_id' => 'id']);
87
     *    }
88
     *
89
     *    public function getItems() {
90
     *        return $this->hasMany(Item::class, ['id' => 'item_id'])->via('orderItems');
91
     *    }
92
     * }
93
     * ```
94
     *
95
     * @param string $relationName the relation name. This refers to a relation declared in {@see primaryModel}.
96
     * @param callable|null $callable $callable a PHP callback for customizing the relation associated with the junction
97
     * table.
98
     * Its signature should be `function($query)`, where `$query` is the query to be customized.
99
     *
100
     * @return static the relation object itself.
101
     */
102 107
    public function via(string $relationName, callable $callable = null): static
103
    {
104 107
        $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

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

231
                    $inverseRelation = $this->/** @scrutinizer ignore-call */ getARInstance()->getRelation($this->inverseOf);
Loading history...
232
                }
233
234
                $result[$i][$this->inverseOf] = $inverseRelation->multiple
235
                    ? [$this->primaryModel] : $this->primaryModel;
236
            }
237
        }
238
    }
239
240
    /**
241
     * Finds the related records and populates them into the primary models.
242
     *
243
     * @param string $name the relation name
244
     * @param array $primaryModels primary models
245 139
     *
246
     * @throws InvalidArgumentException|InvalidConfigException|NotSupportedException|Throwable if {@see link()} is
247 139
     * invalid.
248
     * @throws Exception
249
     *
250
     * @return array the related models
251 139
     */
252
    public function populateRelation(string $name, array &$primaryModels): array
253
    {
254
        if ($this->via instanceof self) {
255
            $viaQuery = $this->via;
256
            $viaModels = $viaQuery->findJunctionRows($primaryModels);
257 12
            $this->filterByModels($viaModels);
258 12
        } elseif (is_array($this->via)) {
259 12
            [$viaName, $viaQuery] = $this->via;
260 139
261
            if ($viaQuery->asArray === null) {
262
                /** inherit asArray from primary query */
263
                $viaQuery->asArray($this->asArray);
264
            }
265
266 72
            $viaQuery->primaryModel = null;
267
            $viaModels = $viaQuery->populateRelation($viaName, $primaryModels);
268 72
            $this->filterByModels($viaModels);
269
        } else {
270 72
            $this->filterByModels($primaryModels);
271
        }
272
273 72
        if (!$this->multiple && count($primaryModels) === 1) {
274 72
            $model = $this->one();
275 72
            $primaryModel = reset($primaryModels);
276
277 139
            if ($primaryModel instanceof ActiveRecordInterface) {
278
                $primaryModel->populateRelation($name, $model);
279
            } else {
280 139
                $primaryModels[key($primaryModels)][$name] = $model;
281 28
            }
282 28
283
            if ($this->inverseOf !== null) {
284 28
                $this->populateInverseRelation($primaryModels, [$model], $name, $this->inverseOf);
285 28
            }
286
287 4
            return [$model];
288
        }
289
290 28
        /**
291 8
         * {@see https://github.com/yiisoft/yii2/issues/3197}
292
         *
293
         * delay indexing related models after buckets are built.
294 28
         */
295
        $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

295
        /** @scrutinizer ignore-call */ 
296
        $indexBy = $this->getIndexBy();
Loading history...
296
        $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

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

573
                $this->/** @scrutinizer ignore-call */ 
574
                       emulateExecution();
Loading history...
574
            }
575 8
        } else {
576
            /**
577 8
             * composite keys ensure keys of $this->link are prefixed the same way as $attributes.
578 8
             */
579
            $prefixedLink = array_combine($attributes, $this->link);
580 8
581 8
            foreach ($models as $model) {
582
                $v = [];
583
584 8
                foreach ($prefixedLink as $attribute => $link) {
585
                    $v[$attribute] = $model[$link];
586 8
                }
587
588
                $values[] = $v;
589
590
                if (empty($v)) {
591
                    $this->emulateExecution();
592 236
                }
593 232
            }
594 232
        }
595 232
596 232
        if (!empty($values)) {
597 228
            $scalarValues = [];
598
            $nonScalarValues = [];
599 8
            foreach ($values as $value) {
600
                if (is_scalar($value)) {
601
                    $scalarValues[] = $value;
602
                } else {
603 232
                    $nonScalarValues[] = $value;
604 232
                }
605
            }
606
607 236
            $scalarValues = array_unique($scalarValues);
608 236
            $values = [...$scalarValues, ...$nonScalarValues];
609
        }
610
611
        $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

611
        $this->/** @scrutinizer ignore-call */ 
612
               andWhere(['in', $attributes, $values]);
Loading history...
612
    }
613
614
    private function getModelKey(ActiveRecordInterface|array $activeRecord, array $attributes): false|int|string
615
    {
616 127
        $key = [];
617
618 127
        foreach ($attributes as $attribute) {
619
            if (isset($activeRecord[$attribute]) || (is_object($activeRecord) && property_exists($activeRecord, $attribute))) {
620 127
                $key[] = $this->normalizeModelKey($activeRecord[$attribute]);
621 127
            }
622
        }
623
624 127
        if (count($key) > 1) {
625
            return serialize($key);
626
        }
627
628 127
        $key = reset($key);
629
630 127
        return is_scalar($key) ? $key : serialize($key);
631
    }
632
633
    /**
634
     * @param mixed $value raw key value.
635
     *
636
     * @return int|string|null normalized key value.
637
     */
638 127
    private function normalizeModelKey(mixed $value): int|string|null
639
    {
640 127
        if ($value instanceof Stringable) {
641
            /**
642
             * ensure matching to special objects, which are convertible to string, for cross-DBMS relations,
643
             * for example: `|MongoId`
644
             */
645
            $value = (string) $value;
646
        }
647
648 127
        return $value;
649
    }
650
651
    /**
652
     * @param array $primaryModels either array of AR instances or arrays.
653
     *
654
     * @throws Exception
655
     * @throws Throwable
656 28
     * @throws \Yiisoft\Definitions\Exception\InvalidConfigException
657
     */
658 28
    private function findJunctionRows(array $primaryModels): array
659
    {
660
        if (empty($primaryModels)) {
661
            return [];
662 28
        }
663
664
        $this->filterByModels($primaryModels);
665 28
666
        /* @var $primaryModel ActiveRecord */
667 28
        $primaryModel = reset($primaryModels);
668
669
        if (!$primaryModel instanceof ActiveRecordInterface) {
0 ignored issues
show
introduced by
$primaryModel is always a sub-type of Yiisoft\ActiveRecord\ActiveRecordInterface.
Loading history...
670
            /** when primaryModels are array of arrays (asArray case) */
671
            $primaryModel = $this->arClass;
0 ignored issues
show
Unused Code introduced by
The assignment to $primaryModel is dead and can be removed.
Loading history...
672 28
        }
673
674
        return $this->asArray()->all();
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

674
        return $this->/** @scrutinizer ignore-call */ asArray()->all();
Loading history...
675
    }
676
677
    /**
678
     * @return bool whether this query represents a relation to more than one record.
679
     *
680
     * This property is only used in relational context. If true, this relation will populate all query results into
681
     * active record instances using {@see Query::all()|all()}. If false, only the first row of the results will be
682 39
     * retrieved using {@see Query::one()|one()}.
683
     */
684 39
    public function getMultiple(): bool
685
    {
686
        return $this->multiple;
687
    }
688
689
    /**
690
     * @return ActiveRecordInterface|null the primary model of a relational query.
691
     *
692 63
     * This is used only in lazy loading with dynamic query options.
693
     */
694 63
    public function getPrimaryModel(): ActiveRecordInterface|null
695
    {
696
        return $this->primaryModel;
697
    }
698
699
    /**
700
     * @return array the columns of the primary and foreign tables that establish a relation.
701
     *
702
     * The array keys must be columns of the table for this relation, and the array values must be the corresponding
703
     * columns from the primary table.
704
     *
705
     * Do not prefix or quote the column names as this will be done automatically by Yii. This property is only used in
706 113
     * relational context.
707
     */
708 113
    public function getLink(): array
709
    {
710
        return $this->link;
711
    }
712
713
    /**
714
     * @return ActiveQueryInterface|array|null the query associated with the junction table. Please call {@see (via)()} to
715
     * set this property instead of directly setting it.
716
     *
717
     * This property is only used in relational context.
718
     *
719 117
     * {@see via()}
720
     */
721 117
    public function getVia(): array|ActiveQueryInterface|null
722
    {
723
        return $this->via;
724 252
    }
725
726 252
    public function multiple(bool $value): self
727
    {
728 252
        $this->multiple = $value;
729
730
        return $this;
731 252
    }
732
733 252
    public function primaryModel(ActiveRecordInterface $value): self
734
    {
735 252
        $this->primaryModel = $value;
736
737
        return $this;
738 252
    }
739
740 252
    public function link(array $value): self
741
    {
742 252
        $this->link = $value;
743
744
        return $this;
745
    }
746
}
747