Completed
Push — inverseof ( 0f640c )
by Carsten
09:43
created

ActiveRelationTrait::findFor()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4.0312

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 12
ccs 7
cts 8
cp 0.875
rs 9.2
cc 4
eloc 7
nc 5
nop 2
crap 4.0312
1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\db;
9
10
use yii\base\InvalidConfigException;
11
use yii\base\InvalidParamException;
12
13
/**
14
 * ActiveRelationTrait implements the common methods and properties for active record relational queries.
15
 *
16
 * @author Qiang Xue <[email protected]>
17
 * @author Carsten Brandt <[email protected]>
18
 * @since 2.0
19
 *
20
 * @method ActiveRecordInterface one()
21
 * @method ActiveRecordInterface[] all()
22
 * @property ActiveRecord $modelClass
23
 */
24
trait ActiveRelationTrait
25
{
26
    /**
27
     * @var boolean whether this query represents a relation to more than one record.
28
     * This property is only used in relational context. If true, this relation will
29
     * populate all query results into AR instances using [[Query::all()|all()]].
30
     * If false, only the first row of the results will be retrieved using [[Query::one()|one()]].
31
     */
32
    public $multiple;
33
    /**
34
     * @var ActiveRecord the primary model of a relational query.
35
     * This is used only in lazy loading with dynamic query options.
36
     */
37
    public $primaryModel;
38
    /**
39
     * @var array the columns of the primary and foreign tables that establish a relation.
40
     * The array keys must be columns of the table for this relation, and the array values
41
     * must be the corresponding columns from the primary table.
42
     * Do not prefix or quote the column names as this will be done automatically by Yii.
43
     * This property is only used in relational context.
44
     */
45
    public $link;
46
    /**
47
     * @var array|object the query associated with the junction table. Please call [[via()]]
48
     * to set this property instead of directly setting it.
49
     * This property is only used in relational context.
50
     * @see via()
51
     */
52
    public $via;
53
    /**
54
     * @var string the name of the relation that is the inverse of this relation.
55
     * For example, an order has a customer, which means the inverse of the "customer" relation
56
     * is the "orders", and the inverse of the "orders" relation is the "customer".
57
     * If this property is set, the primary record(s) will be referenced through the specified relation.
58
     * For example, `$customer->orders[0]->customer` and `$customer` will be the same object,
59
     * and accessing the customer of an order will not trigger new DB query.
60
     * This property is only used in relational context.
61
     * @see inverseOf()
62
     */
63
    public $inverseOf;
64
65
66
    /**
67
     * Clones internal objects.
68
     */
69 12
    public function __clone()
70
    {
71 12
        parent::__clone();
72
        // make a clone of "via" object so that the same query object can be reused multiple times
73 12
        if (is_object($this->via)) {
74
            $this->via = clone $this->via;
75 12
        } elseif (is_array($this->via)) {
76 6
            $this->via = [$this->via[0], clone $this->via[1]];
77 6
        }
78 12
    }
79
80
    /**
81
     * Specifies the relation associated with the junction table.
82
     *
83
     * Use this method to specify a pivot record/table when declaring a relation in the [[ActiveRecord]] class:
84
     *
85
     * ```php
86
     * public function getOrders()
87
     * {
88
     *     return $this->hasOne(Order::className(), ['id' => 'order_id']);
89
     * }
90
     *
91
     * public function getOrderItems()
92
     * {
93
     *     return $this->hasMany(Item::className(), ['id' => 'item_id'])
94
     *                 ->via('orders');
95
     * }
96
     * ```
97
     *
98
     * @param string $relationName the relation name. This refers to a relation declared in [[primaryModel]].
99
     * @param callable $callable a PHP callback for customizing the relation associated with the junction table.
100
     * Its signature should be `function($query)`, where `$query` is the query to be customized.
101
     * @return $this the relation object itself.
102
     */
103 60
    public function via($relationName, callable $callable = null)
104
    {
105 60
        $relation = $this->primaryModel->getRelation($relationName);
106 60
        $this->via = [$relationName, $relation];
107 60
        if ($callable !== null) {
108 42
            call_user_func($callable, $relation);
109 42
        }
110 60
        return $this;
111
    }
112
113
    /**
114
     * Sets the name of the relation that is the inverse of this relation.
115
     * For example, an order has a customer, which means the inverse of the "customer" relation
116
     * is the "orders", and the inverse of the "orders" relation is the "customer".
117
     * If this property is set, the primary record(s) will be referenced through the specified relation.
118
     * For example, `$customer->orders[0]->customer` and `$customer` will be the same object,
119
     * and accessing the customer of an order will not trigger a new DB query.
120
     *
121
     * Use this method when declaring a relation in the [[ActiveRecord]] class:
122
     *
123
     * ```php
124
     * public function getOrders()
125
     * {
126
     *     return $this->hasMany(Order::className(), ['customer_id' => 'id'])->inverseOf('customer');
127
     * }
128
     * ```
129
     *
130
     * @param string $relationName the name of the relation that is the inverse of this relation.
131
     * @return $this the relation object itself.
132
     */
133 6
    public function inverseOf($relationName)
134
    {
135 6
        $this->inverseOf = $relationName;
136 6
        return $this;
137
    }
138
139
    /**
140
     * Finds the related records for the specified primary record.
141
     * This method is invoked when a relation of an ActiveRecord is being accessed in a lazy fashion.
142
     * @param string $name the relation name
143
     * @param ActiveRecordInterface|BaseActiveRecord $model the primary model
144
     * @return mixed the related record(s)
145
     * @throws InvalidParamException if the relation is invalid
146
     */
147 48
    public function findFor($name, $model)
148
    {
149 48
        if (method_exists($model, 'get' . $name)) {
150 48
            $method = new \ReflectionMethod($model, 'get' . $name);
151 48
            $realName = lcfirst(substr($method->getName(), 3));
152 48
            if ($realName !== $name) {
153
                throw new InvalidParamException('Relation names are case sensitive. ' . get_class($model) . " has a relation named \"$realName\" instead of \"$name\".");
154
            }
155 48
        }
156
157 48
        return $this->multiple ? $this->all() : $this->one();
158
    }
159
160
    /**
161
     * If applicable, populate the query's primary model into the related records' inverse relationship
162
     * @param array $result the array of related records as generated by [[populate()]]
163
     */
164 6
    private function addInverseRelations(&$result)
165
    {
166 6
        if ($this->inverseOf === null) {
167
            return;
168
        }
169
170 6
        foreach ($result as $i => $relatedModel) {
171 6
            if ($relatedModel instanceof ActiveRecordInterface) {
172 6
                if (!isset($inverseRelation)) {
173 6
                    $inverseRelation = $relatedModel->getRelation($this->inverseOf);
174 6
                }
175 6
                $relatedModel->populateRelation($this->inverseOf, $inverseRelation->multiple ? [$this->primaryModel] : $this->primaryModel);
176 6
            } else {
177 6
                if (!isset($inverseRelation)) {
178 6
                    $inverseRelation = (new $this->modelClass)->getRelation($this->inverseOf);
179 6
                }
180 6
                $result[$i][$this->inverseOf] = $inverseRelation->multiple ? [$this->primaryModel] : $this->primaryModel;
181
            }
182 6
        }
183 6
    }
184
185
    /**
186
     * Finds the related records and populates them into the primary models.
187
     * @param string $name the relation name
188
     * @param array $primaryModels primary models
189
     * @return array the related models
190
     * @throws InvalidConfigException if [[link]] is invalid
191
     */
192 63
    public function populateRelation($name, &$primaryModels)
193
    {
194 63
        if (!is_array($this->link)) {
195
            throw new InvalidConfigException('Invalid link: it must be an array of key-value pairs.');
196
        }
197
198 63
        if ($this->via instanceof self) {
199
            // via junction table
200
            /* @var $viaQuery ActiveRelationTrait */
201 9
            $viaQuery = $this->via;
202 9
            $viaModels = $viaQuery->findJunctionRows($primaryModels);
203 9
            $this->filterByModels($viaModels);
204 63
        } elseif (is_array($this->via)) {
205
            // via relation
206
            /* @var $viaQuery ActiveRelationTrait|ActiveQueryTrait */
207 33
            list($viaName, $viaQuery) = $this->via;
208 33
            if ($viaQuery->asArray === null) {
209
                // inherit asArray from primary query
210 33
                $viaQuery->asArray($this->asArray);
0 ignored issues
show
Bug introduced by
The property asArray does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
Bug introduced by
The method asArray does only exist in yii\db\ActiveQueryTrait, but not in yii\db\ActiveRelationTrait.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
211 33
            }
212 33
            $viaQuery->primaryModel = null;
213 33
            $viaModels = $viaQuery->populateRelation($viaName, $primaryModels);
0 ignored issues
show
Bug introduced by
The method populateRelation does only exist in yii\db\ActiveRelationTrait, but not in yii\db\ActiveQueryTrait.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
214 33
            $this->filterByModels($viaModels);
215 33
        } else {
216 63
            $this->filterByModels($primaryModels);
217
        }
218
219 63
        if (!$this->multiple && count($primaryModels) === 1) {
220 18
            $model = $this->one();
221 18
            foreach ($primaryModels as $i => $primaryModel) {
222 18
                if ($primaryModel instanceof ActiveRecordInterface) {
223 18
                    $primaryModel->populateRelation($name, $model);
224 18
                } else {
225 3
                    $primaryModels[$i][$name] = $model;
226
                }
227 18
                if ($this->inverseOf !== null) {
228 6
                    $this->populateInverseRelation($primaryModels, [$model], $name, $this->inverseOf);
229 6
                }
230 18
            }
231
232 18
            return [$model];
233
        } else {
234
            // https://github.com/yiisoft/yii2/issues/3197
235
            // delay indexing related models after buckets are built
236 54
            $indexBy = $this->indexBy;
0 ignored issues
show
Bug introduced by
The property indexBy does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
237 54
            $this->indexBy = null;
238 54
            $models = $this->all();
239
240 54
            if (isset($viaModels, $viaQuery)) {
241 33
                $buckets = $this->buildBuckets($models, $this->link, $viaModels, $viaQuery->link);
242 33
            } else {
243 54
                $buckets = $this->buildBuckets($models, $this->link);
244
            }
245
246 54
            $this->indexBy = $indexBy;
247 54
            if ($this->indexBy !== null && $this->multiple) {
248 15
                $buckets = $this->indexBuckets($buckets, $this->indexBy);
249 15
            }
250
251 54
            $link = array_values(isset($viaQuery) ? $viaQuery->link : $this->link);
252 54
            foreach ($primaryModels as $i => $primaryModel) {
253 54
                if ($this->multiple && count($link) === 1 && is_array($keys = $primaryModel[reset($link)])) {
254
                    $value = [];
255
                    foreach ($keys as $key) {
256
                        $key = $this->normalizeModelKey($key);
257
                        if (isset($buckets[$key])) {
258
                            if ($this->indexBy !== null) {
259
                                // if indexBy is set, array_merge will cause renumbering of numeric array
260
                                foreach ($buckets[$key] as $bucketKey => $bucketValue) {
261
                                    $value[$bucketKey] = $bucketValue;
262
                                }
263
                            } else {
264
                                $value = array_merge($value, $buckets[$key]);
265
                            }
266
                        }
267
                    }
268
                } else {
269 54
                    $key = $this->getModelKey($primaryModel, $link);
270 54
                    $value = isset($buckets[$key]) ? $buckets[$key] : ($this->multiple ? [] : null);
271
                }
272 54
                if ($primaryModel instanceof ActiveRecordInterface) {
273 54
                    $primaryModel->populateRelation($name, $value);
274 54
                } else {
275 6
                    $primaryModels[$i][$name] = $value;
276
                }
277 54
            }
278 54
            if ($this->inverseOf !== null) {
279 3
                $this->populateInverseRelation($primaryModels, $models, $name, $this->inverseOf);
280 3
            }
281
282 54
            return $models;
283
        }
284
    }
285
286
    /**
287
     * @param ActiveRecordInterface[] $primaryModels primary models
288
     * @param ActiveRecordInterface[] $models models
289
     * @param string $primaryName the primary relation name
290
     * @param string $name the relation name
291
     */
292 6
    private function populateInverseRelation(&$primaryModels, $models, $primaryName, $name)
293
    {
294 6
        if (empty($models) || empty($primaryModels)) {
295
            return;
296
        }
297 6
        $model = reset($models);
298
        /* @var $relation ActiveQueryInterface|ActiveQuery */
299 6
        $relation = $model instanceof ActiveRecordInterface ? $model->getRelation($name) : (new $this->modelClass)->getRelation($name);
300
301 6
        if ($relation->multiple) {
0 ignored issues
show
Bug introduced by
Accessing multiple on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
302 6
            $buckets = $this->buildBuckets($primaryModels, $relation->link, null, null, false);
0 ignored issues
show
Bug introduced by
Accessing link on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
303 6
            if ($model instanceof ActiveRecordInterface) {
304 6
                foreach ($models as $model) {
305 6
                    $key = $this->getModelKey($model, $relation->link);
0 ignored issues
show
Bug introduced by
Accessing link on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
306 6
                    $model->populateRelation($name, isset($buckets[$key]) ? $buckets[$key] : []);
307 6
                }
308 6
            } else {
309 3
                foreach ($primaryModels as $i => $primaryModel) {
310 3
                    if ($this->multiple) {
311
                        foreach ($primaryModel as $j => $m) {
0 ignored issues
show
Bug introduced by
The expression $primaryModel of type object<yii\db\ActiveRecordInterface> is not traversable.
Loading history...
312
                            $key = $this->getModelKey($m, $relation->link);
0 ignored issues
show
Bug introduced by
Accessing link on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
313
                            $primaryModels[$i][$j][$name] = isset($buckets[$key]) ? $buckets[$key] : [];
314
                        }
315 3
                    } elseif (!empty($primaryModel[$primaryName])) {
316 3
                        $key = $this->getModelKey($primaryModel[$primaryName], $relation->link);
0 ignored issues
show
Bug introduced by
Accessing link on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
317 3
                        $primaryModels[$i][$primaryName][$name] = isset($buckets[$key]) ? $buckets[$key] : [];
318 3
                    }
319 3
                }
320
            }
321 6
        } else {
322 3
            if ($this->multiple) {
323 3
                foreach ($primaryModels as $i => $primaryModel) {
324 3
                    foreach ($primaryModel[$primaryName] as $j => $m) {
325 3
                        if ($m instanceof ActiveRecordInterface) {
326 3
                            $m->populateRelation($name, $primaryModel);
327 3
                        } else {
328 3
                            $primaryModels[$i][$primaryName][$j][$name] = $primaryModel;
329
                        }
330 3
                    }
331 3
                }
332 3
            } else {
333
                foreach ($primaryModels as $i => $primaryModel) {
334
                    if ($primaryModels[$i][$primaryName] instanceof ActiveRecordInterface) {
335
                        $primaryModels[$i][$primaryName]->populateRelation($name, $primaryModel);
336
                    } elseif (!empty($primaryModels[$i][$primaryName])) {
337
                        $primaryModels[$i][$primaryName][$name] = $primaryModel;
338
                    }
339
                }
340
            }
341
        }
342 6
    }
343
344
    /**
345
     * @param array $models
346
     * @param array $link
347
     * @param array $viaModels
348
     * @param array $viaLink
349
     * @param boolean $checkMultiple
350
     * @return array
351
     */
352 57
    private function buildBuckets($models, $link, $viaModels = null, $viaLink = null, $checkMultiple = true)
353
    {
354 57
        if ($viaModels !== null) {
355 33
            $map = [];
356 33
            $viaLinkKeys = array_keys($viaLink);
357 33
            $linkValues = array_values($link);
358 33
            foreach ($viaModels as $viaModel) {
359 33
                $key1 = $this->getModelKey($viaModel, $viaLinkKeys);
360 33
                $key2 = $this->getModelKey($viaModel, $linkValues);
361 33
                $map[$key2][$key1] = true;
362 33
            }
363 33
        }
364
365 57
        $buckets = [];
366 57
        $linkKeys = array_keys($link);
367
368 57
        if (isset($map)) {
369 33
            foreach ($models as $model) {
370 33
                $key = $this->getModelKey($model, $linkKeys);
371 33
                if (isset($map[$key])) {
372 33
                    foreach (array_keys($map[$key]) as $key2) {
373 33
                        $buckets[$key2][] = $model;
374 33
                    }
375 33
                }
376 33
            }
377 33
        } else {
378 57
            foreach ($models as $model) {
379 57
                $key = $this->getModelKey($model, $linkKeys);
380 57
                $buckets[$key][] = $model;
381 57
            }
382
        }
383
384 57
        if ($checkMultiple && !$this->multiple) {
385 15
            foreach ($buckets as $i => $bucket) {
386 15
                $buckets[$i] = reset($bucket);
387 15
            }
388 15
        }
389
390 57
        return $buckets;
391
    }
392
393
394
    /**
395
     * Indexes buckets by column name.
396
     *
397
     * @param array $buckets
398
     * @var string|callable $column the name of the column by which the query results should be indexed by.
399
     * This can also be a callable (e.g. anonymous function) that returns the index value based on the given row data.
400
     * @return array
401
     */
402 15
    private function indexBuckets($buckets, $indexBy)
403
    {
404 15
        $result = [];
405 15
        foreach ($buckets as $key => $models) {
406 15
            $result[$key] = [];
407 15
            foreach ($models as $model) {
408 15
                $index = is_string($indexBy) ? $model[$indexBy] : call_user_func($indexBy, $model);
409 15
                $result[$key][$index] = $model;
410 15
            }
411 15
        }
412 15
        return $result;
413
    }
414
415
    /**
416
     * @param array $attributes the attributes to prefix
417
     * @return array
418
     */
419 120
    private function prefixKeyColumns($attributes)
420
    {
421 120
        if ($this instanceof ActiveQuery && (!empty($this->join) || !empty($this->joinWith))) {
422 24
            if (empty($this->from)) {
423
                /* @var $modelClass ActiveRecord */
424 6
                $modelClass = $this->modelClass;
425 6
                $alias = $modelClass::tableName();
426 6
            } else {
427 24
                foreach ($this->from as $alias => $table) {
428 24
                    if (!is_string($alias)) {
429 24
                        $alias = $table;
430 24
                    }
431 24
                    break;
432 24
                }
433
            }
434 24
            if (isset($alias)) {
435 24
                foreach ($attributes as $i => $attribute) {
436 24
                    $attributes[$i] = "$alias.$attribute";
437 24
                }
438 24
            }
439 24
        }
440 120
        return $attributes;
441
    }
442
443
    /**
444
     * @param array $models
445
     */
446 120
    private function filterByModels($models)
447
    {
448 120
        $attributes = array_keys($this->link);
449
450 120
        $attributes = $this->prefixKeyColumns($attributes);
451
452 120
        $values = [];
453 120
        if (count($attributes) === 1) {
454
            // single key
455 120
            $attribute = reset($this->link);
456 120
            foreach ($models as $model) {
457 120
                if (($value = $model[$attribute]) !== null) {
458 120
                    if (is_array($value)) {
459
                        $values = array_merge($values, $value);
460
                    } else {
461 120
                        $values[] = $value;
462
                    }
463 120
                }
464 120
            }
465 120
        } else {
466
            // composite keys
467
468
            // ensure keys of $this->link are prefixed the same way as $attributes
469 3
            $prefixedLink = array_combine(
470 3
                $attributes,
471 3
                array_values($this->link)
472 3
            );
473 3
            foreach ($models as $model) {
474 3
                $v = [];
475 3
                foreach ($prefixedLink as $attribute => $link) {
476 3
                    $v[$attribute] = $model[$link];
477 3
                }
478 3
                $values[] = $v;
479 3
            }
480
        }
481 120
        $this->andWhere(['in', $attributes, array_unique($values, SORT_REGULAR)]);
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?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
482 120
    }
483
484
    /**
485
     * @param ActiveRecordInterface|array $model
486
     * @param array $attributes
487
     * @return string
488
     */
489 57
    private function getModelKey($model, $attributes)
490
    {
491 57
        $key = [];
492 57
        foreach ($attributes as $attribute) {
493 57
            $key[] = $this->normalizeModelKey($model[$attribute]);
494 57
        }
495 57
        if (count($key) > 1) {
496
            return serialize($key);
497
        }
498 57
        $key = reset($key);
499 57
        return is_scalar($key) ? $key : serialize($key);
500
    }
501
502
    /**
503
     * @param mixed $value raw key value.
504
     * @return string normalized key value.
505
     */
506 57
    private function normalizeModelKey($value)
507
    {
508 57
        if (is_object($value) && method_exists($value, '__toString')) {
509
            // ensure matching to special objects, which are convertable to string, for cross-DBMS relations, for example: `|MongoId`
510
            $value = $value->__toString();
511
        }
512 57
        return $value;
513
    }
514
515
    /**
516
     * @param array $primaryModels either array of AR instances or arrays
517
     * @return array
518
     */
519 18
    private function findJunctionRows($primaryModels)
520
    {
521 18
        if (empty($primaryModels)) {
522
            return [];
523
        }
524 18
        $this->filterByModels($primaryModels);
525
        /* @var $primaryModel ActiveRecord */
526 18
        $primaryModel = reset($primaryModels);
527 18
        if (!$primaryModel instanceof ActiveRecordInterface) {
528
            // when primaryModels are array of arrays (asArray case)
529
            $primaryModel = $this->modelClass;
530
        }
531
532 18
        return $this->asArray()->all($primaryModel::getDb());
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?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
533
    }
534
}
535