Completed
Pull Request — master (#17)
by
unknown
01:35
created

BaseRelationQuery::populateInverseRelation()   D

Complexity

Conditions 9
Paths 6

Size

Total Lines 31
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 31
rs 4.909
c 1
b 0
f 0
cc 9
eloc 18
nc 6
nop 4
1
<?php
2
3
4
namespace Arrilot\BitrixModels\Queries;
5
6
7
use Arrilot\BitrixModels\Models\BaseBitrixModel;
8
use Illuminate\Support\Collection;
9
10
/**
11
 * BaseRelationQuery содержит основные методы и свойства для загрузки релейшенов
12
 *
13
 * @method BaseBitrixModel first()
14
 * @method Collection|BaseBitrixModel[] getList()
15
 * @property array $select
16
 */
17
trait BaseRelationQuery
18
{
19
    /**
20
     * @var bool - когда запрос представляет связь с один-ко-многим. Если true, вернуться все найденные модели, иначе только первая
21
     */
22
    public $multiple;
23
    /**
24
     * @var string - настройка связи моделей. ключ_у_связанной_модели
25
     */
26
    public $foreignKey;
27
    /**
28
     * @var string - настройка связи моделей. ключ_у_текущей_модели
29
     */
30
    public $localKey;
31
    /**
32
     * @var BaseBitrixModel - модель, для которой производится загрузка релейшена
33
     */
34
    public $primaryModel;
35
    /**
36
     * @var array - список связей, которые должны быть подгружены при выполнении запроса
37
     */
38
    public $with;
39
    /**
40
     * @var string - название отношения, обратное текущему отношению
41
     */
42
    public $inverseOf;
43
44
    /**
45
     * Установить название отношения, обратное текущему отношению
46
     *
47
     * @param string $relationName
48
     * @return $this
49
     */
50
    public function inverseOf($relationName)
51
    {
52
        $this->inverseOf = $relationName;
53
        return $this;
54
    }
55
56
    /**
57
     * Найти связанные записи для определенной модели [[$this->primaryModel]]
58
     * Этот метод вызывается когда релейшн вызывается ленивой загрузкой $model->relation
59
     * @return Collection|BaseBitrixModel[]|BaseBitrixModel - связанные модели
60
     * @throws \Exception
61
     */
62
    public function findFor()
63
    {
64
        return $this->multiple ? $this->getList() : $this->first();
65
    }
66
67
    /**
68
     * Определяет связи, которые должны быть загружены при выполнении запроса
69
     *
70
     * Передавая массив можно указать ключем - название релейшена, а значением - коллбек для кастомизации запроса
71
     *
72
     * @param array|string $with - связи, которые необходимо жадно подгрузить
73
     *  // Загрузить Customer и сразу для каждой модели подгрузить orders и country
74
     * Customer::query()->with(['orders', 'country'])->getList();
75
     *
76
     *  // Загрузить Customer и сразу для каждой модели подгрузить orders, а также для orders загрузить address
77
     * Customer::find()->with('orders.address')->getList();
78
     *
79
     *  // Загрузить Customer и сразу для каждой модели подгрузить country и orders (только активные)
80
     * Customer::find()->with([
81
     *     'orders' => function (BaseQuery $query) {
82
     *         $query->filter(['ACTIVE' => 'Y']);
83
     *     },
84
     *     'country',
85
     * ])->all();
86
     *
87
     * @return $this
88
     */
89
    public function with($with)
90
    {
91
        $with = (array)$with;
92
        if (empty($this->with)) {
93
            $this->with = $with;
94
        } elseif (!empty($with)) {
95
            foreach ($with as $name => $value) {
96
                if (is_int($name)) {
97
                    // дубликаты связей будут устранены в normalizeRelations()
98
                    $this->with[] = $value;
99
                } else {
100
                    $this->with[$name] = $value;
101
                }
102
            }
103
        }
104
105
        return $this;
106
    }
107
108
    /**
109
     * Добавить фильтр для загрзуки связи относительно моделей
110
     * @param Collection|BaseBitrixModel[] $models
111
     */
112
    protected function filterByModels($models)
113
    {
114
        $values = [];
115
        foreach ($models as $model) {
116
            if (($value = $model[$this->foreignKey]) !== null) {
117
                if (is_array($value)) {
118
                    $values = array_merge($values, $value);
119
                } else {
120
                    $values[] = $value;
121
                }
122
            }
123
        }
124
125
        if (empty($values)) {
126
            $this->stopQuery();
0 ignored issues
show
Bug introduced by
It seems like stopQuery() 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...
127
        }
128
129
        $primary = $this->localKey;
130
        if (preg_match('/^PROPERTY_(.*)_VALUE$/', $primary, $matches) && !empty($matches[1])) {
131
            $primary = 'PROPERTY_' . $matches[1];
132
        }
133
134
        $this->filter([$primary => array_unique($values, SORT_REGULAR)]);
0 ignored issues
show
Bug introduced by
The method filter() does not exist on Arrilot\BitrixModels\Queries\BaseRelationQuery. Did you maybe mean filterByModels()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
135
        $this->select[] = $primary;
136
    }
137
138
    /**
139
     * Подгрузить связанные модели для уже загруденных моделей
140
     * @param array $with - массив релейшенов, которые необходимо подгрузить
141
     * @param Collection|BaseBitrixModel[] $models модели, для которых загружать связи
142
     */
143
    public function findWith($with, &$models)
144
    {
145
        // --- получаем модель, на основании которой будем брать запросы релейшенов
146
        $primaryModel = reset($models);
147
        if (!$primaryModel instanceof BaseBitrixModel) {
148
            $primaryModel = $this->model;
0 ignored issues
show
Bug introduced by
The property model 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...
149
        }
150
151
        $relations = $this->normalizeRelations($primaryModel, $with);
152
        /* @var $relation BaseQuery */
153
        foreach ($relations as $name => $relation) {
154
            $relation->populateRelation($name, $models);
155
        }
156
    }
157
158
    /**
159
     * @param BaseBitrixModel $model - модель пустышка, чтобы получить запросы
160
     * @param array $with
161
     * @return BaseQuery[]
162
     */
163
    private function normalizeRelations($model, $with)
164
    {
165
        $relations = [];
166
        foreach ($with as $name => $callback) {
167
            if (is_int($name)) { // Если ключ - число, значит в значении написано название релейшена
168
                $name = $callback;
169
                $callback = null;
170
            }
171
172
            if (($pos = strpos($name, '.')) !== false) { // Если есть точка, значит указан вложенный релейшн
173
                $childName = substr($name, $pos + 1); // Название дочернего релейшена
174
                $name = substr($name, 0, $pos); // Название текущего релейшена
175
            } else {
176
                $childName = null;
177
            }
178
179
            if (!isset($relations[$name])) { // Указываем новый релейшн
180
                $relation = $model->getRelation($name); // Берем запрос
181
                $relation->primaryModel = null;
182
                $relations[$name] = $relation;
183
            } else {
184
                $relation = $relations[$name];
185
            }
186
187
            if (isset($childName)) {
188
                $relation->with[$childName] = $callback;
189
            } elseif ($callback !== null) {
190
                call_user_func($callback, $relation);
191
            }
192
        }
193
194
        return $relations;
195
    }
196
    /**
197
     * Находит связанные записи и заполняет их в первичных моделях.
198
     * @param string $name - имя релейшена
199
     * @param array $primaryModels - первичные модели
200
     * @return Collection|BaseBitrixModel[] - найденные модели
201
     */
202
    public function populateRelation($name, &$primaryModels)
203
    {
204
        $this->filterByModels($primaryModels);
205
206
        $models = $this->getList();
207
        $buckets = $this->buildBuckets($models, $this->foreignKey);
0 ignored issues
show
Bug introduced by
It seems like $models defined by $this->getList() on line 206 can also be of type object<Illuminate\Support\Collection>; however, Arrilot\BitrixModels\Que...onQuery::buildBuckets() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
208
209
        foreach ($primaryModels as $i => $primaryModel) {
210
            if ($this->multiple && is_array($keys = $primaryModel[$this->foreignKey])) {
211
                $value = [];
212
                foreach ($keys as $key) {
213
                    $key = $this->normalizeModelKey($key);
214
                    if (isset($buckets[$key])) {
215
                        $value = array_merge($value, $buckets[$key]);
216
                    }
217
                }
218
            } else {
219
                $key = $this->getModelKey($primaryModel, $this->foreignKey);
220
                $value = isset($buckets[$key]) ? $buckets[$key] : ($this->multiple ? [] : null);
221
            }
222
223
            $primaryModel->populateRelation($name, is_array($value) ? new Collection($value) : $value);
224
        }
225
226
        if ($this->inverseOf !== null) {
227
            $this->populateInverseRelation($primaryModels, $models, $name, $this->inverseOf);
228
        }
229
230
        return $models;
231
    }
232
233
    /**
234
     * Сгруппировать найденные модели
235
     * @param array $models
236
     * @param array|string $linkKeys
237
     * @param bool $checkMultiple
238
     * @return array
239
     */
240
    private function buildBuckets($models, $linkKeys, $checkMultiple = true)
241
    {
242
        $buckets = [];
243
244
        foreach ($models as $model) {
245
            $key = $this->getModelKey($model, $linkKeys);
0 ignored issues
show
Bug introduced by
It seems like $linkKeys defined by parameter $linkKeys on line 240 can also be of type string; however, Arrilot\BitrixModels\Que...ionQuery::getModelKey() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
246
            $buckets[$key][] = $model;
247
        }
248
249
        if ($checkMultiple && !$this->multiple) {
250
            foreach ($buckets as $i => $bucket) {
251
                $buckets[$i] = reset($bucket);
252
            }
253
        }
254
255
        return $buckets;
256
    }
257
258
    /**
259
     * Получить значение атрибутов в виде строки
260
     * @param BaseBitrixModel $model
261
     * @param array $attributes
262
     * @return string
263
     */
264
    private function getModelKey($model, $attributes)
265
    {
266
        $key = [];
267
        foreach ((array)$attributes as $attribute) {
268
            $key[] = $this->normalizeModelKey($model[$attribute]);
269
        }
270
        if (count($key) > 1) {
271
            return serialize($key);
272
        }
273
        $key = reset($key);
274
        return is_scalar($key) ? $key : serialize($key);
275
    }
276
277
    /**
278
     * @param mixed $value raw key value.
279
     * @return string normalized key value.
280
     */
281
    private function normalizeModelKey($value)
282
    {
283
        if (is_object($value) && method_exists($value, '__toString')) {
284
            // ensure matching to special objects, which are convertable to string, for cross-DBMS relations, for example: `|MongoId`
285
            $value = $value->__toString();
286
        }
287
288
        return $value;
289
    }
290
291
    /**
292
     * Добавляет основную модель запроса в релейшен
293
     * @param Collection|BaseBitrixModel[] $result
294
     */
295
    private function addInverseRelations(&$result)
296
    {
297
        if ($this->inverseOf === null) {
298
            return;
299
        }
300
301
        foreach ($result as $i => $relatedModel) {
302
            if (!isset($inverseRelation)) {
303
                $inverseRelation = $relatedModel->getRelation($this->inverseOf);
304
            }
305
            $relatedModel->populateRelation($this->inverseOf, $inverseRelation->multiple ? new Collection([$this->primaryModel]) : $this->primaryModel);
306
        }
307
    }
308
    /**
309
     * @param Collection|BaseBitrixModel[] $primaryModels primary models
310
     * @param Collection|BaseBitrixModel[] $models models
311
     * @param string $primaryName the primary relation name
312
     * @param string $name the relation name
313
     */
314
    private function populateInverseRelation(&$primaryModels, $models, $primaryName, $name)
315
    {
316
        if (empty($models) || empty($primaryModels)) {
317
            return;
318
        }
319
320
        /** @var BaseBitrixModel $model */
321
        $model = $models->first();
0 ignored issues
show
Bug introduced by
It seems like $models is not always an object, but can also be of type array<integer,object<Arr...odels\BaseBitrixModel>>. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
322
        $relation = $model->getRelation($name);
323
324
        if ($relation->multiple) {
325
            $buckets = $this->buildBuckets($primaryModels, $relation->foreignKey , false);
0 ignored issues
show
Bug introduced by
It seems like $primaryModels defined by parameter $primaryModels on line 314 can also be of type object<Illuminate\Support\Collection>; however, Arrilot\BitrixModels\Que...onQuery::buildBuckets() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
326
            foreach ($models as $model) {
327
                $key = $this->getModelKey($model, $relation->foreignKey);
0 ignored issues
show
Documentation introduced by
$relation->foreignKey is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
328
                $model->populateRelation($name, new Collection(isset($buckets[$key]) ? $buckets[$key] : []));
329
            }
330
        } else {
331
            foreach ($primaryModels as $i => $primaryModel) {
332
                if ($this->multiple) {
333
                    foreach ($primaryModel->related[$primaryName] as $j => $m) {
334
                        /** @var BaseBitrixModel $m */
335
                        $m->populateRelation($name, $primaryModel);
336
                    }
337
                } else {
338
                    /** @var BaseBitrixModel $m */
339
                    $m = $primaryModel->related[$primaryName];
340
                    $m->populateRelation($name, $primaryModel);
341
                }
342
            }
343
        }
344
    }
345
}