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

BaseRelationQuery::normalizeRelations()   C

Complexity

Conditions 7
Paths 25

Size

Total Lines 33
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 33
rs 6.7272
c 1
b 0
f 0
cc 7
eloc 22
nc 25
nop 2
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
    /**
41
     * Найти связанные записи для определенной модели [[$this->primaryModel]]
42
     * Этот метод вызывается когда релейшн вызывается ленивой загрузкой $model->relation
43
     * @return Collection|BaseBitrixModel[]|BaseBitrixModel - связанные модели
44
     * @throws \Exception
45
     */
46
    public function findFor()
47
    {
48
        return $this->multiple ? $this->getList() : $this->first();
49
    }
50
51
    /**
52
     * Определяет связи, которые должны быть загружены при выполнении запроса
53
     *
54
     * Передавая массив можно указать ключем - название релейшена, а значением - коллбек для кастомизации запроса
55
     *
56
     * @param array|string $with - связи, которые необходимо жадно подгрузить
57
     *  // Загрузить Customer и сразу для каждой модели подгрузить orders и country
58
     * Customer::query()->with(['orders', 'country'])->getList();
59
     *
60
     *  // Загрузить Customer и сразу для каждой модели подгрузить orders, а также для orders загрузить address
61
     * Customer::find()->with('orders.address')->getList();
62
     *
63
     *  // Загрузить Customer и сразу для каждой модели подгрузить country и orders (только активные)
64
     * Customer::find()->with([
65
     *     'orders' => function (BaseQuery $query) {
66
     *         $query->filter(['ACTIVE' => 'Y']);
67
     *     },
68
     *     'country',
69
     * ])->all();
70
     *
71
     * @return $this
72
     */
73
    public function with($with)
74
    {
75
        $with = (array)$with;
76
        if (empty($this->with)) {
77
            $this->with = $with;
78
        } elseif (!empty($with)) {
79
            foreach ($with as $name => $value) {
80
                if (is_int($name)) {
81
                    // дубликаты связей будут устранены в normalizeRelations()
82
                    $this->with[] = $value;
83
                } else {
84
                    $this->with[$name] = $value;
85
                }
86
            }
87
        }
88
89
        return $this;
90
    }
91
92
    /**
93
     * Добавить фильтр для загрзуки связи относительно моделей
94
     * @param Collection|BaseBitrixModel[] $models
95
     */
96
    protected function filterByModels($models)
97
    {
98
        $values = [];
99
        foreach ($models as $model) {
100
            if (($value = $model[$this->foreignKey]) !== null) {
101
                if (is_array($value)) {
102
                    $values = array_merge($values, $value);
103
                } else {
104
                    $values[] = $value;
105
                }
106
            }
107
        }
108
109
        if (empty($values)) {
110
            $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...
111
        }
112
113
        $primary = $this->localKey;
114
        if (preg_match('/^PROPERTY_(.*)_VALUE$/', $primary, $matches) && !empty($matches[1])) {
115
            $primary = 'PROPERTY_' . $matches[1];
116
        }
117
118
        $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...
119
        $this->select[] = $primary;
120
    }
121
122
    /**
123
     * Подгрузить связанные модели для уже загруденных моделей
124
     * @param array $with - массив релейшенов, которые необходимо подгрузить
125
     * @param Collection|BaseBitrixModel[] $models модели, для которых загружать связи
126
     */
127
    public function findWith($with, &$models)
128
    {
129
        // --- получаем модель, на основании которой будем брать запросы релейшенов
130
        $primaryModel = reset($models);
131
        if (!$primaryModel instanceof BaseBitrixModel) {
132
            $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...
133
        }
134
135
        $relations = $this->normalizeRelations($primaryModel, $with);
136
        /* @var $relation BaseQuery */
137
        foreach ($relations as $name => $relation) {
138
            $relation->populateRelation($name, $models);
139
        }
140
    }
141
142
    /**
143
     * @param BaseBitrixModel $model - модель пустышка, чтобы получить запросы
144
     * @param array $with
145
     * @return BaseQuery[]
146
     */
147
    private function normalizeRelations($model, $with)
148
    {
149
        $relations = [];
150
        foreach ($with as $name => $callback) {
151
            if (is_int($name)) { // Если ключ - число, значит в значении написано название релейшена
152
                $name = $callback;
153
                $callback = null;
154
            }
155
156
            if (($pos = strpos($name, '.')) !== false) { // Если есть точка, значит указан вложенный релейшн
157
                $childName = substr($name, $pos + 1); // Название дочернего релейшена
158
                $name = substr($name, 0, $pos); // Название текущего релейшена
159
            } else {
160
                $childName = null;
161
            }
162
163
            if (!isset($relations[$name])) { // Указываем новый релейшн
164
                $relation = $model->getRelation($name); // Берем запрос
165
                $relation->primaryModel = null;
166
                $relations[$name] = $relation;
167
            } else {
168
                $relation = $relations[$name];
169
            }
170
171
            if (isset($childName)) {
172
                $relation->with[$childName] = $callback;
173
            } elseif ($callback !== null) {
174
                call_user_func($callback, $relation);
175
            }
176
        }
177
178
        return $relations;
179
    }
180
    /**
181
     * Находит связанные записи и заполняет их в первичных моделях.
182
     * @param string $name - имя релейшена
183
     * @param array $primaryModels - первичные модели
184
     * @return Collection|BaseBitrixModel[] - найденные модели
185
     */
186
    public function populateRelation($name, &$primaryModels)
187
    {
188
        $this->filterByModels($primaryModels);
189
190
        $models = $this->getList();
191
        $buckets = $this->buildBuckets($models, $this->foreignKey);
0 ignored issues
show
Bug introduced by
It seems like $models defined by $this->getList() on line 190 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...
192
193
        foreach ($primaryModels as $i => $primaryModel) {
194
            if ($this->multiple && is_array($keys = $primaryModel[$this->foreignKey])) {
195
                $value = [];
196
                foreach ($keys as $key) {
197
                    $key = $this->normalizeModelKey($key);
198
                    if (isset($buckets[$key])) {
199
                        $value = array_merge($value, $buckets[$key]);
200
                    }
201
                }
202
            } else {
203
                $key = $this->getModelKey($primaryModel, $this->foreignKey);
204
                $value = isset($buckets[$key]) ? $buckets[$key] : ($this->multiple ? [] : null);
205
            }
206
207
            $primaryModel->populateRelation($name, is_array($value) ? new Collection($value) : $value);
208
        }
209
210
        return $models;
211
    }
212
213
    /**
214
     * Сгруппировать найденные модели
215
     * @param array $models
216
     * @param array|string $linkKeys
217
     * @param bool $checkMultiple
218
     * @return array
219
     */
220
    private function buildBuckets($models, $linkKeys, $checkMultiple = true)
221
    {
222
        $buckets = [];
223
224
        foreach ($models as $model) {
225
            $key = $this->getModelKey($model, $linkKeys);
226
            $buckets[$key][] = $model;
227
        }
228
229
        if ($checkMultiple && !$this->multiple) {
230
            foreach ($buckets as $i => $bucket) {
231
                $buckets[$i] = reset($bucket);
232
            }
233
        }
234
235
        return $buckets;
236
    }
237
238
    /**
239
     * Получить значение атрибутов в виде строки
240
     * @param BaseBitrixModel $model
241
     * @param array|string $attributes
242
     * @return string
243
     */
244
    private function getModelKey($model, $attributes)
245
    {
246
        $key = [];
247
        foreach ((array)$attributes as $attribute) {
248
            $key[] = $this->normalizeModelKey($model[$attribute]);
249
        }
250
        if (count($key) > 1) {
251
            return serialize($key);
252
        }
253
        $key = reset($key);
254
        return is_scalar($key) ? $key : serialize($key);
255
    }
256
257
    /**
258
     * @param mixed $value raw key value.
259
     * @return string normalized key value.
260
     */
261
    private function normalizeModelKey($value)
262
    {
263
        if (is_object($value) && method_exists($value, '__toString')) {
264
            // ensure matching to special objects, which are convertable to string, for cross-DBMS relations, for example: `|MongoId`
265
            $value = $value->__toString();
266
        }
267
268
        return $value;
269
    }
270
}