Passed
Push — master ( ae9b6b...aec4ed )
by Wilmer
11:33 queued 09:14
created

ActiveRelationTrait::__clone()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 8
ccs 0
cts 6
cp 0
rs 10
c 0
b 0
f 0
cc 3
nc 3
nop 0
crap 12
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\ActiveRecord;
6
7
use Yiisoft\Db\Expression\ArrayExpression;
8
use Yiisoft\Db\Exception\InvalidArgumentException;
9
use Yiisoft\Db\Exception\InvalidConfigException;
10
11
/**
12
 * ActiveRelationTrait implements the common methods and properties for active record relational queries.
13
 *
14
 * @method ActiveRecordInterface one()
15
 * @method ActiveRecordInterface[] all()
16
 *
17
 * @property ActiveRecord $modelClass
18
 */
19
trait ActiveRelationTrait
20
{
21
    /**
22
     * @var bool whether this query represents a relation to more than one record.
23
     *
24
     * This property is only used in relational context. If true, this relation will populate all query results into AR
25
     * instances using {@see Query::all()|all()}. If false, only the first row of the results will be retrieved using
26
     * {@see Query::one()|one()}.
27
     */
28
    public bool $multiple = false;
29
30
    /**
31
     * @var ActiveRecord the primary model of a relational query.
32
     *
33
     * This is used only in lazy loading with dynamic query options.
34
     */
35
    public ?ActiveRecord $primaryModel = null;
36
37
    /**
38
     * @var array the columns of the primary and foreign tables that establish a relation.
39
     *
40
     * The array keys must be columns of the table for this relation, and the array values must be the corresponding
41
     * columns from the primary table.
42
     * Do not prefix or quote the column names as this will be done automatically by Yii. This property is only used in
43
     * relational context.
44
     */
45
    public array $link = [];
46
47
    /**
48
     * @var array|object the query associated with the junction table. Please call {@see via()} to set this property
49
     * instead of directly setting it.
50
     *
51
     * This property is only used in relational context.
52
     *
53
     * {@see via()}
54
     */
55
    public $via;
56
57
    /**
58
     * @var string the name of the relation that is the inverse of this relation.
59
     *
60
     * For example, an order has a customer, which means the inverse of the "customer" relation is the "orders", and the
61
     * inverse of the "orders" relation is the "customer". If this property is set, the primary record(s) will be
62
     * referenced through the specified relation.
63
     *
64
     * For example, `$customer->orders[0]->customer` and `$customer` will be the same object, and accessing the customer
65
     * of an order will not trigger new DB query.
66
     *
67
     * This property is only used in relational context.
68
     *
69
     * {@see inverseOf()}
70
     */
71
    public ?string $inverseOf = null;
72
73
    private $viaMap;
74
75
    /**
76
     * Clones internal objects.
77
     */
78
    public function __clone()
79
    {
80
        parent::__clone();
81
        /* make a clone of "via" object so that the same query object can be reused multiple times */
82
        if (\is_object($this->via)) {
83
            $this->via = clone $this->via;
84
        } elseif (\is_array($this->via)) {
0 ignored issues
show
introduced by
The condition is_array($this->via) is always true.
Loading history...
85
            $this->via = [$this->via[0], clone $this->via[1], $this->via[2]];
86
        }
87
    }
88
89
    /**
90
     * Specifies the relation associated with the junction table.
91
     *
92
     * Use this method to specify a pivot record/table when declaring a relation in the {@see ActiveRecord} class:
93
     *
94
     * ```php
95
     * class Order extends ActiveRecord
96
     * {
97
     *    public function getOrderItems() {
98
     *        return $this->hasMany(OrderItem::class, ['order_id' => 'id']);
99
     *    }
100
     *
101
     *    public function getItems() {
102
     *        return $this->hasMany(Item::class, ['id' => 'item_id'])
103
     *                    ->via('orderItems');
104
     *    }
105
     * }
106
     * ```
107
     *
108
     * @param string $relationName the relation name. This refers to a relation declared in {@see primaryModel}.
109
     * @param callable $callable a PHP callback for customizing the relation associated with the junction table.
110
     * Its signature should be `function($query)`, where `$query` is the query to be customized.
111
     *
112
     * @return $this the relation object itself.
113
     */
114 30
    public function via($relationName, callable $callable = null): self
115
    {
116 30
        $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

116
        /** @scrutinizer ignore-call */ 
117
        $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...
117 30
        $callableUsed = $callable !== null;
118 30
        $this->via = [$relationName, $relation, $callableUsed];
119 30
        if ($callable !== null) {
120 12
            $callable($relation);
121
        }
122
123 30
        return $this;
124
    }
125
126
    /**
127
     * Sets the name of the relation that is the inverse of this relation.
128
     *
129
     * For example, a customer has orders, which means the inverse of the "orders" relation is the "customer".
130
     *
131
     * If this property is set, the primary record(s) will be referenced through the specified relation.
132
     *
133
     * For example, `$customer->orders[0]->customer` and `$customer` will be the same object, and accessing the customer
134
     * of an order will not trigger a new DB query.
135
     *
136
     * Use this method when declaring a relation in the {@see ActiveRecord} class, e.g. in Customer model:
137
     *
138
     * ```php
139
     * public function getOrders()
140
     * {
141
     *     return $this->hasMany(Order::class, ['customer_id' => 'id'])->inverseOf('customer');
142
     * }
143
     * ```
144
     *
145
     * This also may be used for Order model, but with caution:
146
     *
147
     * ```php
148
     * public function getCustomer()
149
     * {
150
     *     return $this->hasOne(Customer::class, ['id' => 'customer_id'])->inverseOf('orders');
151
     * }
152
     * ```
153
     *
154
     * in this case result will depend on how order(s) was loaded.
155
     * Let's suppose customer has several orders. If only one order was loaded:
156
     *
157
     * ```php
158
     * $orders = Order::find()->where(['id' => 1])->all();
159
     * $customerOrders = $orders[0]->customer->orders;
160
     * ```
161
     *
162
     * variable `$customerOrders` will contain only one order. If orders was loaded like this:
163
     *
164
     * ```php
165
     * $orders = Order::find()->with('customer')->where(['customer_id' => 1])->all();
166
     * $customerOrders = $orders[0]->customer->orders;
167
     * ```
168
     *
169
     * variable `$customerOrders` will contain all orders of the customer.
170
     *
171
     * @param string $relationName the name of the relation that is the inverse of this relation.
172
     *
173
     * @return $this the relation object itself.
174
     */
175 12
    public function inverseOf($relationName): self
176
    {
177 12
        $this->inverseOf = $relationName;
178
179 12
        return $this;
180
    }
181
182
    /**
183
     * Finds the related records for the specified primary record.
184
     * This method is invoked when a relation of an ActiveRecord is being accessed in a lazy fashion.
185
     * @param string $name the relation name
186
     * @param ActiveRecordInterface|BaseActiveRecord $model the primary model
187
     *
188
     * @throws InvalidArgumentException if the relation is invalid
189
     * @throws \ReflectionException
190
     *
191
     * @return mixed the related record(s)
192
     */
193 45
    public function findFor($name, $model)
194
    {
195 45
        if (\method_exists($model, 'get' . $name)) {
196 45
            $method = new \ReflectionMethod($model, 'get' . $name);
197 45
            $realName = \lcfirst(substr($method->getName(), 3));
198 45
            if ($realName !== $name) {
199
                throw new InvalidArgumentException(
200
                    'Relation names are case sensitive. ' . \get_class($model)
201
                    . " has a relation named \"$realName\" instead of \"$name\"."
202
                );
203
            }
204
        }
205
206 45
        return $this->multiple ? $this->all() : $this->one();
207
    }
208
209
    /**
210
     * If applicable, populate the query's primary model into the related records' inverse relationship.
211
     *
212
     * @param array $result the array of related records as generated by {@see populate()}
213
     */
214 12
    private function addInverseRelations(&$result): void
215
    {
216 12
        if ($this->inverseOf === null) {
217
            return;
218
        }
219
220 12
        foreach ($result as $i => $relatedModel) {
221 12
            if ($relatedModel instanceof ActiveRecordInterface) {
222 12
                if (!isset($inverseRelation)) {
223 12
                    $inverseRelation = $relatedModel->getRelation($this->inverseOf);
224
                }
225 12
                $relatedModel->populateRelation(
226 12
                    $this->inverseOf,
227 12
                    $inverseRelation->multiple ? [$this->primaryModel] : $this->primaryModel
228
                );
229
            } else {
230 9
                if (!isset($inverseRelation)) {
231
                    /* @var $modelClass ActiveRecordInterface */
232 9
                    $modelClass = $this->modelClass;
233 9
                    $inverseRelation = $modelClass::instance()->getRelation($this->inverseOf);
234
                }
235 9
                $result[$i][$this->inverseOf] = $inverseRelation->multiple
236 9
                    ? [$this->primaryModel] : $this->primaryModel;
237
            }
238
        }
239 12
    }
240
241
    /**
242
     * Finds the related records and populates them into the primary models.
243
     *
244
     * @param string $name the relation name
245
     * @param array $primaryModels primary models
246
     *
247
     * @throws InvalidConfigException if {@see link} is invalid
248
     *
249
     * @return array the related models
250
     */
251 57
    public function populateRelation($name, &$primaryModels): array
252
    {
253 57
        if (!\is_array($this->link)) {
0 ignored issues
show
introduced by
The condition is_array($this->link) is always true.
Loading history...
254
            throw new InvalidConfigException('Invalid link: it must be an array of key-value pairs.');
255
        }
256
257 57
        if ($this->via instanceof self) {
258
            /* via junction table */
259
            /* @var $viaQuery ActiveRelationTrait */
260 9
            $viaQuery = $this->via;
261 9
            $viaModels = $viaQuery->findJunctionRows($primaryModels);
262 9
            $this->filterByModels($viaModels);
263 57
        } elseif (\is_array($this->via)) {
264
            /* via relation */
265
            /* @var $viaQuery ActiveRelationTrait|ActiveQueryTrait */
266 21
            [$viaName, $viaQuery] = $this->via;
267
268 21
            if ($viaQuery->asArray === null) {
269
                /* inherit asArray from primary query */
270 21
                $viaQuery->asArray($this->asArray);
0 ignored issues
show
Bug introduced by
It seems like asArray() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

270
                $viaQuery->/** @scrutinizer ignore-call */ 
271
                           asArray($this->asArray);
Loading history...
271
            }
272 21
            $viaQuery->primaryModel = null;
0 ignored issues
show
Bug Best Practice introduced by
The property primaryModel does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
273 21
            $viaModels = $viaQuery->populateRelation($viaName, $primaryModels);
0 ignored issues
show
Bug introduced by
It seems like populateRelation() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

273
            /** @scrutinizer ignore-call */ 
274
            $viaModels = $viaQuery->populateRelation($viaName, $primaryModels);
Loading history...
274 21
            $this->filterByModels($viaModels);
275
        } else {
276 57
            $this->filterByModels($primaryModels);
277
        }
278
279 57
        if (!$this->multiple && \count($primaryModels) === 1) {
280 21
            $model = $this->one();
281 21
            $primaryModel = \reset($primaryModels);
282 21
            if ($primaryModel instanceof ActiveRecordInterface) {
283 21
                $primaryModel->populateRelation($name, $model);
284
            } else {
285 3
                $primaryModels[\key($primaryModels)][$name] = $model;
286
            }
287 21
            if ($this->inverseOf !== null) {
288 6
                $this->populateInverseRelation($primaryModels, [$model], $name, $this->inverseOf);
289
            }
290
291 21
            return [$model];
292
        }
293
294
        /**
295
         * https://github.com/yiisoft/yii2/issues/3197
296
         * delay indexing related models after buckets are built
297
         */
298 48
        $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

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

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

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

572
                        $values = \array_merge($values, /** @scrutinizer ignore-type */ $value->getValue());
Loading history...
573
                    } else {
574 90
                        $values[] = $value;
575
                    }
576
                }
577
            }
578 93
            if (empty($values)) {
579 93
                $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

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

613
        $this->/** @scrutinizer ignore-call */ 
614
               andWhere(['in', $attributes, $values]);
Loading history...
614 96
    }
615
616
    /**
617
     * @param ActiveRecordInterface|array $model
618
     * @param array $attributes
619
     *
620
     * @return int|string
621
     */
622 51
    private function getModelKey($model, array $attributes)
623
    {
624 51
        $key = [];
625
626 51
        foreach ($attributes as $attribute) {
627 51
            $key[] = $this->normalizeModelKey($model[$attribute]);
628
        }
629
630 51
        if (\count($key) > 1) {
631
            return \serialize($key);
632
        }
633
634 51
        $key = \reset($key);
635
636 51
        return \is_scalar($key) ? $key : \serialize($key);
637
    }
638
639
    /**
640
     * @param mixed $value raw key value.
641
     *
642
     * @return int|string normalized key value.
643
     */
644 51
    private function normalizeModelKey($value)
645
    {
646 51
        if (\is_object($value) && \method_exists($value, '__toString')) {
647
            /**
648
             * ensure matching to special objects, which are convertable to string, for cross-DBMS relations,
649
             * for example: `|MongoId`
650
             */
651
            $value = $value->__toString();
652
        }
653
654 51
        return $value;
655
    }
656
657
    /**
658
     * @param array $primaryModels either array of AR instances or arrays
659
     *
660
     * @return array
661
     */
662 15
    private function findJunctionRows(array $primaryModels): array
663
    {
664 15
        if (empty($primaryModels)) {
665
            return [];
666
        }
667
668 15
        $this->filterByModels($primaryModels);
669
        /* @var $primaryModel ActiveRecord */
670 15
        $primaryModel = reset($primaryModels);
671
672 15
        if (!$primaryModel instanceof ActiveRecordInterface) {
0 ignored issues
show
introduced by
$primaryModel is always a sub-type of Yiisoft\ActiveRecord\ActiveRecordInterface.
Loading history...
673
            // when primaryModels are array of arrays (asArray case)
674
            $primaryModel = $this->modelClass;
675
        }
676
677 15
        return $this->asArray()->all();
678
    }
679
}
680