QueryRelationManagerBase::prepare()   B
last analyzed

Complexity

Conditions 6
Paths 24

Size

Total Lines 37
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

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