Completed
Push — master ( dc6239...d68789 )
by Alexander
11:03
created

framework/db/ActiveRelationTrait.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
        }
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 66
    public function via($relationName, callable $callable = null)
105
    {
106 66
        $relation = $this->primaryModel->getRelation($relationName);
107 66
        $this->via = [$relationName, $relation];
108 66
        if ($callable !== null) {
109 45
            call_user_func($callable, $relation);
110
        }
111
112 66
        return $this;
113
    }
114
115
    /**
116
     * Sets the name of the relation that is the inverse of this relation.
117
     * For example, an order has a customer, which means the inverse of the "customer" relation
118
     * is the "orders", and the inverse of the "orders" relation is the "customer".
119
     * If this property is set, the primary record(s) will be referenced through the specified relation.
120
     * For example, `$customer->orders[0]->customer` and `$customer` will be the same object,
121
     * and accessing the customer of an order will not trigger a new DB query.
122
     *
123
     * Use this method when declaring a relation in the [[ActiveRecord]] class:
124
     *
125
     * ```php
126
     * public function getOrders()
127
     * {
128
     *     return $this->hasMany(Order::className(), ['customer_id' => 'id'])->inverseOf('customer');
129
     * }
130
     * ```
131
     *
132
     * @param string $relationName the name of the relation that is the inverse of this relation.
133
     * @return $this the relation object itself.
134
     */
135 6
    public function inverseOf($relationName)
136
    {
137 6
        $this->inverseOf = $relationName;
138 6
        return $this;
139
    }
140
141
    /**
142
     * Finds the related records for the specified primary record.
143
     * This method is invoked when a relation of an ActiveRecord is being accessed in a lazy fashion.
144
     * @param string $name the relation name
145
     * @param ActiveRecordInterface|BaseActiveRecord $model the primary model
146
     * @return mixed the related record(s)
147
     * @throws InvalidParamException if the relation is invalid
148
     */
149 52
    public function findFor($name, $model)
150
    {
151 52
        if (method_exists($model, 'get' . $name)) {
152 52
            $method = new \ReflectionMethod($model, 'get' . $name);
153 52
            $realName = lcfirst(substr($method->getName(), 3));
154 52
            if ($realName !== $name) {
155
                throw new InvalidParamException('Relation names are case sensitive. ' . get_class($model) . " has a relation named \"$realName\" instead of \"$name\".");
156
            }
157
        }
158
159 52
        return $this->multiple ? $this->all() : $this->one();
160
    }
161
162
    /**
163
     * If applicable, populate the query's primary model into the related records' inverse relationship.
164
     * @param array $result the array of related records as generated by [[populate()]]
165
     * @since 2.0.9
166
     */
167 6
    private function addInverseRelations(&$result)
168
    {
169 6
        if ($this->inverseOf === null) {
170
            return;
171
        }
172
173 6
        foreach ($result as $i => $relatedModel) {
174 6
            if ($relatedModel instanceof ActiveRecordInterface) {
175 6
                if (!isset($inverseRelation)) {
176 6
                    $inverseRelation = $relatedModel->getRelation($this->inverseOf);
177
                }
178 6
                $relatedModel->populateRelation($this->inverseOf, $inverseRelation->multiple ? [$this->primaryModel] : $this->primaryModel);
179
            } else {
180 6
                if (!isset($inverseRelation)) {
181 6
                    $inverseRelation = (new $this->modelClass())->getRelation($this->inverseOf);
182
                }
183 6
                $result[$i][$this->inverseOf] = $inverseRelation->multiple ? [$this->primaryModel] : $this->primaryModel;
184
            }
185
        }
186 6
    }
187
188
    /**
189
     * Finds the related records and populates them into the primary models.
190
     * @param string $name the relation name
191
     * @param array $primaryModels primary models
192
     * @return array the related models
193
     * @throws InvalidConfigException if [[link]] is invalid
194
     */
195 72
    public function populateRelation($name, &$primaryModels)
196
    {
197 72
        if (!is_array($this->link)) {
198
            throw new InvalidConfigException('Invalid link: it must be an array of key-value pairs.');
199
        }
200
201 72
        if ($this->via instanceof self) {
202
            // via junction table
203
            /* @var $viaQuery ActiveRelationTrait */
204 9
            $viaQuery = $this->via;
205 9
            $viaModels = $viaQuery->findJunctionRows($primaryModels);
206 9
            $this->filterByModels($viaModels);
207 72
        } elseif (is_array($this->via)) {
208
            // via relation
209
            /* @var $viaQuery ActiveRelationTrait|ActiveQueryTrait */
210 36
            list($viaName, $viaQuery) = $this->via;
211 36
            if ($viaQuery->asArray === null) {
212
                // inherit asArray from primary query
213 36
                $viaQuery->asArray($this->asArray);
214
            }
215 36
            $viaQuery->primaryModel = null;
216 36
            $viaModels = $viaQuery->populateRelation($viaName, $primaryModels);
217 36
            $this->filterByModels($viaModels);
218
        } else {
219 72
            $this->filterByModels($primaryModels);
220
        }
221
222 72
        if (!$this->multiple && count($primaryModels) === 1) {
223 21
            $model = $this->one();
224 21
            $primaryModel = reset($primaryModels);
225 21
            if ($primaryModel instanceof ActiveRecordInterface) {
226 21
                $primaryModel->populateRelation($name, $model);
227
            } else {
228 3
                $primaryModels[key($primaryModels)][$name] = $model;
229
            }
230 21
            if ($this->inverseOf !== null) {
231 6
                $this->populateInverseRelation($primaryModels, [$model], $name, $this->inverseOf);
232
            }
233
234 21
            return [$model];
235
        }
236
237
        // https://github.com/yiisoft/yii2/issues/3197
238
        // delay indexing related models after buckets are built
239 60
        $indexBy = $this->indexBy;
240 60
        $this->indexBy = null;
241 60
        $models = $this->all();
242
243 60
        if (isset($viaModels, $viaQuery)) {
244 36
            $buckets = $this->buildBuckets($models, $this->link, $viaModels, $viaQuery->link);
245
        } else {
246 60
            $buckets = $this->buildBuckets($models, $this->link);
247
        }
248
249 60
        $this->indexBy = $indexBy;
250 60
        if ($this->indexBy !== null && $this->multiple) {
251 15
            $buckets = $this->indexBuckets($buckets, $this->indexBy);
252
        }
253
254 60
        $link = array_values(isset($viaQuery) ? $viaQuery->link : $this->link);
255 60
        foreach ($primaryModels as $i => $primaryModel) {
256 60
            if ($this->multiple && count($link) === 1 && is_array($keys = $primaryModel[reset($link)])) {
257
                $value = [];
258
                foreach ($keys as $key) {
259
                    $key = $this->normalizeModelKey($key);
260
                    if (isset($buckets[$key])) {
261
                        if ($this->indexBy !== null) {
262
                            // if indexBy is set, array_merge will cause renumbering of numeric array
263
                            foreach ($buckets[$key] as $bucketKey => $bucketValue) {
264
                                $value[$bucketKey] = $bucketValue;
265
                            }
266
                        } else {
267
                            $value = array_merge($value, $buckets[$key]);
268
                        }
269
                    }
270
                }
271
            } else {
272 60
                $key = $this->getModelKey($primaryModel, $link);
273 60
                $value = isset($buckets[$key]) ? $buckets[$key] : ($this->multiple ? [] : null);
274
            }
275 60
            if ($primaryModel instanceof ActiveRecordInterface) {
276 60
                $primaryModel->populateRelation($name, $value);
277
            } else {
278 60
                $primaryModels[$i][$name] = $value;
279
            }
280
        }
281 60
        if ($this->inverseOf !== null) {
282 3
            $this->populateInverseRelation($primaryModels, $models, $name, $this->inverseOf);
283
        }
284
285 60
        return $models;
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
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);
305 6
            if ($model instanceof ActiveRecordInterface) {
306 6
                foreach ($models as $model) {
307 6
                    $key = $this->getModelKey($model, $relation->link);
308 6
                    $model->populateRelation($name, isset($buckets[$key]) ? $buckets[$key] : []);
309
                }
310
            } else {
311 3
                foreach ($primaryModels as $i => $primaryModel) {
312 3
                    if ($this->multiple) {
313
                        foreach ($primaryModel as $j => $m) {
314
                            $key = $this->getModelKey($m, $relation->link);
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);
319 6
                        $primaryModels[$i][$primaryName][$name] = isset($buckets[$key]) ? $buckets[$key] : [];
320
                    }
321
                }
322
            }
323
        } 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
                        } else {
330 3
                            $primaryModels[$i][$primaryName][$j][$name] = $primaryModel;
331
                        }
332
                    }
333
                }
334
            } 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 63
    private function buildBuckets($models, $link, $viaModels = null, $viaLink = null, $checkMultiple = true)
355
    {
356 63
        if ($viaModels !== null) {
357 36
            $map = [];
358 36
            $viaLinkKeys = array_keys($viaLink);
359 36
            $linkValues = array_values($link);
360 36
            foreach ($viaModels as $viaModel) {
361 36
                $key1 = $this->getModelKey($viaModel, $viaLinkKeys);
362 36
                $key2 = $this->getModelKey($viaModel, $linkValues);
363 36
                $map[$key2][$key1] = true;
364
            }
365
        }
366
367 63
        $buckets = [];
368 63
        $linkKeys = array_keys($link);
369
370 63
        if (isset($map)) {
371 36
            foreach ($models as $model) {
372 36
                $key = $this->getModelKey($model, $linkKeys);
373 36
                if (isset($map[$key])) {
374 36
                    foreach (array_keys($map[$key]) as $key2) {
375 36
                        $buckets[$key2][] = $model;
376
                    }
377
                }
378
            }
379
        } else {
380 63
            foreach ($models as $model) {
381 63
                $key = $this->getModelKey($model, $linkKeys);
382 63
                $buckets[$key][] = $model;
383
            }
384
        }
385
386 63
        if ($checkMultiple && !$this->multiple) {
387 15
            foreach ($buckets as $i => $bucket) {
388 15
                $buckets[$i] = reset($bucket);
389
            }
390
        }
391
392 63
        return $buckets;
393
    }
394
395
396
    /**
397
     * Indexes buckets by column name.
398
     *
399
     * @param array $buckets
400
     * @param string|callable $indexBy 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
            }
413
        }
414
415 15
        return $result;
416
    }
417
418
    /**
419
     * @param array $attributes the attributes to prefix
420
     * @return array
421
     */
422 133
    private function prefixKeyColumns($attributes)
423
    {
424 133
        if ($this instanceof ActiveQuery && (!empty($this->join) || !empty($this->joinWith))) {
425 24
            if (empty($this->from)) {
426
                /* @var $modelClass ActiveRecord */
427 6
                $modelClass = $this->modelClass;
428 6
                $alias = $modelClass::tableName();
429
            } else {
430 24
                foreach ($this->from as $alias => $table) {
431 24
                    if (!is_string($alias)) {
432 24
                        $alias = $table;
433
                    }
434 24
                    break;
435
                }
436
            }
437 24
            if (isset($alias)) {
438 24
                foreach ($attributes as $i => $attribute) {
439 24
                    $attributes[$i] = "$alias.$attribute";
440
                }
441
            }
442
        }
443
444 133
        return $attributes;
445
    }
446
447
    /**
448
     * @param array $models
449
     */
450 133
    private function filterByModels($models)
451
    {
452 133
        $attributes = array_keys($this->link);
453
454 133
        $attributes = $this->prefixKeyColumns($attributes);
455
456 133
        $values = [];
457 133
        if (count($attributes) === 1) {
458
            // single key
459 133
            $attribute = reset($this->link);
460 133
            foreach ($models as $model) {
461 133
                if (($value = $model[$attribute]) !== null) {
462 133
                    if (is_array($value)) {
463
                        $values = array_merge($values, $value);
464
                    } else {
465 133
                        $values[] = $value;
466
                    }
467
                }
468
            }
469 133
            if (empty($values)) {
470 133
                $this->emulateExecution();
471
            }
472
        } else {
473
            // composite keys
474
475
            // ensure keys of $this->link are prefixed the same way as $attributes
476 3
            $prefixedLink = array_combine(
477 3
                $attributes,
478 3
                array_values($this->link)
479
            );
480 3
            foreach ($models as $model) {
481 3
                $v = [];
482 3
                foreach ($prefixedLink as $attribute => $link) {
483 3
                    $v[$attribute] = $model[$link];
484
                }
485 3
                $values[] = $v;
486 3
                if (empty($v)) {
487 3
                    $this->emulateExecution();
488
                }
489
            }
490
        }
491 133
        $this->andWhere(['in', $attributes, array_unique($values, SORT_REGULAR)]);
492 133
    }
493
494
    /**
495
     * @param ActiveRecordInterface|array $model
496
     * @param array $attributes
497
     * @return string
498
     */
499 63
    private function getModelKey($model, $attributes)
500
    {
501 63
        $key = [];
502 63
        foreach ($attributes as $attribute) {
503 63
            $key[] = $this->normalizeModelKey($model[$attribute]);
504
        }
505 63
        if (count($key) > 1) {
506
            return serialize($key);
507
        }
508 63
        $key = reset($key);
509 63
        return is_scalar($key) ? $key : serialize($key);
510
    }
511
512
    /**
513
     * @param mixed $value raw key value.
514
     * @return string normalized key value.
515
     */
516 63
    private function normalizeModelKey($value)
517
    {
518 63
        if (is_object($value) && method_exists($value, '__toString')) {
519
            // ensure matching to special objects, which are convertable to string, for cross-DBMS relations, for example: `|MongoId`
520
            $value = $value->__toString();
521
        }
522
523 63
        return $value;
524
    }
525
526
    /**
527
     * @param array $primaryModels either array of AR instances or arrays
528
     * @return array
529
     */
530 21
    private function findJunctionRows($primaryModels)
531
    {
532 21
        if (empty($primaryModels)) {
533
            return [];
534
        }
535 21
        $this->filterByModels($primaryModels);
536
        /* @var $primaryModel ActiveRecord */
537 21
        $primaryModel = reset($primaryModels);
538 21
        if (!$primaryModel instanceof ActiveRecordInterface) {
539
            // when primaryModels are array of arrays (asArray case)
540
            $primaryModel = $this->modelClass;
541
        }
542
543 21
        return $this->asArray()->all($primaryModel::getDb());
544
    }
545
}
546