Passed
Push — master ( ab6a5b...75b09b )
by Alexander
05:44
created

ActiveRelationTrait::addInverseRelations()   B

Complexity

Conditions 8
Paths 8

Size

Total Lines 22
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 8.0231

Importance

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

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

104
        /** @scrutinizer ignore-call */ 
105
        $relation = $this->primaryModel->getRelation($relationName);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

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

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

224
                    $inverseRelation = $this->/** @scrutinizer ignore-call */ getARInstance()->getRelation($this->inverseOf);
Loading history...
225
                }
226
227 8
                $result[$i][$this->inverseOf] = $inverseRelation->multiple
228 8
                    ? [$this->primaryModel] : $this->primaryModel;
229
            }
230
        }
231 16
    }
232
233
    /**
234
     * Finds the related records and populates them into the primary models.
235
     *
236
     * @param string $name the relation name
237
     * @param array $primaryModels primary models
238
     *
239
     * @return array the related models
240
     * @throws InvalidArgumentException|NotSupportedException|Throwable|InvalidConfigException if {@see link()} is
241
     * invalid.
242
     *
243
     * @throws Exception
244
     */
245 139
    public function populateRelation(string $name, array &$primaryModels): array
246
    {
247 139
        if (!is_array($this->link)) {
0 ignored issues
show
introduced by
The condition is_array($this->link) is always true.
Loading history...
248
            throw new InvalidConfigException('Invalid link: it must be an array of key-value pairs.');
249
        }
250
251 139
        if ($this->via instanceof self) {
252
            /**
253
             * via junction table
254
             *
255
             * @var $viaQuery ActiveRelationTrait
256
             */
257 12
            $viaQuery = $this->via;
258 12
            $viaModels = $viaQuery->findJunctionRows($primaryModels);
259 12
            $this->filterByModels($viaModels);
260 139
        } elseif (is_array($this->via)) {
261
            /**
262
             * via relation
263
             *
264
             * @var $viaQuery ActiveRelationTrait|ActiveQueryTrait
265
             */
266 72
            [$viaName, $viaQuery] = $this->via;
267
268 72
            if ($viaQuery->asArray === null) {
269
                /** inherit asArray from primary query */
270 72
                $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
273 72
            $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...
274 72
            $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

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

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

303
        $this->/** @scrutinizer ignore-call */ 
304
               indexBy(null);
Loading history...
304 123
        $models = $this->all();
305
306 123
        if (isset($viaModels, $viaQuery)) {
307 72
            $buckets = $this->buildBuckets($models, $this->link, $viaModels, $viaQuery);
308
        } else {
309 123
            $buckets = $this->buildBuckets($models, $this->link);
310
        }
311
312 123
        $this->indexBy($indexBy);
313
314 123
        if ($this->getIndexBy() !== null && $this->multiple) {
315 17
            $buckets = $this->indexBuckets($buckets, $this->getIndexBy());
316
        }
317
318 123
        $link = array_values($this->link);
319 123
        if (isset($viaQuery)) {
320 72
            $deepViaQuery = $viaQuery;
321
322 72
            while ($deepViaQuery->via) {
323 5
                $deepViaQuery = is_array($deepViaQuery->via) ? $deepViaQuery->via[1] : $deepViaQuery->via;
324
            }
325
326 72
            $link = array_values($deepViaQuery->link);
327
        }
328
329 123
        foreach ($primaryModels as $i => $primaryModel) {
330 123
            if ($this->multiple && count($link) === 1 && is_array($keys = $primaryModel[reset($link)])) {
331
                $value = [];
332
                foreach ($keys as $key) {
333
                    $key = $this->normalizeModelKey($key);
334
                    if (isset($buckets[$key])) {
335
                        if ($this->getIndexBy() !== null) {
336
                            /** if indexBy is set, array_merge will cause renumbering of numeric array */
337
                            foreach ($buckets[$key] as $bucketKey => $bucketValue) {
338
                                $value[$bucketKey] = $bucketValue;
339
                            }
340
                        } else {
341
                            $value = array_merge($value, $buckets[$key]);
342
                        }
343
                    }
344
                }
345
            } else {
346 123
                $key = $this->getModelKey($primaryModel, $link);
347 123
                $value = $buckets[$key] ?? ($this->multiple ? [] : null);
348
            }
349
350 123
            if ($primaryModel instanceof ActiveRecordInterface) {
351 123
                $primaryModel->populateRelation($name, $value);
352
            } else {
353 9
                $primaryModels[$i][$name] = $value;
354
            }
355
        }
356 123
        if ($this->inverseOf !== null) {
357 8
            $this->populateInverseRelation($primaryModels, $models, $name, $this->inverseOf);
358
        }
359
360 123
        return $models;
361
    }
362
363 12
    private function populateInverseRelation(
364
        array &$primaryModels,
365
        array $models,
366
        string $primaryName,
367
        string $name
368
    ): void {
369 12
        if (empty($models) || empty($primaryModels)) {
370
            return;
371
        }
372 12
        $model = reset($models);
373
374
        /** @var $relation ActiveQueryInterface|ActiveQuery */
375 12
        if ($model instanceof ActiveRecordInterface) {
376 12
            $relation = $model->getRelation($name);
377
        } else {
378 4
            $relation = $this->getARInstance()->getRelation($name);
379
        }
380
381 12
        if ($relation->multiple) {
382 8
            $buckets = $this->buildBuckets($primaryModels, $relation->link, null, null, false);
383 8
            if ($model instanceof ActiveRecordInterface) {
384 8
                foreach ($models as $model) {
385 8
                    $key = $this->getModelKey($model, $relation->link);
386 8
                    $model->populateRelation($name, $buckets[$key] ?? []);
387
                }
388
            } else {
389 8
                foreach ($primaryModels as $i => $primaryModel) {
390 4
                    if ($this->multiple) {
391
                        foreach ($primaryModel as $j => $m) {
392
                            $key = $this->getModelKey($m, $relation->link);
393
                            $primaryModels[$i][$j][$name] = $buckets[$key] ?? [];
394
                        }
395 4
                    } elseif (!empty($primaryModel[$primaryName])) {
396 4
                        $key = $this->getModelKey($primaryModel[$primaryName], $relation->link);
397 4
                        $primaryModels[$i][$primaryName][$name] = $buckets[$key] ?? [];
398
                    }
399
                }
400
            }
401 8
        } elseif ($this->multiple) {
402 8
            foreach ($primaryModels as $i => $primaryModel) {
403 8
                foreach ($primaryModel[$primaryName] as $j => $m) {
404 8
                    if ($m instanceof ActiveRecordInterface) {
405 8
                        $m->populateRelation($name, $primaryModel);
406
                    } else {
407 4
                        $primaryModels[$i][$primaryName][$j][$name] = $primaryModel;
408
                    }
409
                }
410
            }
411
        } else {
412
            foreach ($primaryModels as $i => $primaryModel) {
413
                if ($primaryModels[$i][$primaryName] instanceof ActiveRecordInterface) {
414
                    $primaryModels[$i][$primaryName]->populateRelation($name, $primaryModel);
415
                } elseif (!empty($primaryModels[$i][$primaryName])) {
416
                    $primaryModels[$i][$primaryName][$name] = $primaryModel;
417
                }
418
            }
419
        }
420 12
    }
421
422 127
    private function buildBuckets(
423
        array $models,
424
        array $link,
425
        array $viaModels = null,
426
        ?self $viaQuery = null,
427
        bool $checkMultiple = true
428
    ): array {
429 127
        if ($viaModels !== null) {
430 72
            $map = [];
431 72
            $viaLink = $viaQuery->link;
432 72
            $viaLinkKeys = array_keys($viaLink);
433 72
            $linkValues = array_values($link);
434
435 72
            foreach ($viaModels as $viaModel) {
436 71
                $key1 = $this->getModelKey($viaModel, $viaLinkKeys);
437 71
                $key2 = $this->getModelKey($viaModel, $linkValues);
438 71
                $map[$key2][$key1] = true;
439
            }
440
441 72
            $viaQuery->viaMap = $map;
0 ignored issues
show
Bug Best Practice introduced by
The property viaMap does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
442
443 72
            $viaVia = $viaQuery->via;
444 72
            while ($viaVia) {
445 5
                $viaViaQuery = is_array($viaVia) ? $viaVia[1] : $viaVia;
446 5
                $map = $this->mapVia($map, $viaViaQuery->viaMap);
447
448 5
                $viaVia = $viaViaQuery->via;
449
            }
450
        }
451
452 127
        $buckets = [];
453 127
        $linkKeys = array_keys($link);
454
455 127
        if (isset($map)) {
456 72
            foreach ($models as $model) {
457 71
                $key = $this->getModelKey($model, $linkKeys);
458 71
                if (isset($map[$key])) {
459 71
                    foreach (array_keys($map[$key]) as $key2) {
460 71
                        $buckets[$key2][] = $model;
461
                    }
462
                }
463
            }
464
        } else {
465 127
            foreach ($models as $model) {
466 127
                $key = $this->getModelKey($model, $linkKeys);
467 127
                $buckets[$key][] = $model;
468
            }
469
        }
470
471 127
        if ($checkMultiple && !$this->multiple) {
472 56
            foreach ($buckets as $i => $bucket) {
473 56
                $buckets[$i] = reset($bucket);
474
            }
475
        }
476
477 127
        return $buckets;
478
    }
479
480 5
    private function mapVia(array $map, array $viaMap): array
481
    {
482 5
        $resultMap = [];
483
484 5
        foreach ($map as $key => $linkKeys) {
485 5
            foreach (array_keys($linkKeys) as $linkKey) {
486 5
                $resultMap[$key] = $viaMap[$linkKey];
487
            }
488
        }
489
490 5
        return $resultMap;
491
    }
492
493
    /**
494
     * Indexes buckets by column name.
495
     *
496
     * @param array $buckets
497
     * @param string|callable $indexBy the name of the column by which the query results should be indexed by. This can
498
     * also be a callable (e.g. anonymous function) that returns the index value based on the given row data.
499
     *
500
     * @return array
501
     */
502 17
    private function indexBuckets(array $buckets, $indexBy): array
503
    {
504 17
        $result = [];
505
506 17
        foreach ($buckets as $key => $models) {
507 17
            $result[$key] = [];
508 17
            foreach ($models as $model) {
509 17
                $index = is_string($indexBy) ? $model[$indexBy] : $indexBy($model);
510 17
                $result[$key][$index] = $model;
511
            }
512
        }
513
514 17
        return $result;
515
    }
516
517
    /**
518
     * @param array $attributes the attributes to prefix.
519
     *
520
     * @return array
521
     */
522 235
    private function prefixKeyColumns(array $attributes): array
523
    {
524 235
        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...
525 36
            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...
526 12
                $alias = $this->getARInstance()->tableName();
0 ignored issues
show
Bug introduced by
The method tableName() does not exist on Yiisoft\ActiveRecord\ActiveRecordInterface. It seems like you code against a sub-type of Yiisoft\ActiveRecord\ActiveRecordInterface such as Yiisoft\ActiveRecord\ActiveRecord. ( Ignorable by Annotation )

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

526
                $alias = $this->getARInstance()->/** @scrutinizer ignore-call */ tableName();
Loading history...
527
            } else {
528 32
                foreach ($this->from as $alias => $table) {
529 32
                    if (!is_string($alias)) {
530 32
                        $alias = $table;
531
                    }
532 32
                    break;
533
                }
534
            }
535
536 36
            if (isset($alias)) {
537 36
                foreach ($attributes as $i => $attribute) {
538 36
                    $attributes[$i] = "$alias.$attribute";
539
                }
540
            }
541
        }
542
543 235
        return $attributes;
544
    }
545
546 235
    protected function filterByModels(array $models): void
547
    {
548 235
        $attributes = array_keys($this->link);
549
550 235
        $attributes = $this->prefixKeyColumns($attributes);
551
552 235
        $values = [];
553 235
        if (count($attributes) === 1) {
554
            /** single key */
555 231
            $attribute = reset($this->link);
556 231
            foreach ($models as $model) {
557 231
                if (($value = $model[$attribute]) !== null) {
558 227
                    if (is_array($value)) {
559
                        $values = array_merge($values, $value);
560 227
                    } elseif ($value instanceof ArrayExpression && $value->getDimension() === 1) {
561
                        $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

561
                        $values = array_merge($values, /** @scrutinizer ignore-type */ $value->getValue());
Loading history...
562
                    } else {
563 227
                        $values[] = $value;
564
                    }
565
                }
566
            }
567
568 231
            if (empty($values)) {
569 231
                $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

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

607
        $this->/** @scrutinizer ignore-call */ 
608
               andWhere(['in', $attributes, $values]);
Loading history...
608 235
    }
609
610
    /**
611
     * @param ActiveRecordInterface|array $model
612
     * @param array $attributes
613
     *
614
     * @return false|int|string
615
     */
616 127
    private function getModelKey($model, array $attributes)
617
    {
618 127
        $key = [];
619
620 127
        foreach ($attributes as $attribute) {
621 127
            $key[] = $this->normalizeModelKey($model[$attribute]);
622
        }
623
624 127
        if (count($key) > 1) {
625
            return serialize($key);
626
        }
627
628 127
        $key = reset($key);
629
630 127
        return is_scalar($key) ? $key : serialize($key);
631
    }
632
633
    /**
634
     * @param mixed $value raw key value.
635
     *
636
     * @return int|string normalized key value.
637
     */
638 127
    private function normalizeModelKey($value)
639
    {
640 127
        if (is_object($value) && method_exists($value, '__toString')) {
641
            /**
642
             * ensure matching to special objects, which are convertible to string, for cross-DBMS relations,
643
             * for example: `|MongoId`
644
             */
645
            $value = $value->__toString();
646
        }
647
648 127
        return $value;
649
    }
650
651
    /**
652
     * @param array $primaryModels either array of AR instances or arrays.
653
     *
654
     * @return array
655
     */
656 28
    private function findJunctionRows(array $primaryModels): array
657
    {
658 28
        if (empty($primaryModels)) {
659
            return [];
660
        }
661
662 28
        $this->filterByModels($primaryModels);
663
664
        /* @var $primaryModel ActiveRecord */
665 28
        $primaryModel = reset($primaryModels);
666
667 28
        if (!$primaryModel instanceof ActiveRecordInterface) {
0 ignored issues
show
introduced by
$primaryModel is always a sub-type of Yiisoft\ActiveRecord\ActiveRecordInterface.
Loading history...
668
            /** when primaryModels are array of arrays (asArray case) */
669
            $primaryModel = $this->modelClass;
0 ignored issues
show
Unused Code introduced by
The assignment to $primaryModel is dead and can be removed.
Loading history...
670
        }
671
672 28
        return $this->asArray()->all();
673
    }
674
675
    /**
676
     * @return bool whether this query represents a relation to more than one record.
677
     *
678
     * This property is only used in relational context. If true, this relation will populate all query results into
679
     * active record instances using {@see Query::all()|all()}. If false, only the first row of the results will be
680
     * retrieved using {@see Query::one()|one()}.
681
     */
682 39
    public function getMultiple(): bool
683
    {
684 39
        return $this->multiple;
685
    }
686
687
    /**
688
     * @return ActiveRecordInterface the primary model of a relational query.
689
     *
690
     * This is used only in lazy loading with dynamic query options.
691
     */
692 63
    public function getPrimaryModel(): ?ActiveRecordInterface
693
    {
694 63
        return $this->primaryModel;
695
    }
696
697
    /**
698
     * @return array the columns of the primary and foreign tables that establish a relation.
699
     *
700
     * The array keys must be columns of the table for this relation, and the array values must be the corresponding
701
     * columns from the primary table.
702
     *
703
     * Do not prefix or quote the column names as this will be done automatically by Yii. This property is only used in
704
     * relational context.
705
     */
706 112
    public function getLink(): array
707
    {
708 112
        return $this->link;
709
    }
710
711
    /**
712
     * @return array|object the query associated with the junction table. Please call {@see via()} to set this property
713
     * instead of directly setting it.
714
     *
715
     * This property is only used in relational context.
716
     *
717
     * {@see via()}
718
     */
719 116
    public function getVia()
720
    {
721 116
        return $this->via;
722
    }
723
724 251
    public function multiple(bool $value): self
725
    {
726 251
        $this->multiple = $value;
727
728 251
        return $this;
729
    }
730
731 251
    public function primaryModel(ActiveRecordInterface $value): self
732
    {
733 251
        $this->primaryModel = $value;
734
735 251
        return $this;
736
    }
737
738 251
    public function link(array $value): self
739
    {
740 251
        $this->link = $value;
741
742 251
        return $this;
743
    }
744
}
745