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

populateInverseRelationToModels()   B

Complexity

Conditions 7
Paths 17

Size

Total Lines 26
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 9.7875

Importance

Changes 0
Metric Value
eloc 16
c 0
b 0
f 0
dl 0
loc 26
ccs 8
cts 13
cp 0.6153
rs 8.8333
cc 7
nc 17
nop 3
crap 9.7875
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, $viaModels, $viaQuery);
280 139
        } else {
281 28
            $buckets = $this->buildBuckets($models);
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
            $this->populateInverseRelationToModels($models, $primaryModels, $name);
367
            return;
368
        }
369 12
370
        $primaryModel = reset($primaryModels);
371
372 12
        if ($primaryModel instanceof ActiveRecordInterface) {
373
            if ($this->multiple) {
374
                foreach ($primaryModels as $primaryModel) {
375 12
                    $models = $primaryModel->relation($primaryName);
376 12
                    $this->populateInverseRelationToModels($models, $primaryModels, $name);
377
                    $primaryModel->populateRelation($primaryName, $models);
378 4
                }
379
            } else {
380
                foreach ($primaryModels as $primaryModel) {
381 12
                    $models = [$primaryModel->relation($primaryName)];
382 8
                    $this->populateInverseRelationToModels($models, $primaryModels, $name);
383 8
                    $primaryModel->populateRelation($primaryName, $models[0]);
384 8
                }
385 8
            }
386 8
        } else {
387
            if ($this->multiple) {
388
                foreach ($primaryModels as &$primaryModel) {
389 8
                    $this->populateInverseRelationToModels($primaryModel[$primaryName], $primaryModels, $name);
390 4
                }
391
            } else {
392
                foreach ($primaryModels as &$primaryModel) {
393
                    $models = [$primaryModel[$primaryName]];
394
                    $this->populateInverseRelationToModels($models, $primaryModels, $name);
395 4
                    $primaryModel[$primaryName] = $models[0];
396 4
                }
397 4
            }
398
        }
399
    }
400
401 8
    private function populateInverseRelationToModels(array &$models, array $primaryModels, string $name): void
402 8
    {
403 8
        if (empty($models)) {
404 8
            return;
405 8
        }
406
407 4
        $model = reset($models);
408
        $isArray = is_array($model);
409
410
        /** @var ActiveQuery $relation */
411
        $relation = $isArray ? $this->getARInstance()->relationQuery($name) : $model->relationQuery($name);
412
        $buckets = $relation->buildBuckets($primaryModels);
413
        $link = $relation->getLink();
414
        $default = $relation->getMultiple() ? [] : null;
415
416
        if ($isArray) {
417
            /** @var array $model */
418
            foreach ($models as &$model) {
419
                $key = $this->getModelKey($model, $link);
420 12
                $model[$name] = $buckets[$key] ?? $default;
421
            }
422 127
        } else {
423
            /** @var ActiveRecordInterface $model */
424
            foreach ($models as $model) {
425
                $key = $this->getModelKey($model, $link);
426
                $model->populateRelation($name, $buckets[$key] ?? $default);
427
            }
428
        }
429 127
    }
430 72
431 72
    private function buildBuckets(
432 72
        array $models,
433 72
        array $viaModels = null,
434
        self $viaQuery = null
435 72
    ): array {
436 71
        if ($viaModels !== null) {
437 71
            $map = [];
438 71
            $linkValues = $this->link;
439
            $viaLink = $viaQuery->link ?? [];
440
            $viaLinkKeys = array_keys($viaLink);
441 72
            $viaVia = null;
442
443 72
            foreach ($viaModels as $viaModel) {
444 72
                $key1 = $this->getModelKey($viaModel, $viaLinkKeys);
445 5
                $key2 = $this->getModelKey($viaModel, $linkValues);
446 5
                $map[$key2][$key1] = true;
447
            }
448 5
449
            if ($viaQuery !== null) {
450
                $viaQuery->viaMap = $map;
451
                $viaVia = $viaQuery->getVia();
452 127
            }
453 127
454
            while ($viaVia) {
455 127
                /**
456 72
                 * @var ActiveQuery $viaViaQuery
457 71
                 *
458 71
                 * @psalm-suppress RedundantCondition
459 71
                 */
460 71
                $viaViaQuery = is_array($viaVia) ? $viaVia[1] : $viaVia;
461
                $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...
462
463
                $viaVia = $viaViaQuery->getVia();
464
            }
465 127
        }
466 127
467 127
        $buckets = [];
468
        $linkKeys = array_keys($this->link);
469
470
        if (isset($map)) {
471 127
            foreach ($models as $model) {
472 56
                $key = $this->getModelKey($model, $linkKeys);
473 56
                if (isset($map[$key])) {
474
                    foreach (array_keys($map[$key]) as $key2) {
475
                        /** @psalm-suppress InvalidArrayOffset */
476
                        $buckets[$key2][] = $model;
477 127
                    }
478
                }
479
            }
480 5
        } else {
481
            foreach ($models as $model) {
482 5
                $key = $this->getModelKey($model, $linkKeys);
483
                $buckets[$key][] = $model;
484 5
            }
485 5
        }
486 5
487
        if (!$this->multiple) {
488
            foreach ($buckets as $i => $bucket) {
489
                $buckets[$i] = reset($bucket);
490 5
            }
491
        }
492
493
        return $buckets;
494
    }
495
496
    private function mapVia(array $map, array $viaMap): array
497
    {
498
        $resultMap = [];
499
500
        foreach ($map as $key => $linkKeys) {
501
            $resultMap[$key] = [];
502 17
            foreach (array_keys($linkKeys) as $linkKey) {
503
                /** @psalm-suppress InvalidArrayOffset */
504 17
                $resultMap[$key] += $viaMap[$linkKey];
505
            }
506 17
        }
507 17
508 17
        return $resultMap;
509 17
    }
510 17
511
    /**
512
     * Indexes buckets by a column name.
513
     *
514 17
     * @param Closure|string $indexBy the name of the column by which the query results should be indexed by. This can
515
     * also be a {@see Closure} that returns the index value based on the given models data.
516
     */
517
    private function indexBuckets(array $buckets, Closure|string $indexBy): array
518
    {
519
        foreach ($buckets as &$models) {
520
            $models = ArArrayHelper::index($models, $indexBy);
521
        }
522 236
523
        return $buckets;
524 236
    }
525 36
526 12
    /**
527
     * @param array $attributes the attributes to prefix.
528 32
     *
529 32
     * @throws \Yiisoft\Definitions\Exception\InvalidConfigException
530 32
     */
531
    private function prefixKeyColumns(array $attributes): array
532 32
    {
533
        if (!empty($this->join) || !empty($this->joinWith)) {
534
            if (empty($this->from)) {
535
                $alias = $this->getARInstance()->getTableName();
536 36
            } else {
537 36
                foreach ($this->from as $alias => $table) {
538 36
                    if (!is_string($alias)) {
539
                        $alias = $table;
540
                    }
541
                    break;
542
                }
543 236
            }
544
545
            if (isset($alias)) {
546 236
                foreach ($attributes as $i => $attribute) {
547
                    $attributes[$i] = "$alias.$attribute";
548 236
                }
549
            }
550 236
        }
551
552 236
        return $attributes;
553 236
    }
554
555 232
    /**
556 232
     * @throws \Yiisoft\Definitions\Exception\InvalidConfigException
557 232
     */
558 228
    protected function filterByModels(array $models): void
559
    {
560 228
        $attributes = array_keys($this->link);
561
        $attributes = $this->prefixKeyColumns($attributes);
562
563 228
        $model = reset($models);
564
        $values = [];
565
566
        if (count($attributes) === 1) {
567
            /** single key */
568 232
            $attribute = reset($this->link);
569 232
570
            if ($model instanceof ActiveRecordInterface) {
571
                foreach ($models as $model) {
572
                    $value = $model->getAttribute($attribute);
573
574
                    if ($value !== null) {
575 8
                        if (is_array($value)) {
576
                            $values = [...$values, ...$value];
577 8
                        } else {
578 8
                            $values[] = $value;
579
                        }
580 8
                    }
581 8
                }
582
            } else {
583
                foreach ($models as $model) {
584 8
                    if (isset($model[$attribute])) {
585
                        $value = $model[$attribute];
586 8
587
                        if (is_array($value)) {
588
                            $values = [...$values, ...$value];
589
                        } else {
590
                            $values[] = $value;
591
                        }
592 236
                    }
593 232
                }
594 232
            }
595 232
596 232
            if (!empty($values)) {
597 228
                $scalarValues = array_filter($values, 'is_scalar');
598
                $nonScalarValues = array_diff_key($values, $scalarValues);
599 8
600
                $scalarValues = array_unique($scalarValues);
601
                $values = [...$scalarValues, ...$nonScalarValues];
602
            }
603 232
        } else {
604 232
            $nulls = array_fill_keys($this->link, null);
605
606
            if ($model instanceof ActiveRecordInterface) {
607 236
                foreach ($models as $model) {
608 236
                    $value = $model->getAttributes($this->link);
609
610
                    if (!empty($value)) {
611
                        $values[] = array_combine($attributes, array_merge($nulls, $value));
612
                    }
613
                }
614
            } else {
615
                foreach ($models as $model) {
616 127
                    $value = array_intersect_key($model, $nulls);
617
618 127
                    if (!empty($value)) {
619
                        $values[] = array_combine($attributes, array_merge($nulls, $value));
620 127
                    }
621 127
                }
622
            }
623
        }
624 127
625
        if (empty($values)) {
626
            $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

626
            $this->/** @scrutinizer ignore-call */ 
627
                   emulateExecution();
Loading history...
627
            $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

627
            $this->/** @scrutinizer ignore-call */ 
628
                   andWhere('1=0');
Loading history...
628 127
            return;
629
        }
630 127
631
        $this->andWhere(['in', $attributes, $values]);
632
    }
633
634
    private function getModelKey(ActiveRecordInterface|array $activeRecord, array $attributes): false|int|string
635
    {
636
        $key = [];
637
638 127
        if (is_array($activeRecord)) {
0 ignored issues
show
introduced by
The condition is_array($activeRecord) is always true.
Loading history...
639
            foreach ($attributes as $attribute) {
640 127
                if (isset($activeRecord[$attribute])) {
641
                    $key[] = $this->normalizeModelKey($activeRecord[$attribute]);
642
                }
643
            }
644
        } else {
645
            foreach ($attributes as $attribute) {
646
                $value = $activeRecord->getAttribute($attribute);
647
648 127
                if ($value !== null) {
649
                    $key[] = $this->normalizeModelKey($value);
650
                }
651
            }
652
        }
653
654
        if (count($key) > 1) {
655
            return serialize($key);
656 28
        }
657
658 28
        $key = reset($key);
659
660
        return is_scalar($key) ? $key : serialize($key);
661
    }
662 28
663
    /**
664
     * @param int|string|Stringable|null $value raw key value.
665 28
     *
666
     * @return int|string|null normalized key value.
667 28
     */
668
    private function normalizeModelKey(int|string|Stringable|null $value): int|string|null
669
    {
670
        if ($value instanceof Stringable) {
671
            /**
672 28
             * ensure matching to special objects, which are convertible to string, for cross-DBMS relations,
673
             * for example: `|MongoId`
674
             */
675
            $value = (string) $value;
676
        }
677
678
        return $value;
679
    }
680
681
    /**
682 39
     * @param array $primaryModels either array of AR instances or arrays.
683
     *
684 39
     * @throws Exception
685
     * @throws Throwable
686
     * @throws \Yiisoft\Definitions\Exception\InvalidConfigException
687
     */
688
    private function findJunctionRows(array $primaryModels): array
689
    {
690
        if (empty($primaryModels)) {
691
            return [];
692 63
        }
693
694 63
        $this->filterByModels($primaryModels);
695
696
        /* @var $primaryModel ActiveRecord */
697
        $primaryModel = reset($primaryModels);
698
699
        if (!$primaryModel instanceof ActiveRecordInterface) {
0 ignored issues
show
introduced by
$primaryModel is always a sub-type of Yiisoft\ActiveRecord\ActiveRecordInterface.
Loading history...
700
            /** when primaryModels are array of arrays (asArray case) */
701
            $primaryModel = $this->arClass;
0 ignored issues
show
Unused Code introduced by
The assignment to $primaryModel is dead and can be removed.
Loading history...
702
        }
703
704
        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

704
        return $this->/** @scrutinizer ignore-call */ asArray()->all();
Loading history...
705
    }
706 113
707
    public function getMultiple(): bool
708 113
    {
709
        return $this->multiple;
710
    }
711
712
    /**
713
     * @return ActiveRecordInterface|null the primary model of a relational query.
714
     *
715
     * This is used only in lazy loading with dynamic query options.
716
     */
717
    public function getPrimaryModel(): ActiveRecordInterface|null
718
    {
719 117
        return $this->primaryModel;
720
    }
721 117
722
    /**
723
     * @psalm-return string[]
724 252
     */
725
    public function getLink(): array
726 252
    {
727
        return $this->link;
728 252
    }
729
730
    public function getVia(): array|ActiveQueryInterface|null
731 252
    {
732
        return $this->via;
733 252
    }
734
735 252
    public function multiple(bool $value): self
736
    {
737
        $this->multiple = $value;
738 252
739
        return $this;
740 252
    }
741
742 252
    public function primaryModel(ActiveRecordInterface $value): self
743
    {
744
        $this->primaryModel = $value;
745
746
        return $this;
747
    }
748
749
    public function link(array $value): self
750
    {
751
        $this->link = $value;
752
753
        return $this;
754
    }
755
}
756