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

BaseRelationQuery::addInverseRelations()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 13
rs 8.8571
c 1
b 0
f 0
cc 5
eloc 7
nc 4
nop 1
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 array - настройка связи моделей. [ключ_у_связанной_модели => ключ_у_текущей_модели]
25
     */
26
    public $link;
27
    /**
28
     * @var BaseBitrixModel - модель, для которой производится загрузка релейшена
29
     */
30
    public $primaryModel;
31
    /**
32
     * @var array - список связей, которые должны быть подгружены при выполнении запроса
33
     */
34
    public $with;
35
    /**
36
     * @var string - название отношения, обратное текущему отношению
37
     */
38
    public $inverseOf;
39
40
    /**
41
     * Установить название отношения, обратное текущему отношению
42
     *
43
     * @param string $relationName
44
     * @return $this
45
     */
46
    public function inverseOf($relationName)
47
    {
48
        $this->inverseOf = $relationName;
49
        return $this;
50
    }
51
52
    /**
53
     * Найти связанные записи для определенной модели [[$this->primaryModel]]
54
     * Этот метод вызывается когда релейшн вызывается ленивой загрузкой $model->relation
55
     * @return Collection|BaseBitrixModel[]|BaseBitrixModel - связанные модели
56
     * @throws \Exception
57
     */
58
    public function findFor()
59
    {
60
        return $this->multiple ? $this->getList() : $this->first();
61
    }
62
63
    /**
64
     * Определяет связи, которые должны быть загружены при выполнении запроса
65
     *
66
     * Передавая массив можно указать ключем - название релейшена, а значением - коллбек для кастомизации запроса
67
     *
68
     * @param array|string $with - связи, которые необходимо жадно подгрузить
69
     *  // Загрузить Customer и сразу для каждой модели подгрузить orders и country
70
     * Customer::query()->with(['orders', 'country'])->getList();
71
     *
72
     *  // Загрузить Customer и сразу для каждой модели подгрузить orders, а также для orders загрузить address
73
     * Customer::find()->with('orders.address')->getList();
74
     *
75
     *  // Загрузить Customer и сразу для каждой модели подгрузить country и orders (только активные)
76
     * Customer::find()->with([
77
     *     'orders' => function (BaseQuery $query) {
78
     *         $query->filter(['ACTIVE' => 'Y']);
79
     *     },
80
     *     'country',
81
     * ])->all();
82
     *
83
     * @return $this
84
     */
85
    public function with($with)
86
    {
87
        $with = (array)$with;
88
        if (empty($this->with)) {
89
            $this->with = $with;
90
        } elseif (!empty($with)) {
91
            foreach ($with as $name => $value) {
92
                if (is_int($name)) {
93
                    // дубликаты связей будут устранены в normalizeRelations()
94
                    $this->with[] = $value;
95
                } else {
96
                    $this->with[$name] = $value;
97
                }
98
            }
99
        }
100
101
        return $this;
102
    }
103
104
    /**
105
     * Добавить фильтр для загрзуки связи относительно моделей
106
     * @param Collection|BaseBitrixModel[] $models
107
     */
108
    protected function filterByModels($models)
109
    {
110
        $attributes = array_keys($this->link);
111
112
        if (count($attributes) != 1) {
113
            throw new \LogicException('Массив link может содержать только один элемент.');
114
        }
115
116
        $values = [];
117
        $primary = current($attributes);
118
        $attribute = reset($this->link);
119
        foreach ($models as $model) {
120
            if (($value = $model[$attribute]) !== null) {
121
                if (is_array($value)) {
122
                    $values = array_merge($values, $value);
123
                } else {
124
                    $values[] = $value;
125
                }
126
            }
127
        }
128
129
        if (empty($values)) {
130
            $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...
131
        }
132
133
        if (preg_match('/^PROPERTY_(.*)_VALUE$/', $primary, $matches) && !empty($matches[1])) {
134
            $primary = 'PROPERTY_' . $matches[1];
135
        }
136
137
        $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...
138
        $this->select[] = $primary;
139
    }
140
141
    /**
142
     * Подгрузить связанные модели для уже загруденных моделей
143
     * @param array $with - массив релейшенов, которые необходимо подгрузить
144
     * @param Collection|BaseBitrixModel[] $models модели, для которых загружать связи
145
     */
146
    public function findWith($with, &$models)
147
    {
148
        // --- получаем модель, на основании которой будем брать запросы релейшенов
149
        $primaryModel = reset($models);
150
        if (!$primaryModel instanceof BaseBitrixModel) {
151
            $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...
152
        }
153
154
        $relations = $this->normalizeRelations($primaryModel, $with);
155
        /* @var $relation BaseQuery */
156
        foreach ($relations as $name => $relation) {
157
            $relation->populateRelation($name, $models);
158
        }
159
    }
160
161
    /**
162
     * @param BaseBitrixModel $model - модель пустышка, чтобы получить запросы
163
     * @param array $with
164
     * @return BaseQuery[]
165
     */
166
    private function normalizeRelations($model, $with)
167
    {
168
        $relations = [];
169
        foreach ($with as $name => $callback) {
170
            if (is_int($name)) { // Если ключ - число, значит в значении написано название релейшена
171
                $name = $callback;
172
                $callback = null;
173
            }
174
175
            if (($pos = strpos($name, '.')) !== false) { // Если есть точка, значит указан вложенный релейшн
176
                $childName = substr($name, $pos + 1); // Название дочернего релейшена
177
                $name = substr($name, 0, $pos); // Название текущего релейшена
178
            } else {
179
                $childName = null;
180
            }
181
182
            if (!isset($relations[$name])) { // Указываем новый релейшн
183
                $relation = $model->getRelation($name); // Берем запрос
184
                $relation->primaryModel = null;
185
                $relations[$name] = $relation;
186
            } else {
187
                $relation = $relations[$name];
188
            }
189
190
            if (isset($childName)) {
191
                $relation->with[$childName] = $callback;
192
            } elseif ($callback !== null) {
193
                call_user_func($callback, $relation);
194
            }
195
        }
196
197
        return $relations;
198
    }
199
    /**
200
     * Находит связанные записи и заполняет их в первичных моделях.
201
     * @param string $name - имя релейшена
202
     * @param array $primaryModels - первичные модели
203
     * @return Collection|BaseBitrixModel[] - найденные модели
204
     */
205
    public function populateRelation($name, &$primaryModels)
206
    {
207
        if (!is_array($this->link)) {
208
            throw new \LogicException('Invalid link: it must be an array of key-value pairs.');
209
        }
210
211
        $this->filterByModels($primaryModels);
212
213
        $models = $this->getList();
214
        $buckets = $this->buildBuckets($models, $this->link);
0 ignored issues
show
Bug introduced by
It seems like $models defined by $this->getList() on line 213 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...
215
216
        foreach ($primaryModels as $i => $primaryModel) {
217
            if ($this->multiple && count($this->link) === 1 && is_array($keys = $primaryModel[reset($this->link)])) {
218
                $value = [];
219
                foreach ($keys as $key) {
220
                    $key = $this->normalizeModelKey($key);
221
                    if (isset($buckets[$key])) {
222
                        $value = array_merge($value, $buckets[$key]);
223
                    }
224
                }
225
            } else {
226
                $key = $this->getModelKey($primaryModel, $this->link);
227
                $value = isset($buckets[$key]) ? $buckets[$key] : ($this->multiple ? [] : null);
228
            }
229
230
            $primaryModel->populateRelation($name, is_array($value) ? new Collection($value) : $value);
231
        }
232
233
        if ($this->inverseOf !== null) {
234
            $this->populateInverseRelation($primaryModels, $models, $name, $this->inverseOf);
235
        }
236
237
        return $models;
238
    }
239
240
    /**
241
     * Сгруппировать найденные модели
242
     * @param array $models
243
     * @param array $link
244
     * @param bool $checkMultiple
245
     * @return array
246
     */
247
    private function buildBuckets($models, $link, $checkMultiple = true)
248
    {
249
        $buckets = [];
250
        $linkKeys = array_keys($link);
251
252
        foreach ($models as $model) {
253
            $key = $this->getModelKey($model, $linkKeys);
254
            $buckets[$key][] = $model;
255
        }
256
257
        if ($checkMultiple && !$this->multiple) {
258
            foreach ($buckets as $i => $bucket) {
259
                $buckets[$i] = reset($bucket);
260
            }
261
        }
262
263
        return $buckets;
264
    }
265
266
    /**
267
     * Получить значение атрибутов в виде строки
268
     * @param BaseBitrixModel $model
269
     * @param array $attributes
270
     * @return string
271
     */
272
    private function getModelKey($model, $attributes)
273
    {
274
        $key = [];
275
        foreach ($attributes as $attribute) {
276
            $key[] = $this->normalizeModelKey($model[$attribute]);
277
        }
278
        if (count($key) > 1) {
279
            return serialize($key);
280
        }
281
        $key = reset($key);
282
        return is_scalar($key) ? $key : serialize($key);
283
    }
284
285
    /**
286
     * @param mixed $value raw key value.
287
     * @return string normalized key value.
288
     */
289
    private function normalizeModelKey($value)
290
    {
291
        if (is_object($value) && method_exists($value, '__toString')) {
292
            // ensure matching to special objects, which are convertable to string, for cross-DBMS relations, for example: `|MongoId`
293
            $value = $value->__toString();
294
        }
295
296
        return $value;
297
    }
298
299
    /**
300
     * Добавляет основную модель запроса в релейшен
301
     * @param Collection|BaseBitrixModel[] $result
302
     */
303
    private function addInverseRelations(&$result)
304
    {
305
        if ($this->inverseOf === null) {
306
            return;
307
        }
308
309
        foreach ($result as $i => $relatedModel) {
310
            if (!isset($inverseRelation)) {
311
                $inverseRelation = $relatedModel->getRelation($this->inverseOf);
312
            }
313
            $relatedModel->populateRelation($this->inverseOf, $inverseRelation->multiple ? new Collection([$this->primaryModel]) : $this->primaryModel);
314
        }
315
    }
316
    /**
317
     * @param Collection|BaseBitrixModel[] $primaryModels primary models
318
     * @param Collection|BaseBitrixModel[] $models models
319
     * @param string $primaryName the primary relation name
320
     * @param string $name the relation name
321
     */
322
    private function populateInverseRelation(&$primaryModels, $models, $primaryName, $name)
323
    {
324
        if (empty($models) || empty($primaryModels)) {
325
            return;
326
        }
327
328
        /** @var BaseBitrixModel $model */
329
        $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...
330
        $relation = $model->getRelation($name);
331
332
        if ($relation->multiple) {
333
            $buckets = $this->buildBuckets($primaryModels, $relation->link , false);
0 ignored issues
show
Bug introduced by
It seems like $primaryModels defined by parameter $primaryModels on line 322 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...
334
            foreach ($models as $model) {
335
                $key = $this->getModelKey($model, $relation->link);
336
                $model->populateRelation($name, new Collection(isset($buckets[$key]) ? $buckets[$key] : []));
337
            }
338
        } else {
339
            foreach ($primaryModels as $i => $primaryModel) {
340
                if ($this->multiple) {
341
                    foreach ($primaryModel->related[$primaryName] as $j => $m) {
342
                        /** @var BaseBitrixModel $m */
343
                        $m->populateRelation($name, $primaryModel);
344
                    }
345
                } else {
346
                    /** @var BaseBitrixModel $m */
347
                    $m = $primaryModel->related[$primaryName];
348
                    $m->populateRelation($name, $primaryModel);
349
                }
350
            }
351
        }
352
    }
353
}