Completed
Push — activequery-alias2 ( d7e45e...ce3573 )
by Carsten
39:33 queued 33:21
created

ActiveRelationTrait::prefixKeyColumns()   C

Complexity

Conditions 9
Paths 9

Size

Total Lines 23
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 9
Metric Value
dl 0
loc 23
rs 5.8541
ccs 19
cts 19
cp 1
cc 9
eloc 14
nc 9
nop 1
crap 9
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 3
    public function inverseOf($relationName)
134
    {
135 3
        $this->inverseOf = $relationName;
136 3
        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 42
    public function findFor($name, $model)
148
    {
149 42
        if (method_exists($model, 'get' . $name)) {
150 42
            $method = new \ReflectionMethod($model, 'get' . $name);
151 42
            $realName = lcfirst(substr($method->getName(), 3));
152 42
            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 42
        }
156
157 42
        $related = $this->multiple ? $this->all() : $this->one();
158
159 42
        if ($this->inverseOf === null || empty($related)) {
160 39
            return $related;
161
        }
162
163 3
        $inverseRelation = (new $this->modelClass)->getRelation($this->inverseOf);
164
165 3
        if ($this->multiple) {
166 3
            foreach ($related as $i => $relatedModel) {
0 ignored issues
show
Bug introduced by
The expression $related of type array<integer,object<yii...\ActiveRecordInterface> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
167 3
                if ($relatedModel instanceof ActiveRecordInterface) {
168 3
                    $relatedModel->populateRelation($this->inverseOf, $inverseRelation->multiple ? [$model] : $model);
169 3
                } else {
170
                    $related[$i][$this->inverseOf] = $inverseRelation->multiple ? [$model] : $model;
171
                }
172 3
            }
173 3
        } else {
174
            if ($related instanceof ActiveRecordInterface) {
175
                $related->populateRelation($this->inverseOf, $inverseRelation->multiple ? [$model] : $model);
176
            } else {
177
                $related[$this->inverseOf] = $inverseRelation->multiple ? [$model] : $model;
178
            }
179
        }
180
181 3
        return $related;
182
    }
183
184
    /**
185
     * Finds the related records and populates them into the primary models.
186
     * @param string $name the relation name
187
     * @param array $primaryModels primary models
188
     * @return array the related models
189
     * @throws InvalidConfigException if [[link]] is invalid
190
     */
191 46
    public function populateRelation($name, &$primaryModels)
192
    {
193 46
        if (!is_array($this->link)) {
194
            throw new InvalidConfigException('Invalid link: it must be an array of key-value pairs.');
195
        }
196
197 46
        if ($this->via instanceof self) {
198
            // via junction table
199
            /* @var $viaQuery ActiveRelationTrait */
200 9
            $viaQuery = $this->via;
201 9
            $viaModels = $viaQuery->findJunctionRows($primaryModels);
202 9
            $this->filterByModels($viaModels);
203 46
        } elseif (is_array($this->via)) {
204
            // via relation
205
            /* @var $viaQuery ActiveRelationTrait|ActiveQueryTrait */
206 33
            list($viaName, $viaQuery) = $this->via;
207 33
            if ($viaQuery->asArray === null) {
208
                // inherit asArray from primary query
209 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...
210 33
            }
211 33
            $viaQuery->primaryModel = null;
212 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...
213 33
            $this->filterByModels($viaModels);
214 33
        } else {
215 46
            $this->filterByModels($primaryModels);
216
        }
217
218 46
        if (!$this->multiple && count($primaryModels) === 1) {
219 9
            $model = $this->one();
220 9
            foreach ($primaryModels as $i => $primaryModel) {
221 9
                if ($primaryModel instanceof ActiveRecordInterface) {
222 9
                    $primaryModel->populateRelation($name, $model);
223 9
                } else {
224 3
                    $primaryModels[$i][$name] = $model;
225
                }
226 9
                if ($this->inverseOf !== null) {
227 3
                    $this->populateInverseRelation($primaryModels, [$model], $name, $this->inverseOf);
228 3
                }
229 9
            }
230
231 9
            return [$model];
232
        } else {
233
            // https://github.com/yiisoft/yii2/issues/3197
234
            // delay indexing related models after buckets are built
235 46
            $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...
236 46
            $this->indexBy = null;
237 46
            $models = $this->all();
238
239 46
            if (isset($viaModels, $viaQuery)) {
240 33
                $buckets = $this->buildBuckets($models, $this->link, $viaModels, $viaQuery->link);
241 33
            } else {
242 46
                $buckets = $this->buildBuckets($models, $this->link);
243
            }
244
245 46
            $this->indexBy = $indexBy;
246 46
            if ($this->indexBy !== null && $this->multiple) {
247 9
                $buckets = $this->indexBuckets($buckets, $this->indexBy);
248 9
            }
249
250 46
            $link = array_values(isset($viaQuery) ? $viaQuery->link : $this->link);
251 46
            foreach ($primaryModels as $i => $primaryModel) {
252 46
                if ($this->multiple && count($link) === 1 && is_array($keys = $primaryModel[reset($link)])) {
253
                    $value = [];
254
                    foreach ($keys as $key) {
255
                        $key = $this->normalizeModelKey($key);
256
                        if (isset($buckets[$key])) {
257
                            if ($this->indexBy !== null) {
258
                                // if indexBy is set, array_merge will cause renumbering of numeric array
259
                                foreach ($buckets[$key] as $bucketKey => $bucketValue) {
260
                                    $value[$bucketKey] = $bucketValue;
261
                                }
262
                            } else {
263
                                $value = array_merge($value, $buckets[$key]);
264
                            }
265
                        }
266
                    }
267
                } else {
268 46
                    $key = $this->getModelKey($primaryModel, $link);
269 46
                    $value = isset($buckets[$key]) ? $buckets[$key] : ($this->multiple ? [] : null);
270
                }
271 46
                if ($primaryModel instanceof ActiveRecordInterface) {
272 46
                    $primaryModel->populateRelation($name, $value);
273 46
                } else {
274 6
                    $primaryModels[$i][$name] = $value;
275
                }
276 46
            }
277 46
            if ($this->inverseOf !== null) {
278 3
                $this->populateInverseRelation($primaryModels, $models, $name, $this->inverseOf);
279 3
            }
280
281 46
            return $models;
282
        }
283
    }
284
285
    /**
286
     * @param ActiveRecordInterface[] $primaryModels primary models
287
     * @param ActiveRecordInterface[] $models models
288
     * @param string $primaryName the primary relation name
289
     * @param string $name the relation name
290
     */
291 3
    private function populateInverseRelation(&$primaryModels, $models, $primaryName, $name)
292
    {
293 3
        if (empty($models) || empty($primaryModels)) {
294
            return;
295
        }
296 3
        $model = reset($models);
297
        /* @var $relation ActiveQueryInterface|ActiveQuery */
298 3
        $relation = $model instanceof ActiveRecordInterface ? $model->getRelation($name) : (new $this->modelClass)->getRelation($name);
299
300 3
        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...
301 3
            $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...
302 3
            if ($model instanceof ActiveRecordInterface) {
303 3
                foreach ($models as $model) {
304 3
                    $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...
305 3
                    $model->populateRelation($name, isset($buckets[$key]) ? $buckets[$key] : []);
306 3
                }
307 3
            } else {
308 3
                foreach ($primaryModels as $i => $primaryModel) {
309 3
                    if ($this->multiple) {
310
                        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...
311
                            $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...
312
                            $primaryModels[$i][$j][$name] = isset($buckets[$key]) ? $buckets[$key] : [];
313
                        }
314 3
                    } elseif (!empty($primaryModel[$primaryName])) {
315 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...
316 3
                        $primaryModels[$i][$primaryName][$name] = isset($buckets[$key]) ? $buckets[$key] : [];
317 3
                    }
318 3
                }
319
            }
320 3
        } else {
321 3
            if ($this->multiple) {
322 3
                foreach ($primaryModels as $i => $primaryModel) {
323 3
                    foreach ($primaryModel[$primaryName] as $j => $m) {
324 3
                        if ($m instanceof ActiveRecordInterface) {
325 3
                            $m->populateRelation($name, $primaryModel);
326 3
                        } else {
327 3
                            $primaryModels[$i][$primaryName][$j][$name] = $primaryModel;
328
                        }
329 3
                    }
330 3
                }
331 3
            } else {
332
                foreach ($primaryModels as $i => $primaryModel) {
333
                    if ($primaryModels[$i][$primaryName] instanceof ActiveRecordInterface) {
334
                        $primaryModels[$i][$primaryName]->populateRelation($name, $primaryModel);
335
                    } elseif (!empty($primaryModels[$i][$primaryName])) {
336
                        $primaryModels[$i][$primaryName][$name] = $primaryModel;
337
                    }
338
                }
339
            }
340
        }
341 3
    }
342
343
    /**
344
     * @param array $models
345
     * @param array $link
346
     * @param array $viaModels
347
     * @param array $viaLink
348
     * @param boolean $checkMultiple
349
     * @return array
350
     */
351 46
    private function buildBuckets($models, $link, $viaModels = null, $viaLink = null, $checkMultiple = true)
352
    {
353 46
        if ($viaModels !== null) {
354 33
            $map = [];
355 33
            $viaLinkKeys = array_keys($viaLink);
356 33
            $linkValues = array_values($link);
357 33
            foreach ($viaModels as $viaModel) {
358 33
                $key1 = $this->getModelKey($viaModel, $viaLinkKeys);
359 33
                $key2 = $this->getModelKey($viaModel, $linkValues);
360 33
                $map[$key2][$key1] = true;
361 33
            }
362 33
        }
363
364 46
        $buckets = [];
365 46
        $linkKeys = array_keys($link);
366
367 46
        if (isset($map)) {
368 33
            foreach ($models as $model) {
369 33
                $key = $this->getModelKey($model, $linkKeys);
370 33
                if (isset($map[$key])) {
371 33
                    foreach (array_keys($map[$key]) as $key2) {
372 33
                        $buckets[$key2][] = $model;
373 33
                    }
374 33
                }
375 33
            }
376 33
        } else {
377 46
            foreach ($models as $model) {
378 46
                $key = $this->getModelKey($model, $linkKeys);
379 46
                $buckets[$key][] = $model;
380 46
            }
381
        }
382
383 46
        if ($checkMultiple && !$this->multiple) {
384 15
            foreach ($buckets as $i => $bucket) {
385 15
                $buckets[$i] = reset($bucket);
386 15
            }
387 15
        }
388
389 46
        return $buckets;
390
    }
391
392
393
    /**
394
     * Indexes buckets by column name.
395
     *
396
     * @param array $buckets
397
     * @var string|callable $column the name of the column by which the query results should be indexed by.
398
     * This can also be a callable (e.g. anonymous function) that returns the index value based on the given row data.
399
     * @return array
400
     */
401 9
    private function indexBuckets($buckets, $indexBy)
402
    {
403 9
        $result = [];
404 9
        foreach ($buckets as $key => $models) {
405 9
            $result[$key] = [];
406 9
            foreach ($models as $model) {
407 9
                $index = is_string($indexBy) ? $model[$indexBy] : call_user_func($indexBy, $model);
408 9
                $result[$key][$index] = $model;
409 9
            }
410 9
        }
411 9
        return $result;
412
    }
413
414
    /**
415
     * @param array $attributes the attributes to prefix
416
     * @return array
417
     */
418 91
    private function prefixKeyColumns($attributes)
419
    {
420 91
        if ($this instanceof ActiveQuery && (!empty($this->join) || !empty($this->joinWith))) {
421 6
            if (empty($this->from)) {
422
                /* @var $modelClass ActiveRecord */
423 6
                $modelClass = $this->modelClass;
424 6
                $alias = $modelClass::tableName();
425 6
            } else {
426 6
                foreach ($this->from as $alias => $table) {
427 6
                    if (!is_string($alias)) {
428 6
                        $alias = $table;
429 6
                    }
430 6
                    break;
431 6
                }
432
            }
433 6
            if (isset($alias)) {
434 6
                foreach ($attributes as $i => $attribute) {
435 6
                    $attributes[$i] = "$alias.$attribute";
436 6
                }
437 6
            }
438 6
        }
439 91
        return $attributes;
440
    }
441
442
    /**
443
     * @param array $models
444
     */
445 91
    private function filterByModels($models)
446
    {
447 91
        $attributes = array_keys($this->link);
448
449 91
        $attributes = $this->prefixKeyColumns($attributes);
450
451 91
        $values = [];
452 91
        if (count($attributes) === 1) {
453
            // single key
454 91
            $attribute = reset($this->link);
455 91
            foreach ($models as $model) {
456 91
                if (($value = $model[$attribute]) !== null) {
457 91
                    if (is_array($value)) {
458
                        $values = array_merge($values, $value);
459
                    } else {
460 91
                        $values[] = $value;
461
                    }
462 91
                }
463 91
            }
464 91
        } else {
465
            // composite keys
466
            foreach ($models as $model) {
467
                $v = [];
468
                foreach ($this->link as $attribute => $link) {
469
                    $v[$attribute] = $model[$link];
470
                }
471
                $values[] = $v;
472
            }
473
        }
474 91
        $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...
475 91
    }
476
477
    /**
478
     * @param ActiveRecord|array $model
479
     * @param array $attributes
480
     * @return string
481
     */
482 46
    private function getModelKey($model, $attributes)
483
    {
484 46
        $key = [];
485 46
        foreach ($attributes as $attribute) {
486 46
            $key[] = $this->normalizeModelKey($model[$attribute]);
487 46
        }
488 46
        if (count($key) > 1) {
489
            return serialize($key);
490
        }
491 46
        $key = reset($key);
492 46
        return is_scalar($key) ? $key : serialize($key);
493
    }
494
495
    /**
496
     * @param mixed $value raw key value.
497
     * @return string normalized key value.
498
     */
499 46
    private function normalizeModelKey($value)
500
    {
501 46
        if (is_object($value) && method_exists($value, '__toString')) {
502
            // ensure matching to special objects, which are convertable to string, for cross-DBMS relations, for example: `|MongoId`
503
            $value = $value->__toString();
504
        }
505 46
        return $value;
506
    }
507
508
    /**
509
     * @param array $primaryModels either array of AR instances or arrays
510
     * @return array
511
     */
512 18
    private function findJunctionRows($primaryModels)
513
    {
514 18
        if (empty($primaryModels)) {
515
            return [];
516
        }
517 18
        $this->filterByModels($primaryModels);
518
        /* @var $primaryModel ActiveRecord */
519 18
        $primaryModel = reset($primaryModels);
520 18
        if (!$primaryModel instanceof ActiveRecordInterface) {
521
            // when primaryModels are array of arrays (asArray case)
522
            $primaryModel = new $this->modelClass;
523
        }
524
525 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...
526
    }
527
}
528