Passed
Push — master ( 300fb2...9a33a3 )
by Smoren
01:41
created

QueryRelationManagerBase::all()   C

Complexity

Conditions 13
Paths 82

Size

Total Lines 60
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 13
eloc 35
c 1
b 0
f 0
nc 82
nop 1
dl 0
loc 60
rs 6.6166

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Smoren\QueryRelationManager\Base;
4
5
use Smoren\QueryRelationManager\Base\Structs\JoinCondition;
6
use Smoren\QueryRelationManager\Base\Structs\JoinConditionCollection;
7
use Smoren\QueryRelationManager\Base\Structs\Table;
8
use Smoren\QueryRelationManager\Base\Structs\TableCollection;
9
10
/**
11
 * Абстрактный класс, отвечающий за построение запроса на получение данных из нескольких таблиц
12
 * с учетом их отношений и дополнительных условий, а также за представление результата в древовидной структуре
13
 * @author Smoren <[email protected]>
14
 */
15
abstract class QueryRelationManagerBase
16
{
17
    /**
18
     * @var QueryWrapperInterface объект билдера запроса
19
     */
20
    protected QueryWrapperInterface $query;
21
22
    /**
23
     * @var JoinConditionCollection коллекция отношений в запросе
24
     */
25
    protected JoinConditionCollection $joinConditionCollection;
26
27
    /**
28
     * @var TableCollection коллекция таблиц, участвующих в запросе
29
     */
30
    protected TableCollection $tableCollection;
31
32
    /**
33
     * @var array<callable> массив функций-коллбэков для внесения дополнительных корректив в запров
34
     */
35
    protected array $filters = [];
36
37
    /**
38
     * @var array<callable> карта функций-коллбэков по псевдониму таблицы для внесения изменений
39
     * в готовые данные результата
40
     */
41
    protected array $modifierMap = [];
42
43
    /**
44
     * Начинает формирование данных запроса
45
     * @param string $className имя класса ActiveRecord, сущности которого нужно получить
46
     * @param string $tableAlias псевдоним таблицы в БД для записи отношений
47
     * @return static новый объект relation-мененджера
48
     * @throws QueryRelationManagerException
49
     */
50
    public static function select(string $className, string $tableAlias): self
51
    {
52
        return new static($className, $tableAlias);
53
    }
54
55
    /**
56
     * Добавляет к запросу связь "один к одному" с другой сущностью ActiveRecord
57
     * @param string $containerFieldAlias название поля, куда будет записана сущность в результате
58
     * @param string $className имя класса ActiveRecord, сущности которого нужно подключить
59
     * @param string $joinAs псевдоним для таблицы, связанной с классом
60
     * @param string $joinTo псевдоним таблицы, к которой будут подключаться сущности класса
61
     * @param array<string, string> $joinCondition основное условие присоединения
62
     * @param string $joinType тип присоединения таблицы ("inner", "left", "right")
63
     * @param string|null $extraJoinCondition дополнительные условия join-связи
64
     * @param array<string, scalar> $extraJoinParams параметры дополнительных условий join-связи
65
     * @return $this
66
     * @throws QueryRelationManagerException
67
     */
68
    public function withSingle(
69
        string $containerFieldAlias,
70
        string $className,
71
        string $joinAs,
72
        string $joinTo,
73
        array $joinCondition,
74
        string $joinType = 'left',
75
        ?string $extraJoinCondition = null,
76
        array $extraJoinParams = []
77
    ): self {
78
        $table = new Table(
79
            $className,
80
            $this->getTableName($className),
81
            $joinAs,
82
            $this->getTableFields($className),
83
            $this->getPrimaryKey($className),
84
            $containerFieldAlias
85
        );
86
87
        $this->tableCollection->add($table);
88
89
        $this->joinConditionCollection->add(new JoinCondition(
90
            JoinCondition::TYPE_SINGLE,
91
            $table,
92
            $this->tableCollection->byAlias($joinTo),
93
            $joinCondition,
94
            $joinType,
95
            $extraJoinCondition,
96
            $extraJoinParams
97
        ));
98
99
        return $this;
100
    }
101
102
    /**
103
     * Добавляет к запросу связь "один ко многим" с другими сущностями ActiveRecord
104
     * @param string $containerFieldAlias название поля, куда будет записана сущность в результате
105
     * @param string $className имя класса ActiveRecord, сущности которого нужно подключить
106
     * @param string $joinAs псевдоним для таблицы, связанной с классом
107
     * @param string $joinTo псевдоним таблицы, к которой будут подключаться сущности класса
108
     * @param array<string, string> $joinCondition основное условие присоединения
109
     * @param string $joinType тип присоединения таблицы ("inner", "left", "right")
110
     * @param string|null $extraJoinCondition дополнительные условия join-связи
111
     * @param array<string, scalar> $extraJoinParams параметры дополнительных условий join-связи
112
     * @return $this
113
     * @throws QueryRelationManagerException
114
     */
115
    public function withMultiple(
116
        string $containerFieldAlias,
117
        string $className,
118
        string $joinAs,
119
        string $joinTo,
120
        array $joinCondition,
121
        string $joinType = 'left',
122
        ?string $extraJoinCondition = null,
123
        array $extraJoinParams = []
124
    ): self {
125
        $table = new Table(
126
            $className,
127
            $this->getTableName($className),
128
            $joinAs,
129
            $this->getTableFields($className),
130
            $this->getPrimaryKey($className),
131
            $containerFieldAlias
132
        );
133
134
        $this->tableCollection->add($table);
135
136
        $this->joinConditionCollection->add(new JoinCondition(
137
            JoinCondition::TYPE_MULTIPLE,
138
            $table,
139
            $this->tableCollection->byAlias($joinTo),
140
            $joinCondition,
141
            $joinType,
142
            $extraJoinCondition,
143
            $extraJoinParams
144
        ));
145
146
        return $this;
147
    }
148
149
    /**
150
     * Добавляет функцию-модификатор запроса
151
     * @param callable $filter
152
     * @return $this
153
     */
154
    public function filter(callable $filter): self
155
    {
156
        $this->filters[] = $filter;
157
158
        return $this;
159
    }
160
161
    /**
162
     * Устанавливает для таблицы функцию-модификатор сущности результата
163
     * @param string $tableAlias псевдоним таблицы
164
     * @param callable $modifier функция-модификатор результата
165
     * @return $this
166
     */
167
    public function modify(string $tableAlias, callable $modifier): self
168
    {
169
        $this->modifierMap[$tableAlias] = $modifier;
170
171
        return $this;
172
    }
173
174
    /**
175
     * Выполняет запрос к базе, собирает и возвращает результат
176
     * @param mixed|null $db подключение к БД
177
     * @return array<array<mixed>> массив сущностей главной таблицы с отношениями подключенных таблиц
178
     * @throws QueryRelationManagerException
179
     */
180
    public function all($db = null): array
181
    {
182
        $this->prepare();
183
184
        $rows = $this->query->all($db);
185
186
        $map = [];
187
        foreach($this->tableCollection as $table) {
188
            $map[$table->alias] = [];
189
        }
190
191
        $bufMap = [];
192
193
        foreach($rows as $row) {
194
            foreach($this->tableCollection as $table) {
195
                if(!$table->issetDataInRow($row)) {
196
                    continue;
197
                }
198
199
                [$item, $pkValue, $alias, $aliasTo, $fkValue, $containerFieldAlias, $type]
200
                    = $table->getDataFromRow($row, $this->joinConditionCollection);
201
202
                if(!isset($map[$alias][$pkValue])) {
203
                    $map[$alias][$pkValue] = &$item;
204
                }
205
206
                if($aliasTo !== null) {
207
                    $bufMapKey = implode('-', [$aliasTo, $fkValue, $containerFieldAlias, $pkValue]);
208
                    /** @var string $type */
209
                    switch($type) {
210
                        case JoinCondition::TYPE_SINGLE:
211
                            if(!isset($bufMap[$bufMapKey])) {
212
                                /** @var mixed[][][] $map */
213
                                $map[$aliasTo][$fkValue][$containerFieldAlias] = &$item;
214
                                $bufMap[$bufMapKey] = 1;
215
                            }
216
                            break;
217
                        case JoinCondition::TYPE_MULTIPLE:
218
                            if(!isset($bufMap[$bufMapKey])) {
219
                                /** @var mixed[][][][] $map */
220
                                $map[$aliasTo][$fkValue][$containerFieldAlias][] = &$item;
221
                                $bufMap[$bufMapKey] = 1;
222
                            }
223
                            break;
224
                        default:
225
                            throw new QueryRelationManagerException("unknown condition type '{$type}'");
226
                    }
227
                }
228
                unset($item);
229
            }
230
        }
231
232
        foreach($this->modifierMap as $alias => $modifier) {
233
            foreach($map[$alias] as $pk => &$item) {
234
                ($modifier)($item);
235
            }
236
            unset($item);
237
        }
238
239
        return array_values($map[$this->tableCollection->getMainTable()->alias]);
240
    }
241
242
    /**
243
     * Создает и выстраивает SQL-запрос
244
     * @return QueryWrapperInterface
245
     * @throws QueryRelationManagerException
246
     */
247
    public function prepare(): QueryWrapperInterface
248
    {
249
        foreach($this->tableCollection as $table) {
250
            $table->setPkFieldChain(
251
                $this->tableCollection->getPkFieldChain($table->alias, $this->joinConditionCollection)
252
            );
253
        }
254
255
        $this->query = $this->createQuery();
256
257
        $arSelect = [];
258
        foreach($this->tableCollection as $table) {
259
            foreach($table->getFieldMap() as $fieldName => $fieldNamePrefixed) {
260
                $arSelect[$fieldNamePrefixed] = $fieldName;
261
            }
262
        }
263
264
        $mainTable = $this->tableCollection->getMainTable();
265
266
        $this->query
267
            ->select($arSelect)
268
            ->from([$mainTable->alias => $mainTable->name]);
269
270
        foreach($this->joinConditionCollection as $cond) {
271
            $this->query->join(
272
                $cond->joinType,
273
                [$cond->table->alias => $cond->table->name],
274
                $cond->stringify(),
275
                $cond->extraJoinParams
276
            );
277
        }
278
279
        foreach($this->filters as $modifier) {
280
            $modifier($this->query->getQuery());
281
        }
282
283
        return $this->query;
284
    }
285
286
    /**
287
     * Возвращает коллекцию объектов таблиц, участвующих в запросе
288
     * @return TableCollection
289
     */
290
    public function getTableCollection(): TableCollection
291
    {
292
        return $this->tableCollection;
293
    }
294
295
    /**
296
     * Возвращает текст SQL-запроса
297
     * @return string текст SQL-запроса
298
     * @throws QueryRelationManagerException
299
     */
300
    public function getRawSql(): string
301
    {
302
        $this->prepare();
303
304
        return $this->query->getRawSql();
305
    }
306
307
    /**
308
     * Возвращает имя таблицы по классу сущности ActiveRecord
309
     * @param string $className имя класса
310
     * @return string имя таблицы
311
     * @throws QueryRelationManagerException
312
     */
313
    abstract protected function getTableName(string $className): string;
314
315
    /**
316
     * Возвращает список полей таблицы
317
     * @param string $className
318
     * @return array<string>
319
     */
320
    abstract protected function getTableFields(string $className): array;
321
322
    /**
323
     * Возвращает поля первичного ключа таблицы
324
     * @param string $className
325
     * @return array<string>
326
     */
327
    abstract protected function getPrimaryKey(string $className): array;
328
329
    /**
330
     * Создает объект запроса
331
     * @return QueryWrapperInterface
332
     */
333
    abstract protected function createQuery(): QueryWrapperInterface;
334
335
    /**
336
     * Магический метод для клонирования инстанса
337
     */
338
    public function __clone()
339
    {
340
        $this->joinConditionCollection = clone $this->joinConditionCollection;
341
        $this->tableCollection = clone $this->tableCollection;
342
    }
343
344
    /**
345
     * QueryRelationManager constructor.
346
     * @param string $className имя класса сущности ActiveRecord
347
     * @param string $alias псевдоним таблицы сущности
348
     * @throws QueryRelationManagerException
349
     */
350
    final protected function __construct(string $className, string $alias)
351
    {
352
        $this->tableCollection = new TableCollection();
353
        $this->joinConditionCollection = new JoinConditionCollection();
354
355
        $this->tableCollection->add(new Table(
356
            $className,
357
            $this->getTableName($className),
358
            $alias,
359
            $this->getTableFields($className),
360
            $this->getPrimaryKey($className)
361
        ));
362
    }
363
}
364