Passed
Push — master ( a75adc...a47013 )
by Wilmer
14:47 queued 12:46
created

ActiveRelationTrait::prefixKeyColumns()   B

Complexity

Conditions 9
Paths 9

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 9

Importance

Changes 0
Metric Value
eloc 13
dl 0
loc 23
ccs 13
cts 13
cp 1
rs 8.0555
c 0
b 0
f 0
cc 9
nc 9
nop 1
crap 9
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\ActiveRecord;
6
7
use Yiisoft\Db\Exception\Exception;
8
use Yiisoft\Db\Exception\NotSupportedException;
9
use Yiisoft\Db\Expression\ArrayExpression;
10
use Yiisoft\Db\Exception\InvalidArgumentException;
11
use Yiisoft\Db\Exception\InvalidConfigException;
12
13
/**
14
 * ActiveRelationTrait implements the common methods and properties for active record relational queries.
15
 *
16
 * @method ActiveRecordInterface one()
17
 * @method ActiveRecordInterface[] all()
18
 *
19
 * @property ActiveRecord $modelClass
20
 */
21
trait ActiveRelationTrait
22
{
23
    protected bool $multiple = false;
24
    protected ?ActiveRecord $primaryModel = null;
25
    protected array $link = [];
26
    protected $via;
27
    protected ?string $inverseOf = null;
28
    protected $viaMap;
29
30
    /**
31
     * Clones internal objects.
32
     */
33
    public function __clone()
34
    {
35
        parent::__clone();
36
        /* make a clone of "via" object so that the same query object can be reused multiple times */
37
        if (\is_object($this->via)) {
38
            $this->via = clone $this->via;
39
        } elseif (\is_array($this->via)) {
40
            $this->via = [$this->via[0], clone $this->via[1], $this->via[2]];
41
        }
42
    }
43
44
    /**
45
     * Specifies the relation associated with the junction table.
46
     *
47
     * Use this method to specify a pivot record/table when declaring a relation in the {@see ActiveRecord} class:
48
     *
49
     * ```php
50
     * class Order extends ActiveRecord
51
     * {
52
     *    public function getOrderItems() {
53
     *        return $this->hasMany(OrderItem::class, ['order_id' => 'id']);
54
     *    }
55
     *
56
     *    public function getItems() {
57
     *        return $this->hasMany(Item::class, ['id' => 'item_id'])
58
     *                    ->via('orderItems');
59
     *    }
60
     * }
61
     * ```
62
     *
63
     * @param string $relationName the relation name. This refers to a relation declared in {@see primaryModel}.
64
     * @param callable $callable a PHP callback for customizing the relation associated with the junction table.
65
     * Its signature should be `function($query)`, where `$query` is the query to be customized.
66
     *
67
     * @throws InvalidArgumentException
68
     * @throws \ReflectionException
69
     *
70
     * @return self the relation object itself.
71
     */
72 60
    public function via(string $relationName, callable $callable = null): self
73
    {
74 60
        $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

74
        /** @scrutinizer ignore-call */ 
75
        $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...
75 60
        $callableUsed = $callable !== null;
76 60
        $this->via = [$relationName, $relation, $callableUsed];
77
78 60
        if ($callable !== null) {
79 39
            $callable($relation);
80
        }
81
82 60
        return $this;
83
    }
84
85
    /**
86
     * Sets the name of the relation that is the inverse of this relation.
87
     *
88
     * For example, a customer has orders, which means the inverse of the "orders" relation is the "customer".
89
     *
90
     * If this property is set, the primary record(s) will be referenced through the specified relation.
91
     *
92
     * For example, `$customer->orders[0]->customer` and `$customer` will be the same object, and accessing the customer
93
     * of an order will not trigger a new DB query.
94
     *
95
     * Use this method when declaring a relation in the {@see ActiveRecord} class, e.g. in Customer model:
96
     *
97
     * ```php
98
     * public function getOrders()
99
     * {
100
     *     return $this->hasMany(Order::class, ['customer_id' => 'id'])->inverseOf('customer');
101
     * }
102
     * ```
103
     *
104
     * This also may be used for Order model, but with caution:
105
     *
106
     * ```php
107
     * public function getCustomer()
108
     * {
109
     *     return $this->hasOne(Customer::class, ['id' => 'customer_id'])->inverseOf('orders');
110
     * }
111
     * ```
112
     *
113
     * in this case result will depend on how order(s) was loaded.
114
     * Let's suppose customer has several orders. If only one order was loaded:
115
     *
116
     * ```php
117
     * $orders = Order::find()->where(['id' => 1])->all();
118
     * $customerOrders = $orders[0]->customer->orders;
119
     * ```
120
     *
121
     * variable `$customerOrders` will contain only one order. If orders was loaded like this:
122
     *
123
     * ```php
124
     * $orders = Order::find()->with('customer')->where(['customer_id' => 1])->all();
125
     * $customerOrders = $orders[0]->customer->orders;
126
     * ```
127
     *
128
     * variable `$customerOrders` will contain all orders of the customer.
129
     *
130
     * @param string $relationName the name of the relation that is the inverse of this relation.
131
     *
132
     * @return self the relation object itself.
133
     */
134 12
    public function inverseOf(string $relationName): self
135
    {
136 12
        $this->inverseOf = $relationName;
137
138 12
        return $this;
139
    }
140
141
    /**
142
     * Finds the related records for the specified primary record.
143
     *
144
     * This method is invoked when a relation of an ActiveRecord is being accessed in a lazy fashion.
145
     *
146
     * @param string $name the relation name.
147
     * @param ActiveRecordInterface $model the primary model.
148
     *
149
     * @throws \ReflectionException
150
     * @throws Exception
151
     * @throws InvalidArgumentException if the relation is invalid.
152
     * @throws InvalidConfigException
153
     * @throws NotSupportedException
154
     *
155
     * @return mixed the related record(s).
156
     */
157 69
    public function findFor(string $name, ActiveRecordInterface $model)
158
    {
159 69
        if (\method_exists($model, 'get' . $name)) {
160 69
            $method = new \ReflectionMethod($model, 'get' . $name);
161 69
            $realName = \lcfirst(substr($method->getName(), 3));
162 69
            if ($realName !== $name) {
163
                throw new InvalidArgumentException(
164
                    'Relation names are case sensitive. ' . \get_class($model)
165
                    . " has a relation named \"$realName\" instead of \"$name\"."
166
                );
167
            }
168
        }
169
170 69
        return $this->multiple ? $this->all() : $this->one();
171
    }
172
173
    /**
174
     * If applicable, populate the query's primary model into the related records' inverse relationship.
175
     *
176
     * @param array $result the array of related records as generated by {@see populate()}
177
     */
178 12
    private function addInverseRelations(array &$result): void
179
    {
180 12
        if ($this->inverseOf === null) {
181
            return;
182
        }
183
184 12
        foreach ($result as $i => $relatedModel) {
185 12
            if ($relatedModel instanceof ActiveRecordInterface) {
186 12
                if (!isset($inverseRelation)) {
187 12
                    $inverseRelation = $relatedModel->getRelation($this->inverseOf);
188
                }
189 12
                $relatedModel->populateRelation(
190 12
                    $this->inverseOf,
191 12
                    $inverseRelation->multiple ? [$this->primaryModel] : $this->primaryModel
192
                );
193
            } else {
194 9
                if (!isset($inverseRelation)) {
195
                    /** @var $modelClass ActiveRecordInterface */
196 9
                    $modelClass = $this->modelClass;
197 9
                    $inverseRelation = $modelClass::instance()->getRelation($this->inverseOf);
0 ignored issues
show
Bug introduced by
It seems like getRelation() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

197
                    $inverseRelation = $modelClass::instance()->/** @scrutinizer ignore-call */ getRelation($this->inverseOf);
Loading history...
198
                }
199 9
                $result[$i][$this->inverseOf] = $inverseRelation->multiple
200 9
                    ? [$this->primaryModel] : $this->primaryModel;
201
            }
202
        }
203 12
    }
204
205
    /**
206
     * Finds the related records and populates them into the primary models.
207
     *
208
     * @param string $name the relation name
209
     * @param array $primaryModels primary models
210
     *
211
     * @throws Exception
212
     * @throws InvalidArgumentException
213
     * @throws InvalidConfigException if {@see link()} is invalid
214
     * @throws NotSupportedException
215
     *
216
     * @return array the related models
217
     */
218 75
    public function populateRelation(string $name, array &$primaryModels): array
219
    {
220 75
        if (!\is_array($this->link)) {
0 ignored issues
show
introduced by
The condition is_array($this->link) is always true.
Loading history...
221
            throw new InvalidConfigException('Invalid link: it must be an array of key-value pairs.');
222
        }
223
224 75
        if ($this->via instanceof self) {
225
            /**
226
             * via junction table
227
             *
228
             * @var $viaQuery ActiveRelationTrait
229
             */
230 9
            $viaQuery = $this->via;
231 9
            $viaModels = $viaQuery->findJunctionRows($primaryModels);
232 9
            $this->filterByModels($viaModels);
233 75
        } elseif (\is_array($this->via)) {
234
            /**
235
             * via relation
236
             *
237
             * @var $viaQuery ActiveRelationTrait|ActiveQueryTrait
238
             */
239 39
            [$viaName, $viaQuery] = $this->via;
240
241 39
            if ($viaQuery->asArray === null) {
242
                /** inherit asArray from primary query */
243 39
                $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

243
                $viaQuery->/** @scrutinizer ignore-call */ 
244
                           asArray($this->asArray);
Loading history...
244
            }
245 39
            $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...
246 39
            $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

246
            /** @scrutinizer ignore-call */ 
247
            $viaModels = $viaQuery->populateRelation($viaName, $primaryModels);
Loading history...
247 39
            $this->filterByModels($viaModels);
248
        } else {
249 75
            $this->filterByModels($primaryModels);
250
        }
251
252 75
        if (!$this->multiple && \count($primaryModels) === 1) {
253 21
            $model = $this->one();
254 21
            $primaryModel = \reset($primaryModels);
255 21
            if ($primaryModel instanceof ActiveRecordInterface) {
256 21
                $primaryModel->populateRelation($name, $model);
257
            } else {
258 3
                $primaryModels[\key($primaryModels)][$name] = $model;
259
            }
260 21
            if ($this->inverseOf !== null) {
261 6
                $this->populateInverseRelation($primaryModels, [$model], $name, $this->inverseOf);
262
            }
263
264 21
            return [$model];
265
        }
266
267
        /**
268
         * {@see https://github.com/yiisoft/yii2/issues/3197}
269
         *
270
         * delay indexing related models after buckets are built.
271
         */
272 66
        $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

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

273
        $this->/** @scrutinizer ignore-call */ 
274
               indexBy(null);
Loading history...
274 66
        $models = $this->all();
275
276 66
        if (isset($viaModels, $viaQuery)) {
277 39
            $buckets = $this->buildBuckets($models, $this->link, $viaModels, $viaQuery);
278
        } else {
279 66
            $buckets = $this->buildBuckets($models, $this->link);
280
        }
281
282 66
        $this->indexBy($indexBy);
283 66
        if ($this->getIndexBy() !== null && $this->multiple) {
284 12
            $buckets = $this->indexBuckets($buckets, $this->getIndexBy());
285
        }
286
287 66
        $link = \array_values($this->link);
288 66
        if (isset($viaQuery)) {
289 39
            $deepViaQuery = $viaQuery;
290 39
            while ($deepViaQuery->via) {
291 3
                $deepViaQuery = \is_array($deepViaQuery->via) ? $deepViaQuery->via[1] : $deepViaQuery->via;
292
            }
293 39
            $link = \array_values($deepViaQuery->link);
294
        }
295 66
        foreach ($primaryModels as $i => $primaryModel) {
296 66
            if ($this->multiple && \count($link) === 1 && \is_array($keys = $primaryModel[\reset($link)])) {
297
                $value = [];
298
                foreach ($keys as $key) {
299
                    $key = $this->normalizeModelKey($key);
300
                    if (isset($buckets[$key])) {
301
                        if ($this->getIndexBy() !== null) {
302
                            // if indexBy is set, array_merge will cause renumbering of numeric array
303
                            foreach ($buckets[$key] as $bucketKey => $bucketValue) {
304
                                $value[$bucketKey] = $bucketValue;
305
                            }
306
                        } else {
307
                            $value = \array_merge($value, $buckets[$key]);
308
                        }
309
                    }
310
                }
311
            } else {
312 66
                $key = $this->getModelKey($primaryModel, $link);
313 66
                $value = isset($buckets[$key]) ? $buckets[$key] : ($this->multiple ? [] : null);
314
            }
315
316 66
            if ($primaryModel instanceof ActiveRecordInterface) {
317 66
                $primaryModel->populateRelation($name, $value);
318
            } else {
319 9
                $primaryModels[$i][$name] = $value;
320
            }
321
        }
322 66
        if ($this->inverseOf !== null) {
323 6
            $this->populateInverseRelation($primaryModels, $models, $name, $this->inverseOf);
324
        }
325
326 66
        return $models;
327
    }
328
329 9
    private function populateInverseRelation(array &$primaryModels, array $models, string $primaryName, string $name): void
330
    {
331 9
        if (empty($models) || empty($primaryModels)) {
332
            return;
333
        }
334 9
        $model = \reset($models);
335
336
        /** @var $relation ActiveQueryInterface|ActiveQuery */
337 9
        if ($model instanceof ActiveRecordInterface) {
338 9
            $relation = $model->getRelation($name);
339
        } else {
340
            /** @var $modelClass ActiveRecordInterface */
341 6
            $modelClass = $this->modelClass;
342 6
            $relation = $modelClass::instance()->getRelation($name);
343
        }
344
345 9
        if ($relation->multiple) {
346 6
            $buckets = $this->buildBuckets($primaryModels, $relation->link, null, null, false);
347 6
            if ($model instanceof ActiveRecordInterface) {
348 6
                foreach ($models as $model) {
349 6
                    $key = $this->getModelKey($model, $relation->link);
350 6
                    $model->populateRelation($name, $buckets[$key] ?? []);
351
                }
352
            } else {
353 6
                foreach ($primaryModels as $i => $primaryModel) {
354 3
                    if ($this->multiple) {
355
                        foreach ($primaryModel as $j => $m) {
356
                            $key = $this->getModelKey($m, $relation->link);
357
                            $primaryModels[$i][$j][$name] = $buckets[$key] ?? [];
358
                        }
359 3
                    } elseif (!empty($primaryModel[$primaryName])) {
360 3
                        $key = $this->getModelKey($primaryModel[$primaryName], $relation->link);
361 3
                        $primaryModels[$i][$primaryName][$name] = $buckets[$key] ?? [];
362
                    }
363
                }
364
            }
365 6
        } elseif ($this->multiple) {
366 6
            foreach ($primaryModels as $i => $primaryModel) {
367 6
                foreach ($primaryModel[$primaryName] as $j => $m) {
368 6
                    if ($m instanceof ActiveRecordInterface) {
369 6
                        $m->populateRelation($name, $primaryModel);
370
                    } else {
371 6
                        $primaryModels[$i][$primaryName][$j][$name] = $primaryModel;
372
                    }
373
                }
374
            }
375
        } else {
376
            foreach ($primaryModels as $i => $primaryModel) {
377
                if ($primaryModels[$i][$primaryName] instanceof ActiveRecordInterface) {
378
                    $primaryModels[$i][$primaryName]->populateRelation($name, $primaryModel);
379
                } elseif (!empty($primaryModels[$i][$primaryName])) {
380
                    $primaryModels[$i][$primaryName][$name] = $primaryModel;
381
                }
382
            }
383
        }
384 9
    }
385
386 69
    private function buildBuckets(array $models, array $link, array $viaModels = null, ?self $viaQuery = null, bool $checkMultiple = true): array
387
    {
388 69
        if ($viaModels !== null) {
389 39
            $map = [];
390 39
            $viaLink = $viaQuery->link;
391 39
            $viaLinkKeys = \array_keys($viaLink);
392 39
            $linkValues = \array_values($link);
393
394 39
            foreach ($viaModels as $viaModel) {
395 39
                $key1 = $this->getModelKey($viaModel, $viaLinkKeys);
396 39
                $key2 = $this->getModelKey($viaModel, $linkValues);
397 39
                $map[$key2][$key1] = true;
398
            }
399
400 39
            $viaQuery->viaMap = $map;
401
402 39
            $viaVia = $viaQuery->via;
403 39
            while ($viaVia) {
404 3
                $viaViaQuery = \is_array($viaVia) ? $viaVia[1] : $viaVia;
405 3
                $map = $this->mapVia($map, $viaViaQuery->viaMap);
406
407 3
                $viaVia = $viaViaQuery->via;
408
            }
409
        }
410
411 69
        $buckets = [];
412 69
        $linkKeys = \array_keys($link);
413
414 69
        if (isset($map)) {
415 39
            foreach ($models as $model) {
416 39
                $key = $this->getModelKey($model, $linkKeys);
417 39
                if (isset($map[$key])) {
418 39
                    foreach (\array_keys($map[$key]) as $key2) {
419 39
                        $buckets[$key2][] = $model;
420
                    }
421
                }
422
            }
423
        } else {
424 69
            foreach ($models as $model) {
425 69
                $key = $this->getModelKey($model, $linkKeys);
426 69
                $buckets[$key][] = $model;
427
            }
428
        }
429
430 69
        if ($checkMultiple && !$this->multiple) {
431 24
            foreach ($buckets as $i => $bucket) {
432 24
                $buckets[$i] = \reset($bucket);
433
            }
434
        }
435
436 69
        return $buckets;
437
    }
438
439 3
    private function mapVia(array $map, array $viaMap): array
440
    {
441 3
        $resultMap = [];
442 3
        foreach ($map as $key => $linkKeys) {
443 3
            foreach (\array_keys($linkKeys) as $linkKey) {
444 3
                $resultMap[$key] = $viaMap[$linkKey];
445
            }
446
        }
447 3
        return $resultMap;
448
    }
449
450
    /**
451
     * Indexes buckets by column name.
452
     *
453
     * @param array $buckets
454
     * @param string|callable $indexBy the name of the column by which the query results should be indexed by. This can
455
     * also be a callable (e.g. anonymous function) that returns the index value based on the given row data.
456
     *
457
     * @return array
458
     */
459 12
    private function indexBuckets(array $buckets, $indexBy): array
460
    {
461 12
        $result = [];
462 12
        foreach ($buckets as $key => $models) {
463 12
            $result[$key] = [];
464 12
            foreach ($models as $model) {
465 12
                $index = \is_string($indexBy) ? $model[$indexBy] : $indexBy($model);
466 12
                $result[$key][$index] = $model;
467
            }
468
        }
469
470 12
        return $result;
471
    }
472
473
    /**
474
     * @param array $attributes the attributes to prefix
475
     *
476
     * @return array
477
     */
478 138
    private function prefixKeyColumns(array $attributes): array
479
    {
480 138
        if ($this instanceof ActiveQuery && (!empty($this->join) || !empty($this->joinWith))) {
0 ignored issues
show
Bug introduced by
The property joinWith is declared private in Yiisoft\ActiveRecord\ActiveQuery and cannot be accessed from this context.
Loading history...
Bug introduced by
The property join is declared protected in Yiisoft\Db\Query\Query and cannot be accessed from this context.
Loading history...
481 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...
482
                /** @var $modelClass ActiveRecord */
483 6
                $modelClass = $this->modelClass;
0 ignored issues
show
Bug introduced by
The property modelClass is declared private in Yiisoft\ActiveRecord\ActiveQuery and cannot be accessed from this context.
Loading history...
484 6
                $alias = $modelClass::tableName();
485
            } else {
486 27
                foreach ($this->from as $alias => $table) {
487 27
                    if (!\is_string($alias)) {
488 27
                        $alias = $table;
489
                    }
490 27
                    break;
491
                }
492
            }
493 27
            if (isset($alias)) {
494 27
                foreach ($attributes as $i => $attribute) {
495 27
                    $attributes[$i] = "$alias.$attribute";
496
                }
497
            }
498
        }
499
500 138
        return $attributes;
501
    }
502
503 138
    private function filterByModels(array $models): void
504
    {
505 138
        $attributes = \array_keys($this->link);
506
507 138
        $attributes = $this->prefixKeyColumns($attributes);
508
509 138
        $values = [];
510 138
        if (\count($attributes) === 1) {
511
            // single key
512 135
            $attribute = \reset($this->link);
513 135
            foreach ($models as $model) {
514 135
                if (($value = $model[$attribute]) !== null) {
515 132
                    if (\is_array($value)) {
516
                        $values = \array_merge($values, $value);
517 132
                    } elseif ($value instanceof ArrayExpression && $value->getDimension() === 1) {
518
                        $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

518
                        $values = \array_merge($values, /** @scrutinizer ignore-type */ $value->getValue());
Loading history...
519
                    } else {
520 132
                        $values[] = $value;
521
                    }
522
                }
523
            }
524 135
            if (empty($values)) {
525 135
                $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

525
                $this->/** @scrutinizer ignore-call */ 
526
                       emulateExecution();
Loading history...
526
            }
527
        } else {
528
            /**
529
             * composite keys ensure keys of $this->link are prefixed the same way as $attributes.
530
             */
531 6
            $prefixedLink = \array_combine($attributes, $this->link);
532 6
            foreach ($models as $model) {
533 6
                $v = [];
534 6
                foreach ($prefixedLink as $attribute => $link) {
535 6
                    $v[$attribute] = $model[$link];
536
                }
537 6
                $values[] = $v;
538 6
                if (empty($v)) {
539
                    $this->emulateExecution();
540
                }
541
            }
542
        }
543
544 138
        if (!empty($values)) {
545 135
            $scalarValues = [];
546 135
            $nonScalarValues = [];
547 135
            foreach ($values as $value) {
548 135
                if (\is_scalar($value)) {
549 132
                    $scalarValues[] = $value;
550
                } else {
551 6
                    $nonScalarValues[] = $value;
552
                }
553
            }
554
555 135
            $scalarValues = \array_unique($scalarValues);
556 135
            $values = \array_merge($scalarValues, $nonScalarValues);
557
        }
558
559 138
        $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

559
        $this->/** @scrutinizer ignore-call */ 
560
               andWhere(['in', $attributes, $values]);
Loading history...
560 138
    }
561
562
    /**
563
     * @param ActiveRecordInterface|array $model
564
     * @param array $attributes
565
     *
566
     * @return int|string
567
     */
568 69
    private function getModelKey($model, array $attributes)
569
    {
570 69
        $key = [];
571
572 69
        foreach ($attributes as $attribute) {
573 69
            $key[] = $this->normalizeModelKey($model[$attribute]);
574
        }
575
576 69
        if (\count($key) > 1) {
577
            return \serialize($key);
578
        }
579
580 69
        $key = \reset($key);
581
582 69
        return \is_scalar($key) ? $key : \serialize($key);
583
    }
584
585
    /**
586
     * @param mixed $value raw key value.
587
     *
588
     * @return int|string normalized key value.
589
     */
590 69
    private function normalizeModelKey($value)
591
    {
592 69
        if (\is_object($value) && \method_exists($value, '__toString')) {
593
            /**
594
             * ensure matching to special objects, which are convertible to string, for cross-DBMS relations,
595
             * for example: `|MongoId`
596
             */
597
            $value = $value->__toString();
598
        }
599
600 69
        return $value;
601
    }
602
603
    /**
604
     * @param array $primaryModels either array of AR instances or arrays.
605
     *
606
     * @return array
607
     */
608 21
    private function findJunctionRows(array $primaryModels): array
609
    {
610 21
        if (empty($primaryModels)) {
611
            return [];
612
        }
613
614 21
        $this->filterByModels($primaryModels);
615
616
        /* @var $primaryModel ActiveRecord */
617 21
        $primaryModel = reset($primaryModels);
618
619 21
        if (!$primaryModel instanceof ActiveRecordInterface) {
0 ignored issues
show
introduced by
$primaryModel is always a sub-type of Yiisoft\ActiveRecord\ActiveRecordInterface.
Loading history...
620
            /** when primaryModels are array of arrays (asArray case) */
621
            $primaryModel = $this->modelClass;
0 ignored issues
show
Unused Code introduced by
The assignment to $primaryModel is dead and can be removed.
Loading history...
622
        }
623
624 21
        return $this->asArray()->all();
625
    }
626
627
    /**
628
     * @return bool whether this query represents a relation to more than one record.
629
     *
630
     * This property is only used in relational context. If true, this relation will populate all query results into AR
631
     * instances using {@see Query::all()|all()}. If false, only the first row of the results will be retrieved using
632
     * {@see Query::one()|one()}.
633
     */
634 24
    public function getMultiple(): bool
635
    {
636 24
        return $this->multiple;
637
    }
638
639
    /**
640
     * @return ActiveRecord the primary model of a relational query.
641
     *
642
     * This is used only in lazy loading with dynamic query options.
643
     */
644
    public function getPrimaryModel(): ActiveRecord
645
    {
646
        return $this->primaryModel;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->primaryModel could return the type null which is incompatible with the type-hinted return Yiisoft\ActiveRecord\ActiveRecord. Consider adding an additional type-check to rule them out.
Loading history...
647
    }
648
649
    /**
650
     * @return array the columns of the primary and foreign tables that establish a relation.
651
     *
652
     * The array keys must be columns of the table for this relation, and the array values must be the corresponding
653
     * columns from the primary table.
654
     *
655
     * Do not prefix or quote the column names as this will be done automatically by Yii. This property is only used in
656
     * relational context.
657
     */
658 78
    public function getLink(): array
659
    {
660 78
        return $this->link;
661
    }
662
663
    /**
664
     * @return array|object the query associated with the junction table. Please call {@see via()} to set this property
665
     * instead of directly setting it.
666
     *
667
     * This property is only used in relational context.
668
     *
669
     * {@see via()}
670
     */
671 81
    public function getVia()
672
    {
673 81
        return $this->via;
674
    }
675
676
    /**
677
     * @return string the name of the relation that is the inverse of this relation.
678
     *
679
     * For example, an order has a customer, which means the inverse of the "customer" relation is the "orders", and the
680
     * inverse of the "orders" relation is the "customer". If this property is set, the primary record(s) will be
681
     * referenced through the specified relation.
682
     *
683
     * For example, `$customer->orders[0]->customer` and `$customer` will be the same object, and accessing the customer
684
     * of an order will not trigger new DB query.
685
     *
686
     * This property is only used in relational context.
687
     *
688
     * {@see inverseOf()}
689
     */
690
    public function getInverseOf(): string
691
    {
692
        return $this->inverseOf;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->inverseOf could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
693
    }
694
695
    public function getViaMap()
696
    {
697
        return $this->viaMap;
698
    }
699
700 144
    public function multiple(bool $value): self
701
    {
702 144
        $this->multiple = $value;
703
704 144
        return $this;
705
    }
706
707 141
    public function primaryModel(ActiveRecord $value): self
708
    {
709 141
        $this->primaryModel = $value;
710
711 141
        return $this;
712
    }
713
714 144
    public function link(array $value): self
715
    {
716 144
        $this->link = $value;
717
718 144
        return $this;
719
    }
720
}
721