Completed
Push — aria-required-and-aria-invalid... ( 0b9feb...e1b0cb )
by Alexander
12:52 queued 09:30
created

ActiveRelationTrait   D

Complexity

Total Complexity 108

Size/Duplication

Total Lines 519
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 85.61%

Importance

Changes 0
Metric Value
wmc 108
lcom 1
cbo 5
dl 0
loc 519
ccs 232
cts 271
cp 0.8561
rs 4.8717
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __clone() 0 10 3
A via() 0 9 2
A inverseOf() 0 5 1
A findFor() 0 12 4
B addInverseRelations() 0 20 8
F populateRelation() 0 93 26
C populateInverseRelation() 0 51 21
C buildBuckets() 0 40 11
A indexBuckets() 0 12 4
C prefixKeyColumns() 0 23 9
D filterByModels() 0 43 9
A getModelKey() 0 12 4
A normalizeModelKey() 0 8 3
A findJunctionRows() 0 15 3

How to fix   Complexity   

Complex Class

Complex classes like ActiveRelationTrait often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ActiveRelationTrait, and based on these observations, apply Extract Interface, too.

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 bool 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
     * class Order extends ActiveRecord
87
     * {
88
     *    public function getOrderItems() {
89
     *        return $this->hasMany(OrderItem::className(), ['order_id' => 'id']);
90
     *    }
91
     * 
92
     *    public function getItems() {
93
     *        return $this->hasMany(Item::className(), ['id' => 'item_id'])
94
     *                    ->via('orderItems');
95
     *    }
96
     * }
97
     * ```
98
     *
99
     * @param string $relationName the relation name. This refers to a relation declared in [[primaryModel]].
100
     * @param callable $callable a PHP callback for customizing the relation associated with the junction table.
101
     * Its signature should be `function($query)`, where `$query` is the query to be customized.
102
     * @return $this the relation object itself.
103
     */
104 60
    public function via($relationName, callable $callable = null)
105
    {
106 60
        $relation = $this->primaryModel->getRelation($relationName);
107 60
        $this->via = [$relationName, $relation];
108 60
        if ($callable !== null) {
109 42
            call_user_func($callable, $relation);
110 42
        }
111 60
        return $this;
112
    }
113
114
    /**
115
     * Sets the name of the relation that is the inverse of this relation.
116
     * For example, an order has a customer, which means the inverse of the "customer" relation
117
     * is the "orders", and the inverse of the "orders" relation is the "customer".
118
     * If this property is set, the primary record(s) will be referenced through the specified relation.
119
     * For example, `$customer->orders[0]->customer` and `$customer` will be the same object,
120
     * and accessing the customer of an order will not trigger a new DB query.
121
     *
122
     * Use this method when declaring a relation in the [[ActiveRecord]] class:
123
     *
124
     * ```php
125
     * public function getOrders()
126
     * {
127
     *     return $this->hasMany(Order::className(), ['customer_id' => 'id'])->inverseOf('customer');
128
     * }
129
     * ```
130
     *
131
     * @param string $relationName the name of the relation that is the inverse of this relation.
132
     * @return $this the relation object itself.
133
     */
134 6
    public function inverseOf($relationName)
135
    {
136 6
        $this->inverseOf = $relationName;
137 6
        return $this;
138
    }
139
140
    /**
141
     * Finds the related records for the specified primary record.
142
     * This method is invoked when a relation of an ActiveRecord is being accessed in a lazy fashion.
143
     * @param string $name the relation name
144
     * @param ActiveRecordInterface|BaseActiveRecord $model the primary model
145
     * @return mixed the related record(s)
146
     * @throws InvalidParamException if the relation is invalid
147
     */
148 52
    public function findFor($name, $model)
149
    {
150 52
        if (method_exists($model, 'get' . $name)) {
151 52
            $method = new \ReflectionMethod($model, 'get' . $name);
152 52
            $realName = lcfirst(substr($method->getName(), 3));
153 52
            if ($realName !== $name) {
154
                throw new InvalidParamException('Relation names are case sensitive. ' . get_class($model) . " has a relation named \"$realName\" instead of \"$name\".");
155
            }
156 52
        }
157
158 52
        return $this->multiple ? $this->all() : $this->one();
159
    }
160
161
    /**
162
     * If applicable, populate the query's primary model into the related records' inverse relationship
163
     * @param array $result the array of related records as generated by [[populate()]]
164
     * @since 2.0.9
165
     */
166 6
    private function addInverseRelations(&$result)
167
    {
168 6
        if ($this->inverseOf === null) {
169
            return;
170
        }
171
172 6
        foreach ($result as $i => $relatedModel) {
173 6
            if ($relatedModel instanceof ActiveRecordInterface) {
174 6
                if (!isset($inverseRelation)) {
175 6
                    $inverseRelation = $relatedModel->getRelation($this->inverseOf);
176 6
                }
177 6
                $relatedModel->populateRelation($this->inverseOf, $inverseRelation->multiple ? [$this->primaryModel] : $this->primaryModel);
178 6
            } else {
179 6
                if (!isset($inverseRelation)) {
180 6
                    $inverseRelation = (new $this->modelClass)->getRelation($this->inverseOf);
181 6
                }
182 6
                $result[$i][$this->inverseOf] = $inverseRelation->multiple ? [$this->primaryModel] : $this->primaryModel;
183
            }
184 6
        }
185 6
    }
186
187
    /**
188
     * Finds the related records and populates them into the primary models.
189
     * @param string $name the relation name
190
     * @param array $primaryModels primary models
191
     * @return array the related models
192
     * @throws InvalidConfigException if [[link]] is invalid
193
     */
194 63
    public function populateRelation($name, &$primaryModels)
195
    {
196 63
        if (!is_array($this->link)) {
197
            throw new InvalidConfigException('Invalid link: it must be an array of key-value pairs.');
198
        }
199
200 63
        if ($this->via instanceof self) {
201
            // via junction table
202
            /* @var $viaQuery ActiveRelationTrait */
203 9
            $viaQuery = $this->via;
204 9
            $viaModels = $viaQuery->findJunctionRows($primaryModels);
205 9
            $this->filterByModels($viaModels);
206 63
        } elseif (is_array($this->via)) {
207
            // via relation
208
            /* @var $viaQuery ActiveRelationTrait|ActiveQueryTrait */
209 33
            list($viaName, $viaQuery) = $this->via;
210 33
            if ($viaQuery->asArray === null) {
211
                // inherit asArray from primary query
212 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...
213 33
            }
214 33
            $viaQuery->primaryModel = null;
215 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...
216 33
            $this->filterByModels($viaModels);
217 33
        } else {
218 63
            $this->filterByModels($primaryModels);
219
        }
220
221 63
        if (!$this->multiple && count($primaryModels) === 1) {
222 18
            $model = $this->one();
223 18
            foreach ($primaryModels as $i => $primaryModel) {
224 18
                if ($primaryModel instanceof ActiveRecordInterface) {
225 18
                    $primaryModel->populateRelation($name, $model);
226 18
                } else {
227 3
                    $primaryModels[$i][$name] = $model;
228
                }
229 18
                if ($this->inverseOf !== null) {
230 6
                    $this->populateInverseRelation($primaryModels, [$model], $name, $this->inverseOf);
231 6
                }
232 18
            }
233
234 18
            return [$model];
235
        } else {
236
            // https://github.com/yiisoft/yii2/issues/3197
237
            // delay indexing related models after buckets are built
238 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...
239 54
            $this->indexBy = null;
240 54
            $models = $this->all();
241
242 54
            if (isset($viaModels, $viaQuery)) {
243 33
                $buckets = $this->buildBuckets($models, $this->link, $viaModels, $viaQuery->link);
244 33
            } else {
245 54
                $buckets = $this->buildBuckets($models, $this->link);
246
            }
247
248 54
            $this->indexBy = $indexBy;
249 54
            if ($this->indexBy !== null && $this->multiple) {
250 15
                $buckets = $this->indexBuckets($buckets, $this->indexBy);
251 15
            }
252
253 54
            $link = array_values(isset($viaQuery) ? $viaQuery->link : $this->link);
254 54
            foreach ($primaryModels as $i => $primaryModel) {
255 54
                if ($this->multiple && count($link) === 1 && is_array($keys = $primaryModel[reset($link)])) {
256
                    $value = [];
257
                    foreach ($keys as $key) {
258
                        $key = $this->normalizeModelKey($key);
259
                        if (isset($buckets[$key])) {
260
                            if ($this->indexBy !== null) {
261
                                // if indexBy is set, array_merge will cause renumbering of numeric array
262
                                foreach ($buckets[$key] as $bucketKey => $bucketValue) {
263
                                    $value[$bucketKey] = $bucketValue;
264
                                }
265
                            } else {
266
                                $value = array_merge($value, $buckets[$key]);
267
                            }
268
                        }
269
                    }
270
                } else {
271 54
                    $key = $this->getModelKey($primaryModel, $link);
272 54
                    $value = isset($buckets[$key]) ? $buckets[$key] : ($this->multiple ? [] : null);
273
                }
274 54
                if ($primaryModel instanceof ActiveRecordInterface) {
275 54
                    $primaryModel->populateRelation($name, $value);
276 54
                } else {
277 6
                    $primaryModels[$i][$name] = $value;
278
                }
279 54
            }
280 54
            if ($this->inverseOf !== null) {
281 3
                $this->populateInverseRelation($primaryModels, $models, $name, $this->inverseOf);
282 3
            }
283
284 54
            return $models;
285
        }
286
    }
287
288
    /**
289
     * @param ActiveRecordInterface[] $primaryModels primary models
290
     * @param ActiveRecordInterface[] $models models
291
     * @param string $primaryName the primary relation name
292
     * @param string $name the relation name
293
     */
294 6
    private function populateInverseRelation(&$primaryModels, $models, $primaryName, $name)
295
    {
296 6
        if (empty($models) || empty($primaryModels)) {
297
            return;
298
        }
299 6
        $model = reset($models);
300
        /* @var $relation ActiveQueryInterface|ActiveQuery */
301 6
        $relation = $model instanceof ActiveRecordInterface ? $model->getRelation($name) : (new $this->modelClass)->getRelation($name);
302
303 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...
304 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...
305 6
            if ($model instanceof ActiveRecordInterface) {
306 6
                foreach ($models as $model) {
307 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...
308 6
                    $model->populateRelation($name, isset($buckets[$key]) ? $buckets[$key] : []);
309 6
                }
310 6
            } else {
311 3
                foreach ($primaryModels as $i => $primaryModel) {
312 3
                    if ($this->multiple) {
313
                        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...
314
                            $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...
315
                            $primaryModels[$i][$j][$name] = isset($buckets[$key]) ? $buckets[$key] : [];
316
                        }
317 3
                    } elseif (!empty($primaryModel[$primaryName])) {
318 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...
319 3
                        $primaryModels[$i][$primaryName][$name] = isset($buckets[$key]) ? $buckets[$key] : [];
320 3
                    }
321 3
                }
322
            }
323 6
        } else {
324 3
            if ($this->multiple) {
325 3
                foreach ($primaryModels as $i => $primaryModel) {
326 3
                    foreach ($primaryModel[$primaryName] as $j => $m) {
327 3
                        if ($m instanceof ActiveRecordInterface) {
328 3
                            $m->populateRelation($name, $primaryModel);
329 3
                        } else {
330 3
                            $primaryModels[$i][$primaryName][$j][$name] = $primaryModel;
331
                        }
332 3
                    }
333 3
                }
334 3
            } else {
335
                foreach ($primaryModels as $i => $primaryModel) {
336
                    if ($primaryModels[$i][$primaryName] instanceof ActiveRecordInterface) {
337
                        $primaryModels[$i][$primaryName]->populateRelation($name, $primaryModel);
338
                    } elseif (!empty($primaryModels[$i][$primaryName])) {
339
                        $primaryModels[$i][$primaryName][$name] = $primaryModel;
340
                    }
341
                }
342
            }
343
        }
344 6
    }
345
346
    /**
347
     * @param array $models
348
     * @param array $link
349
     * @param array $viaModels
350
     * @param array $viaLink
351
     * @param bool $checkMultiple
352
     * @return array
353
     */
354 57
    private function buildBuckets($models, $link, $viaModels = null, $viaLink = null, $checkMultiple = true)
355
    {
356 57
        if ($viaModels !== null) {
357 33
            $map = [];
358 33
            $viaLinkKeys = array_keys($viaLink);
359 33
            $linkValues = array_values($link);
360 33
            foreach ($viaModels as $viaModel) {
361 33
                $key1 = $this->getModelKey($viaModel, $viaLinkKeys);
362 33
                $key2 = $this->getModelKey($viaModel, $linkValues);
363 33
                $map[$key2][$key1] = true;
364 33
            }
365 33
        }
366
367 57
        $buckets = [];
368 57
        $linkKeys = array_keys($link);
369
370 57
        if (isset($map)) {
371 33
            foreach ($models as $model) {
372 33
                $key = $this->getModelKey($model, $linkKeys);
373 33
                if (isset($map[$key])) {
374 33
                    foreach (array_keys($map[$key]) as $key2) {
375 33
                        $buckets[$key2][] = $model;
376 33
                    }
377 33
                }
378 33
            }
379 33
        } else {
380 57
            foreach ($models as $model) {
381 57
                $key = $this->getModelKey($model, $linkKeys);
382 57
                $buckets[$key][] = $model;
383 57
            }
384
        }
385
386 57
        if ($checkMultiple && !$this->multiple) {
387 15
            foreach ($buckets as $i => $bucket) {
388 15
                $buckets[$i] = reset($bucket);
389 15
            }
390 15
        }
391
392 57
        return $buckets;
393
    }
394
395
396
    /**
397
     * Indexes buckets by column name.
398
     *
399
     * @param array $buckets
400
     * @var string|callable $column the name of the column by which the query results should be indexed by.
401
     * This can also be a callable (e.g. anonymous function) that returns the index value based on the given row data.
402
     * @return array
403
     */
404 15
    private function indexBuckets($buckets, $indexBy)
405
    {
406 15
        $result = [];
407 15
        foreach ($buckets as $key => $models) {
408 15
            $result[$key] = [];
409 15
            foreach ($models as $model) {
410 15
                $index = is_string($indexBy) ? $model[$indexBy] : call_user_func($indexBy, $model);
411 15
                $result[$key][$index] = $model;
412 15
            }
413 15
        }
414 15
        return $result;
415
    }
416
417
    /**
418
     * @param array $attributes the attributes to prefix
419
     * @return array
420
     */
421 124
    private function prefixKeyColumns($attributes)
422
    {
423 124
        if ($this instanceof ActiveQuery && (!empty($this->join) || !empty($this->joinWith))) {
424 24
            if (empty($this->from)) {
425
                /* @var $modelClass ActiveRecord */
426 6
                $modelClass = $this->modelClass;
427 6
                $alias = $modelClass::tableName();
428 6
            } else {
429 24
                foreach ($this->from as $alias => $table) {
430 24
                    if (!is_string($alias)) {
431 24
                        $alias = $table;
432 24
                    }
433 24
                    break;
434 24
                }
435
            }
436 24
            if (isset($alias)) {
437 24
                foreach ($attributes as $i => $attribute) {
438 24
                    $attributes[$i] = "$alias.$attribute";
439 24
                }
440 24
            }
441 24
        }
442 124
        return $attributes;
443
    }
444
445
    /**
446
     * @param array $models
447
     */
448 124
    private function filterByModels($models)
449
    {
450 124
        $attributes = array_keys($this->link);
451
452 124
        $attributes = $this->prefixKeyColumns($attributes);
453
454 124
        $values = [];
455 124
        if (count($attributes) === 1) {
456
            // single key
457 124
            $attribute = reset($this->link);
458 124
            foreach ($models as $model) {
459 124
                if (($value = $model[$attribute]) !== null) {
460 124
                    if (is_array($value)) {
461
                        $values = array_merge($values, $value);
462
                    } else {
463 124
                        $values[] = $value;
464
                    }
465 124
                }
466 124
            }
467 124
            if (empty($values)) {
468 15
                $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?

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...
469 15
            }
470 124
        } else {
471
            // composite keys
472
473
            // ensure keys of $this->link are prefixed the same way as $attributes
474 3
            $prefixedLink = array_combine(
475 3
                $attributes,
476 3
                array_values($this->link)
477 3
            );
478 3
            foreach ($models as $model) {
479 3
                $v = [];
480 3
                foreach ($prefixedLink as $attribute => $link) {
481 3
                    $v[$attribute] = $model[$link];
482 3
                }
483 3
                $values[] = $v;
484 3
                if (empty($v)) {
485
                    $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?

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