Passed
Pull Request — master (#354)
by Sergei
03:30
created

ActiveRelationTrait::populateRelationFromBuckets()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 4

Importance

Changes 0
Metric Value
eloc 10
c 0
b 0
f 0
dl 0
loc 17
ccs 11
cts 11
cp 1
rs 9.9332
cc 4
nc 4
nop 4
crap 4
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\ActiveRecord;
6
7
use Closure;
8
use ReflectionException;
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
16
use function array_combine;
17
use function array_diff_key;
18
use function array_fill_keys;
19
use function array_filter;
20
use function array_intersect_key;
21
use function array_keys;
22
use function array_merge;
23
use function array_unique;
24
use function array_values;
25
use function count;
26
use function is_array;
27
use function is_object;
28
use function is_scalar;
29
use function is_string;
30
use function key;
31
use function reset;
32
use function serialize;
33
34
/**
35
 * ActiveRelationTrait implements the common methods and properties for active record relational queries.
36
 */
37
trait ActiveRelationTrait
38
{
39
    private bool $multiple = false;
40
    private ActiveRecordInterface|null $primaryModel = null;
41
    /** @psalm-var string[] */
42
    private array $link = [];
43
    /**
44
     * @var string|null the name of the relation that is the inverse of this relation.
45
     *
46
     * For example, an order has a customer, which means the inverse of the "customer" relation is the "orders", and the
47
     * inverse of the "orders" relation is the "customer". If this property is set, the primary record(s) will be
48
     * referenced through the specified relation.
49
     *
50
     * For example, `$customer->orders[0]->customer` and `$customer` will be the same object, and accessing the customer
51
     * of an order will not trigger new DB query.
52
     *
53
     * This property is only used in relational context.
54
     *
55
     * {@see inverseOf()}
56
     */
57
    private string|null $inverseOf = null;
58
    private array|ActiveQuery|null $via = null;
59
    private array $viaMap = [];
60
61
    /**
62
     * Clones internal objects.
63
     */
64
    public function __clone()
65
    {
66
        /** make a clone of "via" object so that the same query object can be reused multiple times */
67
        if (is_object($this->via)) {
68
            $this->via = clone $this->via;
69
        } elseif (is_array($this->via)) {
70
            $this->via = [$this->via[0], clone $this->via[1], $this->via[2]];
71
        }
72
    }
73
74
    /**
75
     * Specifies the relation associated with the junction table.
76
     *
77
     * Use this method to specify a pivot record/table when declaring a relation in the {@see ActiveRecord} class:
78
     *
79
     * ```php
80
     * class Order extends ActiveRecord
81
     * {
82
     *    public function getOrderItems() {
83
     *        return $this->hasMany(OrderItem::class, ['order_id' => 'id']);
84
     *    }
85
     *
86
     *    public function getItems() {
87
     *        return $this->hasMany(Item::class, ['id' => 'item_id'])->via('orderItems');
88
     *    }
89
     * }
90
     * ```
91
     *
92
     * @param string $relationName the relation name. This refers to a relation declared in {@see primaryModel}.
93
     * @param callable|null $callable a PHP callback for customizing the relation associated with the junction table.
94
     * Its signature should be `function($query)`, where `$query` is the query to be customized.
95
     *
96
     * @return static the relation object itself.
97
     */
98
    public function via(string $relationName, callable $callable = null): static
99
    {
100
        $relation = $this->primaryModel?->relationQuery($relationName);
101
        $callableUsed = $callable !== null;
102 107
        $this->via = [$relationName, $relation, $callableUsed];
103
104 107
        if ($callableUsed) {
105 107
            $callable($relation);
106 107
        }
107
108 107
        return $this;
109 73
    }
110
111
    /**
112 107
     * Sets the name of the relation that is the inverse of this relation.
113
     *
114
     * For example, a customer has orders, which means the inverse of the "orders" relation is the "customer".
115
     *
116
     * If this property is set, the primary record(s) will be referenced through the specified relation.
117
     *
118
     * For example, `$customer->orders[0]->customer` and `$customer` will be the same object, and accessing the customer
119
     * of an order will not trigger a new DB query.
120
     *
121
     * Use this method when declaring a relation in the {@see ActiveRecord} class, e.g. in Customer model:
122
     *
123
     * ```php
124
     * public function getOrders()
125
     * {
126
     *     return $this->hasMany(Order::class, ['customer_id' => 'id'])->inverseOf('customer');
127
     * }
128
     * ```
129
     *
130
     * This also may be used for Order model, but with caution:
131
     *
132
     * ```php
133
     * public function getCustomer()
134
     * {
135
     *     return $this->hasOne(Customer::class, ['id' => 'customer_id'])->inverseOf('orders');
136
     * }
137
     * ```
138
     *
139
     * in this case result will depend on how order(s) was loaded.
140
     * Let's suppose customer has several orders. If only one order was loaded:
141
     *
142
     * ```php
143
     * $orderQuery = new ActiveQuery(Order::class, $db);
144
     * $orders = $orderQuery->where(['id' => 1])->all();
145
     * $customerOrders = $orders[0]->customer->orders;
146
     * ```
147
     *
148
     * variable `$customerOrders` will contain only one order. If orders was loaded like this:
149
     *
150
     * ```php
151
     * $orderQuery = new ActiveQuery(Order::class, $db);
152
     * $orders = $orderQuery->with('customer')->where(['customer_id' => 1])->all();
153
     * $customerOrders = $orders[0]->customer->orders;
154
     * ```
155
     *
156
     * variable `$customerOrders` will contain all orders of the customer.
157
     *
158
     * @param string $relationName the name of the relation that is the inverse of this relation.
159
     *
160
     * @return static the relation object itself.
161
     */
162
    public function inverseOf(string $relationName): static
163
    {
164
        $this->inverseOf = $relationName;
165
166 16
        return $this;
167
    }
168 16
169
    /**
170 16
     * Returns query records depends on {@see $multiple} .
171
     *
172
     * This method is invoked when a relation of an ActiveRecord is being accessed in a lazy fashion.
173
     *
174
     * @throws Exception
175
     * @throws InvalidArgumentException
176
     * @throws InvalidConfigException
177
     * @throws ReflectionException
178
     * @throws Throwable if the relation is invalid.
179
     *
180
     * @return ActiveRecordInterface|array|null the related record(s).
181
     */
182
    public function relatedRecords(): ActiveRecordInterface|array|null
183
    {
184
        return $this->multiple ? $this->all() : $this->onePopulate();
0 ignored issues
show
Bug introduced by
It seems like all() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

184
        return $this->multiple ? $this->/** @scrutinizer ignore-call */ all() : $this->onePopulate();
Loading history...
Bug introduced by
It seems like onePopulate() 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

184
        return $this->multiple ? $this->all() : $this->/** @scrutinizer ignore-call */ onePopulate();
Loading history...
185
    }
186 101
187
    /**
188 101
     * If applicable, populate the query's primary model into the related records' inverse relationship.
189 101
     *
190 101
     * @param array $result the array of related records as generated by {@see populate()}
191 101
     *
192
     * @throws \Yiisoft\Definitions\Exception\InvalidConfigException
193
     */
194
    private function addInverseRelations(array &$result): void
195
    {
196
        if ($this->inverseOf === null) {
197
            return;
198
        }
199 101
200
        $relatedModel = reset($result);
201
202
        if ($relatedModel instanceof ActiveRecordInterface) {
203
            $inverseRelation = $relatedModel->relationQuery($this->inverseOf);
204
            $primaryModel = $inverseRelation->getMultiple() ? [$this->primaryModel] : $this->primaryModel;
205
206
            foreach ($result as $relatedModel) {
207 16
                $relatedModel->populateRelation($this->inverseOf, $primaryModel);
208
            }
209 16
        } else {
210
            $inverseRelation = $this->getARInstance()->relationQuery($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

210
            $inverseRelation = $this->/** @scrutinizer ignore-call */ getARInstance()->relationQuery($this->inverseOf);
Loading history...
211
            $primaryModel = $inverseRelation->getMultiple() ? [$this->primaryModel] : $this->primaryModel;
212
213 16
            foreach ($result as &$relatedModel) {
214 16
                $relatedModel[$this->inverseOf] = $primaryModel;
215 16
            }
216 16
        }
217
    }
218 16
219 16
    /**
220 16
     * Finds the related records and populates them into the primary models.
221
     *
222
     * @param string $name the relation name
223 8
     * @param array $primaryModels primary models
224 8
     *
225
     * @throws InvalidArgumentException|InvalidConfigException|NotSupportedException|Throwable if {@see link()} is
226
     * invalid.
227 8
     * @throws Exception
228 8
     *
229
     * @return array the related models
230
     */
231 16
    public function populateRelation(string $name, array &$primaryModels): array
232
    {
233
        if ($this->via instanceof self) {
234
            $viaQuery = $this->via;
235
            $viaModels = $viaQuery->findJunctionRows($primaryModels);
236
            $this->filterByModels($viaModels);
237
        } elseif (is_array($this->via)) {
238
            [$viaName, $viaQuery] = $this->via;
239
240
            if ($viaQuery->asArray === null) {
241
                /** inherit asArray from primary query */
242
                $viaQuery->asArray($this->asArray);
243
            }
244
245 139
            $viaQuery->primaryModel = null;
246
            $viaModels = $viaQuery->populateRelation($viaName, $primaryModels);
247 139
            $this->filterByModels($viaModels);
248
        } else {
249
            $this->filterByModels($primaryModels);
250
        }
251 139
252
        if (!$this->multiple && count($primaryModels) === 1) {
253
            $model = $this->onePopulate();
254
            $primaryModel = reset($primaryModels);
255
256
            if ($primaryModel instanceof ActiveRecordInterface) {
257 12
                $primaryModel->populateRelation($name, $model);
258 12
            } else {
259 12
                $primaryModels[key($primaryModels)][$name] = $model;
260 139
            }
261
262
            if ($this->inverseOf !== null) {
263
                $this->populateInverseRelation($primaryModels, [$model], $name, $this->inverseOf);
264
            }
265
266 72
            return [$model];
267
        }
268 72
269
        /**
270 72
         * {@see https://github.com/yiisoft/yii2/issues/3197}
271
         *
272
         * delay indexing related models after buckets are built.
273 72
         */
274 72
        $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

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

275
        $this->/** @scrutinizer ignore-call */ 
276
               indexBy(null);
Loading history...
276
        $models = $this->all();
277 139
278
        if (isset($viaModels, $viaQuery)) {
279
            $buckets = $this->buildBuckets($models, $this->link, $viaModels, $viaQuery);
280 139
        } else {
281 28
            $buckets = $this->buildBuckets($models, $this->link);
282 28
        }
283
284 28
        $this->indexBy($indexBy);
285 28
286
        if ($indexBy !== null && $this->multiple) {
287 4
            $buckets = $this->indexBuckets($buckets, $indexBy);
288
        }
289
290 28
        if (isset($viaQuery)) {
291 8
            $deepViaQuery = $viaQuery;
292
293
            while ($deepViaQuery->via) {
294 28
                $deepViaQuery = is_array($deepViaQuery->via) ? $deepViaQuery->via[1] : $deepViaQuery->via;
295
            }
296
297
            $link = $deepViaQuery->link;
298
        } else {
299
            $link = $this->link;
300
        }
301
302 123
        foreach ($primaryModels as $i => $primaryModel) {
303 123
            $keys = null;
304 123
305
            if ($this->multiple && count($link) === 1) {
306 123
                $primaryModelKey = reset($link);
307 72
308
                if ($primaryModel instanceof ActiveRecordInterface) {
309 123
                    $keys = $primaryModel->getAttribute($primaryModelKey);
310
                } else {
311
                    $keys = $primaryModel[$primaryModelKey] ?? null;
312 123
                }
313
            }
314 123
315 17
            if (is_array($keys)) {
316
                $value = [];
317
318 123
                foreach ($keys as $key) {
319 123
                    $key = $this->normalizeModelKey($key);
320 72
                    if (isset($buckets[$key])) {
321
                        $value[] = $buckets[$key];
322 72
                    }
323 5
                }
324
325
                if ($indexBy !== null) {
326 72
                    /** if indexBy is set, array_merge will cause renumbering of numeric array */
327
                    $value = array_replace(...$value);
328
                } else {
329 123
                    $value = array_merge(...$value);
330 123
                }
331
            } else {
332
                $key = $this->getModelKey($primaryModel, $link);
333
                $value = $buckets[$key] ?? ($this->multiple ? [] : null);
334
            }
335
336
            if ($primaryModel instanceof ActiveRecordInterface) {
337
                $primaryModel->populateRelation($name, $value);
338
            } else {
339
                $primaryModels[$i][$name] = $value;
340
            }
341
        }
342
343
        if ($this->inverseOf !== null) {
344
            $this->populateInverseRelation($primaryModels, $models, $name, $this->inverseOf);
345
        }
346 123
347 123
        return $models;
348
    }
349
350 123
    /**
351 123
     * @throws \Yiisoft\Definitions\Exception\InvalidConfigException
352
     */
353 9
    private function populateInverseRelation(
354
        array &$primaryModels,
355
        array $models,
356 123
        string $primaryName,
357 8
        string $name
358
    ): void {
359
        if (empty($models) || empty($primaryModels)) {
360 123
            return;
361
        }
362
363 12
        $model = reset($models);
364
365
        if ($model instanceof ActiveRecordInterface) {
366
            /** @var ActiveQuery $relation */
367
            $relation = $model->relationQuery($name);
368
            $buckets = $relation->buildBuckets($primaryModels, $relation->getLink());
369 12
            $this->populateRelationFromBuckets($models, $name, $relation->getLink(), $buckets);
370
371
            return;
372 12
        }
373
374
        /** @var ActiveQuery $relation */
375 12
        $relation = $this->getARInstance()->relationQuery($name);
376 12
377
        $link = $relation->getLink();
378 4
        $primaryModel = reset($primaryModels);
379
380
        if ($relation->getMultiple()) {
381 12
            $buckets = $relation->buildBuckets($primaryModels, $link);
382 8
383 8
            if ($primaryModel instanceof ActiveRecordInterface) {
384 8
                if ($this->multiple) {
385 8
                    foreach ($primaryModels as $primaryModel) {
386 8
                        $models = $primaryModel->relation($primaryName);
387
                        $models = $this->populateRelationFromBuckets($models, $name, $link, $buckets);
388
                        $primaryModel->populateRelation($primaryName, $models);
389 8
                    }
390 4
                } else {
391
                    foreach ($primaryModels as $primaryModel) {
392
                        $models = [$primaryModel->relation($primaryName)];
393
                        $models = $this->populateRelationFromBuckets($models, $name, $link, $buckets);
394
                        $primaryModel->populateRelation($primaryName, $models[0]);
395 4
                    }
396 4
                }
397 4
            } else {
398
                if ($this->multiple) {
399
                    foreach ($primaryModels as &$primaryModel) {
400
                        $primaryModel[$primaryName] =
401 8
                            $this->populateRelationFromBuckets($primaryModel[$primaryName], $name, $link, $buckets);
402 8
                    }
403 8
                } else {
404 8
                    foreach ($primaryModels as &$primaryModel) {
405 8
                        $primaryModel[$primaryName] =
406
                            $this->populateRelationFromBuckets([$primaryModel[$primaryName]], $name, $link, $buckets)[0];
407 4
                    }
408
                }
409
            }
410
        } elseif ($this->multiple) {
411
            if ($primaryModel instanceof ActiveRecordInterface) {
412
                foreach ($primaryModels as $primaryModel) {
413
                    $models = $primaryModel->relation($primaryName);
414
415
                    foreach ($models as &$model) {
416
                        $model[$name] = $primaryModel;
417
                    }
418
                    unset($model);
419
420 12
                    $primaryModel->populateRelation($primaryName, $models);
421
                }
422 127
            } else {
423
                foreach ($primaryModels as &$primaryModel) {
424
                    foreach ($primaryModel[$primaryName] as &$model) {
425
                        $model[$name] = $primaryModel;
426
                    }
427
                    unset($model);
428
                }
429 127
            }
430 72
        } else {
431 72
            if ($primaryModel instanceof ActiveRecordInterface) {
432 72
                foreach ($primaryModels as $primaryModel) {
433 72
                    $model = $primaryModel->relation($primaryName);
434
                    $model[$name] = $primaryModel;
435 72
                    $primaryModel->populateRelation($primaryName, $model);
436 71
                }
437 71
            } else {
438 71
                foreach ($primaryModels as &$primaryModel) {
439
                    $primaryModel[$primaryName][$name] = $primaryModel;
440
                }
441 72
            }
442
        }
443 72
    }
444 72
445 5
    private function populateRelationFromBuckets(array $models, string $name, array $link, array $buckets): array
446 5
    {
447
        $model = reset($models);
448 5
        if ($model instanceof ActiveRecordInterface) {
449
            /** @var ActiveRecordInterface $model */
450
            foreach ($models as $model) {
451
                $key = $this->getModelKey($model, $link);
452 127
                $model->populateRelation($name, $buckets[$key] ?? []);
453 127
            }
454
        } else {
455 127
            foreach ($models as $i => $model) {
456 72
                $key = $this->getModelKey($model, $link);
457 71
                $models[$i][$name] = $buckets[$key] ?? [];
458 71
            }
459 71
        }
460 71
461
        return $models;
462
    }
463
464
    private function buildBuckets(
465 127
        array $models,
466 127
        array $link,
467 127
        array $viaModels = null,
468
        self $viaQuery = null
469
    ): array {
470
        if ($viaModels !== null) {
471 127
            $map = [];
472 56
            $linkValues = array_values($link);
473 56
            $viaLink = $viaQuery->link ?? [];
474
            $viaLinkKeys = array_keys($viaLink);
475
            $viaVia = null;
476
477 127
            foreach ($viaModels as $viaModel) {
478
                $key1 = $this->getModelKey($viaModel, $viaLinkKeys);
479
                $key2 = $this->getModelKey($viaModel, $linkValues);
480 5
                $map[$key2][$key1] = true;
481
            }
482 5
483
            if ($viaQuery !== null) {
484 5
                $viaQuery->viaMap = $map;
485 5
                $viaVia = $viaQuery->getVia();
486 5
            }
487
488
            while ($viaVia) {
489
                /**
490 5
                 * @var ActiveQuery $viaViaQuery
491
                 *
492
                 * @psalm-suppress RedundantCondition
493
                 */
494
                $viaViaQuery = is_array($viaVia) ? $viaVia[1] : $viaVia;
495
                $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...
496
497
                $viaVia = $viaViaQuery->getVia();
498
            }
499
        }
500
501
        $buckets = [];
502 17
        $linkKeys = array_keys($link);
503
504 17
        if (isset($map)) {
505
            foreach ($models as $model) {
506 17
                $key = $this->getModelKey($model, $linkKeys);
507 17
                if (isset($map[$key])) {
508 17
                    foreach (array_keys($map[$key]) as $key2) {
509 17
                        /** @psalm-suppress InvalidArrayOffset */
510 17
                        $buckets[$key2][] = $model;
511
                    }
512
                }
513
            }
514 17
        } else {
515
            foreach ($models as $model) {
516
                $key = $this->getModelKey($model, $linkKeys);
517
                $buckets[$key][] = $model;
518
            }
519
        }
520
521
        if (!$this->multiple) {
522 236
            foreach ($buckets as $i => $bucket) {
523
                $buckets[$i] = reset($bucket);
524 236
            }
525 36
        }
526 12
527
        return $buckets;
528 32
    }
529 32
530 32
    private function mapVia(array $map, array $viaMap): array
531
    {
532 32
        $resultMap = [];
533
534
        foreach ($map as $key => $linkKeys) {
535
            $resultMap[$key] = [];
536 36
            foreach (array_keys($linkKeys) as $linkKey) {
537 36
                /** @psalm-suppress InvalidArrayOffset */
538 36
                $resultMap[$key] += $viaMap[$linkKey];
539
            }
540
        }
541
542
        return $resultMap;
543 236
    }
544
545
    /**
546 236
     * Indexes buckets by a column name.
547
     *
548 236
     * @param Closure|string $indexBy the name of the column by which the query results should be indexed by. This can
549
     * also be a {@see Closure} that returns the index value based on the given models data.
550 236
     */
551
    private function indexBuckets(array $buckets, Closure|string $indexBy): array
552 236
    {
553 236
        foreach ($buckets as &$models) {
554
            $models = ArArrayHelper::index($models, $indexBy);
555 232
        }
556 232
557 232
        return $buckets;
558 228
    }
559
560 228
    /**
561
     * @param array $attributes the attributes to prefix.
562
     *
563 228
     * @throws \Yiisoft\Definitions\Exception\InvalidConfigException
564
     */
565
    private function prefixKeyColumns(array $attributes): array
566
    {
567
        if (!empty($this->join) || !empty($this->joinWith)) {
568 232
            if (empty($this->from)) {
569 232
                $alias = $this->getARInstance()->getTableName();
570
            } else {
571
                foreach ($this->from as $alias => $table) {
572
                    if (!is_string($alias)) {
573
                        $alias = $table;
574
                    }
575 8
                    break;
576
                }
577 8
            }
578 8
579
            if (isset($alias)) {
580 8
                foreach ($attributes as $i => $attribute) {
581 8
                    $attributes[$i] = "$alias.$attribute";
582
                }
583
            }
584 8
        }
585
586 8
        return $attributes;
587
    }
588
589
    /**
590
     * @throws \Yiisoft\Definitions\Exception\InvalidConfigException
591
     */
592 236
    protected function filterByModels(array $models): void
593 232
    {
594 232
        $attributes = array_keys($this->link);
595 232
        $attributes = $this->prefixKeyColumns($attributes);
596 232
597 228
        $model = reset($models);
598
        $values = [];
599 8
600
        if (count($attributes) === 1) {
601
            /** single key */
602
            $attribute = reset($this->link);
603 232
604 232
            if ($model instanceof ActiveRecordInterface) {
605
                foreach ($models as $model) {
606
                    $value = $model->getAttribute($attribute);
607 236
608 236
                    if ($value !== null) {
609
                        if (is_array($value)) {
610
                            $values = [...$values, ...$value];
611
                        } else {
612
                            $values[] = $value;
613
                        }
614
                    }
615
                }
616 127
            } else {
617
                foreach ($models as $model) {
618 127
                    if (isset($model[$attribute])) {
619
                        $value = $model[$attribute];
620 127
621 127
                        if (is_array($value)) {
622
                            $values = [...$values, ...$value];
623
                        } else {
624 127
                            $values[] = $value;
625
                        }
626
                    }
627
                }
628 127
            }
629
630 127
            if (!empty($values)) {
631
                $scalarValues = array_filter($values, 'is_scalar');
632
                $nonScalarValues = array_diff_key($values, $scalarValues);
633
634
                $scalarValues = array_unique($scalarValues);
635
                $values = [...$scalarValues, ...$nonScalarValues];
636
            }
637
        } else {
638 127
            $nulls = array_fill_keys($this->link, null);
639
640 127
            if ($model instanceof ActiveRecordInterface) {
641
                foreach ($models as $model) {
642
                    $value = $model->getAttributes($this->link);
643
644
                    if (!empty($value)) {
645
                        $values[] = array_combine($attributes, array_merge($nulls, $value));
646
                    }
647
                }
648 127
            } else {
649
                foreach ($models as $model) {
650
                    $value = array_intersect_key($model, $nulls);
651
652
                    if (!empty($value)) {
653
                        $values[] = array_combine($attributes, array_merge($nulls, $value));
654
                    }
655
                }
656 28
            }
657
        }
658 28
659
        if (empty($values)) {
660
            $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

660
            $this->/** @scrutinizer ignore-call */ 
661
                   emulateExecution();
Loading history...
661
            $this->andWhere('1=0');
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

661
            $this->/** @scrutinizer ignore-call */ 
662
                   andWhere('1=0');
Loading history...
662 28
            return;
663
        }
664
665 28
        $this->andWhere(['in', $attributes, $values]);
666
    }
667 28
668
    private function getModelKey(ActiveRecordInterface|array $activeRecord, array $attributes): false|int|string
669
    {
670
        $key = [];
671
672 28
        if (is_array($activeRecord)) {
0 ignored issues
show
introduced by
The condition is_array($activeRecord) is always true.
Loading history...
673
            foreach ($attributes as $attribute) {
674
                if (isset($activeRecord[$attribute])) {
675
                    $key[] = $this->normalizeModelKey($activeRecord[$attribute]);
676
                }
677
            }
678
        } else {
679
            foreach ($attributes as $attribute) {
680
                $value = $activeRecord->getAttribute($attribute);
681
682 39
                if ($value !== null) {
683
                    $key[] = $this->normalizeModelKey($value);
684 39
                }
685
            }
686
        }
687
688
        if (count($key) > 1) {
689
            return serialize($key);
690
        }
691
692 63
        $key = reset($key);
693
694 63
        return is_scalar($key) ? $key : serialize($key);
695
    }
696
697
    /**
698
     * @param int|string|Stringable|null $value raw key value.
699
     *
700
     * @return int|string|null normalized key value.
701
     */
702
    private function normalizeModelKey(int|string|Stringable|null $value): int|string|null
703
    {
704
        if ($value instanceof Stringable) {
705
            /**
706 113
             * ensure matching to special objects, which are convertible to string, for cross-DBMS relations,
707
             * for example: `|MongoId`
708 113
             */
709
            $value = (string) $value;
710
        }
711
712
        return $value;
713
    }
714
715
    /**
716
     * @param array $primaryModels either array of AR instances or arrays.
717
     *
718
     * @throws Exception
719 117
     * @throws Throwable
720
     * @throws \Yiisoft\Definitions\Exception\InvalidConfigException
721 117
     */
722
    private function findJunctionRows(array $primaryModels): array
723
    {
724 252
        if (empty($primaryModels)) {
725
            return [];
726 252
        }
727
728 252
        $this->filterByModels($primaryModels);
729
730
        /* @var $primaryModel ActiveRecord */
731 252
        $primaryModel = reset($primaryModels);
732
733 252
        if (!$primaryModel instanceof ActiveRecordInterface) {
0 ignored issues
show
introduced by
$primaryModel is always a sub-type of Yiisoft\ActiveRecord\ActiveRecordInterface.
Loading history...
734
            /** when primaryModels are array of arrays (asArray case) */
735 252
            $primaryModel = $this->arClass;
0 ignored issues
show
Unused Code introduced by
The assignment to $primaryModel is dead and can be removed.
Loading history...
736
        }
737
738 252
        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

738
        return $this->/** @scrutinizer ignore-call */ asArray()->all();
Loading history...
739
    }
740 252
741
    public function getMultiple(): bool
742 252
    {
743
        return $this->multiple;
744
    }
745
746
    /**
747
     * @return ActiveRecordInterface|null the primary model of a relational query.
748
     *
749
     * This is used only in lazy loading with dynamic query options.
750
     */
751
    public function getPrimaryModel(): ActiveRecordInterface|null
752
    {
753
        return $this->primaryModel;
754
    }
755
756
    /**
757
     * @psalm-return string[]
758
     */
759
    public function getLink(): array
760
    {
761
        return $this->link;
762
    }
763
764
    public function getVia(): array|ActiveQueryInterface|null
765
    {
766
        return $this->via;
767
    }
768
769
    public function multiple(bool $value): self
770
    {
771
        $this->multiple = $value;
772
773
        return $this;
774
    }
775
776
    public function primaryModel(ActiveRecordInterface $value): self
777
    {
778
        $this->primaryModel = $value;
779
780
        return $this;
781
    }
782
783
    public function link(array $value): self
784
    {
785
        $this->link = $value;
786
787
        return $this;
788
    }
789
}
790