1 | <?php |
||||||
2 | |||||||
3 | declare(strict_types=1); |
||||||
4 | |||||||
5 | namespace Cycle\ORM\Parser; |
||||||
6 | |||||||
7 | use Cycle\ORM\Exception\ParserException; |
||||||
8 | use Cycle\ORM\Parser\Traits\DuplicateTrait; |
||||||
9 | |||||||
10 | /** |
||||||
11 | * Represents data node in a tree with ability to parse line of results, split it into sub |
||||||
12 | * relations, aggregate reference keys and etc. |
||||||
13 | * |
||||||
14 | * Nodes can be used as to parse one big and flat query, or when multiple queries provide their |
||||||
15 | * data into one dataset, in both cases flow is identical from standpoint of Nodes (but offsets are |
||||||
16 | * different). |
||||||
17 | * |
||||||
18 | * @internal |
||||||
19 | */ |
||||||
20 | abstract class AbstractNode |
||||||
21 | { |
||||||
22 | use DuplicateTrait; |
||||||
23 | |||||||
24 | // Indicates tha data must be placed at the last registered reference |
||||||
25 | protected const LAST_REFERENCE = ['~']; |
||||||
26 | |||||||
27 | /** |
||||||
28 | * Indicates that node data is joined to parent row and must receive part of incoming row |
||||||
29 | * subset. |
||||||
30 | */ |
||||||
31 | protected bool $joined = false; |
||||||
32 | |||||||
33 | /** |
||||||
34 | * Declared column list which must be aggregated in a parent node. i.e. Parent Key |
||||||
35 | * |
||||||
36 | * @var string[] |
||||||
37 | */ |
||||||
38 | protected array $outerKeys; |
||||||
39 | |||||||
40 | /** |
||||||
41 | * Node location in a tree. Set when node is registered. |
||||||
42 | * |
||||||
43 | * @internal |
||||||
44 | */ |
||||||
45 | protected ?string $container = null; |
||||||
46 | |||||||
47 | /** @internal */ |
||||||
48 | protected ?self $parent = null; |
||||||
49 | |||||||
50 | /** @var array<string, AbstractNode> */ |
||||||
51 | protected array $nodes = []; |
||||||
52 | |||||||
53 | protected ?ParentMergeNode $mergeParent = null; |
||||||
54 | |||||||
55 | /** @var SubclassMergeNode[] */ |
||||||
56 | protected array $mergeSubclass = []; |
||||||
57 | |||||||
58 | protected ?string $indexName; |
||||||
59 | |||||||
60 | /** |
||||||
61 | * Indexed keys and values associated with references |
||||||
62 | * |
||||||
63 | * @internal |
||||||
64 | */ |
||||||
65 | protected ?MultiKeyCollection $indexedData = null; |
||||||
66 | |||||||
67 | /** |
||||||
68 | * @param string[] $columns List of columns node must fetch from the row. |
||||||
69 | * When columns are empty original line will be returned as result. |
||||||
70 | * @param string[]|null $outerKeys Defines column name in parent Node to be aggregated. |
||||||
71 | */ |
||||||
72 | public function __construct( |
||||||
73 | 6410 | protected array $columns, |
|||||
74 | ?array $outerKeys = null, |
||||||
75 | ) { |
||||||
76 | $this->indexName = empty($outerKeys) ? null : \implode(':', $outerKeys); |
||||||
77 | 6410 | $this->outerKeys = $outerKeys ?? []; |
|||||
78 | 6410 | $this->indexedData = new MultiKeyCollection(); |
|||||
79 | 6410 | } |
|||||
80 | |||||||
81 | /** |
||||||
82 | 6394 | * Parse given row of data and populate reference tree. |
|||||
83 | * |
||||||
84 | 6394 | * @return int Must return number of parsed columns. |
|||||
85 | 6394 | */ |
|||||
86 | 6394 | public function parseRow(int $offset, array $row): int |
|||||
87 | 6394 | { |
|||||
88 | $data = $this->fetchData($offset, $row); |
||||||
89 | |||||||
90 | $innerOffset = 0; |
||||||
91 | $relatedNodes = \array_merge( |
||||||
92 | $this->mergeParent === null ? [] : [$this->mergeParent], |
||||||
93 | $this->nodes, |
||||||
94 | $this->mergeSubclass, |
||||||
95 | 6308 | ); |
|||||
96 | |||||||
97 | 6308 | if ($this->isEmptyPrimaryKey($data)) { |
|||||
98 | // Skip all columns that are related to current node and sub nodes. |
||||||
99 | 6308 | return \count($this->columns) |
|||||
100 | 6308 | + \array_reduce( |
|||||
101 | $relatedNodes, |
||||||
102 | 4072 | static fn(int $cnt, AbstractNode $node): int => $node::class === ArrayNode::class |
|||||
103 | 336 | ? 0 |
|||||
104 | : $cnt + \count($node->columns), |
||||||
105 | 0, |
||||||
106 | ); |
||||||
107 | } |
||||||
108 | 6308 | ||||||
109 | 3656 | if ($this->deduplicate($data)) { |
|||||
110 | foreach ($this->indexedData->getIndexes() as $index) { |
||||||
111 | try { |
||||||
112 | 3656 | $this->indexedData->addItem($index, $data); |
|||||
0 ignored issues
–
show
|
|||||||
113 | } catch (\Throwable) { |
||||||
0 ignored issues
–
show
Coding Style
Comprehensibility
introduced
by
|
|||||||
114 | } |
||||||
115 | 6308 | } |
|||||
116 | 780 | ||||||
117 | //Let's force placeholders for every sub loaded |
||||||
118 | 434 | foreach ($this->nodes as $name => $node) { |
|||||
119 | if ($node instanceof ParentMergeNode) { |
||||||
120 | continue; |
||||||
121 | 6304 | } |
|||||
122 | 6304 | $data[$name] = $node instanceof ArrayNode ? [] : null; |
|||||
123 | 6304 | } |
|||||
124 | 6304 | ||||||
125 | 6304 | $this->push($data); |
|||||
126 | } elseif ($this->parent !== null) { |
||||||
127 | 6304 | // register duplicate rows in each parent row |
|||||
128 | 4072 | $this->push($data); |
|||||
129 | 2522 | } |
|||||
130 | |||||||
131 | foreach ($relatedNodes as $node) { |
||||||
132 | if (!$node->joined) { |
||||||
133 | continue; |
||||||
134 | } |
||||||
135 | |||||||
136 | /** |
||||||
137 | * We are looking into branch like structure: |
||||||
138 | * node |
||||||
139 | * - node |
||||||
140 | * - node |
||||||
141 | * - node |
||||||
142 | 2792 | * node |
|||||
143 | * |
||||||
144 | * This means offset has to be calculated using all nested nodes |
||||||
145 | 2792 | */ |
|||||
146 | $innerColumns = $node->parseRow(\count($this->columns) + $offset, $row); |
||||||
147 | |||||||
148 | 2792 | //Counting next selection offset |
|||||
149 | $offset += $innerColumns; |
||||||
150 | |||||||
151 | 6304 | //Counting nested tree offset |
|||||
152 | $innerOffset += $innerColumns; |
||||||
153 | } |
||||||
154 | |||||||
155 | return \count($this->columns) + $innerOffset; |
||||||
156 | } |
||||||
157 | |||||||
158 | /** |
||||||
159 | 2382 | * Get list of reference key values aggregated by parent. |
|||||
160 | * |
||||||
161 | 2382 | * @throws ParserException |
|||||
162 | 2 | */ |
|||||
163 | public function getReferenceValues(): array |
||||||
164 | 2380 | { |
|||||
165 | if ($this->parent === null) { |
||||||
166 | throw new ParserException('Unable to aggregate reference values, parent is missing.'); |
||||||
167 | } |
||||||
168 | 2380 | if (!$this->parent->indexedData->hasIndex($this->indexName)) { |
|||||
0 ignored issues
–
show
It seems like
$this->indexName can also be of type null ; however, parameter $index of Cycle\ORM\Parser\MultiKeyCollection::hasIndex() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() The method
hasIndex() does not exist on null .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces. This is most likely a typographical error or the method has been renamed. ![]() |
|||||||
169 | return []; |
||||||
170 | } |
||||||
171 | |||||||
172 | return $this->parent->indexedData->getCriteria($this->indexName, true); |
||||||
0 ignored issues
–
show
It seems like
$this->indexName can also be of type null ; however, parameter $index of Cycle\ORM\Parser\MultiKeyCollection::getCriteria() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
173 | } |
||||||
174 | |||||||
175 | /** |
||||||
176 | * Register new node into NodeTree. Nodes used to convert flat results into tree representation |
||||||
177 | 4114 | * using reference aggregations. Node would not be used to parse incoming row results. |
|||||
178 | * |
||||||
179 | 4114 | * @throws ParserException |
|||||
180 | 4114 | */ |
|||||
181 | 3698 | public function linkNode(?string $container, self $node): void |
|||||
182 | 3698 | { |
|||||
183 | $node->parent = $this; |
||||||
184 | 880 | if ($container !== null) { |
|||||
185 | 744 | $this->nodes[$container] = $node; |
|||||
186 | $node->container = $container; |
||||||
187 | 880 | } else { |
|||||
188 | 336 | if ($node instanceof ParentMergeNode) { |
|||||
189 | $this->mergeParent = $node; |
||||||
190 | } |
||||||
191 | if ($node instanceof SubclassMergeNode) { |
||||||
192 | 4114 | $this->mergeSubclass[] = $node; |
|||||
193 | 4114 | } |
|||||
194 | } |
||||||
195 | 4114 | ||||||
196 | if ($node->indexName !== null) { |
||||||
197 | foreach ($node->outerKeys as $key) { |
||||||
198 | // foreach ($node->indexValues->getIndex($this->indexName) as $key) { |
||||||
199 | 4114 | if (!\in_array($key, $this->columns, true)) { |
|||||
200 | 4114 | throw new ParserException("Unable to create reference, key `{$key}` does not exist."); |
|||||
201 | } |
||||||
202 | } |
||||||
203 | if (!$this->indexedData->hasIndex($node->indexName)) { |
||||||
204 | $this->indexedData->createIndex($node->indexName, $node->outerKeys); |
||||||
205 | } |
||||||
206 | } |
||||||
207 | } |
||||||
208 | |||||||
209 | /** |
||||||
210 | * Register new node into NodeTree. Nodes used to convert flat results into tree representation |
||||||
211 | 2834 | * using reference aggregations. Node will used to parse row results. |
|||||
212 | * |
||||||
213 | 2834 | * @throws ParserException |
|||||
214 | 2834 | */ |
|||||
215 | public function joinNode(?string $container, self $node): void |
||||||
216 | { |
||||||
217 | $node->joined = true; |
||||||
218 | $this->linkNode($container, $node); |
||||||
219 | } |
||||||
220 | |||||||
221 | /** |
||||||
222 | 3646 | * Fetch sub node. |
|||||
223 | * |
||||||
224 | 3646 | * @throws ParserException |
|||||
225 | 2 | */ |
|||||
226 | public function getNode(string $container): self |
||||||
227 | { |
||||||
228 | 3644 | if (!isset($this->nodes[$container])) { |
|||||
229 | throw new ParserException("Undefined node `{$container}`."); |
||||||
230 | } |
||||||
231 | 744 | ||||||
232 | return $this->nodes[$container]; |
||||||
233 | 744 | } |
|||||
234 | |||||||
235 | public function getParentMergeNode(): ParentMergeNode |
||||||
236 | { |
||||||
237 | return $this->mergeParent; |
||||||
0 ignored issues
–
show
|
|||||||
238 | } |
||||||
239 | 6286 | ||||||
240 | /** |
||||||
241 | 6286 | * @return SubclassMergeNode[] |
|||||
242 | */ |
||||||
243 | public function getSubclassMergeNodes(): array |
||||||
244 | 6286 | { |
|||||
245 | return $this->mergeSubclass; |
||||||
246 | 6286 | } |
|||||
247 | 6286 | ||||||
248 | 328 | public function mergeInheritanceNodes(bool $includeRole = false): void |
|||||
249 | { |
||||||
250 | $this->mergeParent?->mergeInheritanceNodes(); |
||||||
251 | foreach ($this->mergeSubclass as $subclassNode) { |
||||||
252 | $subclassNode->mergeInheritanceNodes($includeRole); |
||||||
253 | } |
||||||
254 | } |
||||||
255 | |||||||
256 | public function __destruct() |
||||||
257 | { |
||||||
258 | $this->parent = null; |
||||||
259 | $this->nodes = []; |
||||||
260 | $this->indexedData = null; |
||||||
261 | $this->duplicates = []; |
||||||
262 | } |
||||||
263 | |||||||
264 | /** |
||||||
265 | * Mount record data into internal data storage under specified container using reference key |
||||||
266 | * (inner key) and reference criteria (outer key value). |
||||||
267 | * |
||||||
268 | * Example (default ORM Loaders): |
||||||
269 | * |
||||||
270 | 2586 | * $this->parent->mount('profile', 'id', 1, [ |
|||||
271 | * 'id' => 100, |
||||||
272 | 2586 | * 'user_id' => 1, |
|||||
273 | 264 | * // ... |
|||||
274 | * ]); |
||||||
275 | * |
||||||
276 | 264 | * In this example "id" argument is inner key of "user" record and it's linked to outer key |
|||||
277 | * "user_id" in "profile" record, which defines reference criteria as 1. |
||||||
278 | * |
||||||
279 | 2586 | * Attention, data WILL be referenced to new memory location! |
|||||
280 | 2 | * |
|||||
281 | * @throws ParserException |
||||||
282 | */ |
||||||
283 | 2586 | protected function mount(string $container, string $index, array $criteria, array &$data): void |
|||||
284 | 2586 | { |
|||||
285 | if ($criteria === self::LAST_REFERENCE) { |
||||||
286 | 336 | if (!$this->indexedData->hasIndex($index)) { |
|||||
287 | return; |
||||||
288 | 2586 | } |
|||||
289 | $criteria = $this->indexedData->getLastItemKeys($index); |
||||||
290 | } |
||||||
291 | 2586 | ||||||
292 | if ($this->indexedData->getItemsCount($index, $criteria) === 0) { |
||||||
293 | throw new ParserException(\sprintf('Undefined reference `%s` "%s".', $index, \implode(':', $criteria))); |
||||||
294 | } |
||||||
295 | |||||||
296 | foreach ($this->indexedData->getItemsSubset($index, $criteria) as &$subset) { |
||||||
0 ignored issues
–
show
The expression
$this->indexedData->getI...bset($index, $criteria) cannot be used as a reference.
Let?s assume that you have the following foreach ($array as &$itemValue) { }
However, if we were to replace foreach (getArray() as &$itemValue) { }
then assigning by reference is not possible anymore as there is no target that could be modified. Available Fixes1. Do not assign by referenceforeach (getArray() as $itemValue) { }
2. Assign to a local variable first$array = getArray();
foreach ($array as &$itemValue) {}
3. Return a referencefunction &getArray() { $array = array(); return $array; }
foreach (getArray() as &$itemValue) { }
![]() |
|||||||
297 | if (isset($subset[$container])) { |
||||||
298 | // back reference! |
||||||
299 | $data = &$subset[$container]; |
||||||
300 | } else { |
||||||
301 | $subset[$container] = &$data; |
||||||
302 | } |
||||||
303 | |||||||
304 | unset($subset); |
||||||
305 | } |
||||||
306 | } |
||||||
307 | |||||||
308 | /** |
||||||
309 | * Mount record data into internal data storage under specified container using reference key |
||||||
310 | * (inner key) and reference criteria (outer key value). |
||||||
311 | * |
||||||
312 | * Example (default ORM Loaders): |
||||||
313 | 1916 | * |
|||||
314 | * $this->parent->mountArray('comments', 'id', 1, [ |
||||||
315 | 1916 | * 'id' => 100, |
|||||
316 | * 'user_id' => 1, |
||||||
317 | * // ... |
||||||
318 | * ]); |
||||||
319 | 1916 | * |
|||||
320 | 1916 | * In this example "id" argument is inner key of "user" record and it's linked to outer key |
|||||
321 | 1916 | * "user_id" in "profile" record, which defines reference criteria as 1. |
|||||
322 | * |
||||||
323 | * Add added records will be added as array items. |
||||||
324 | 1916 | * |
|||||
325 | * @throws ParserException |
||||||
326 | */ |
||||||
327 | protected function mountArray(string $container, string $index, mixed $criteria, array &$data): void |
||||||
328 | { |
||||||
329 | if (!$this->indexedData->hasIndex($index)) { |
||||||
330 | 880 | throw new ParserException("Undefined index `{$index}`."); |
|||||
331 | } |
||||||
332 | 880 | ||||||
333 | foreach ($this->indexedData->getItemsSubset($index, $criteria) as &$subset) { |
||||||
0 ignored issues
–
show
The expression
$this->indexedData->getI...bset($index, $criteria) cannot be used as a reference.
Let?s assume that you have the following foreach ($array as &$itemValue) { }
However, if we were to replace foreach (getArray() as &$itemValue) { }
then assigning by reference is not possible anymore as there is no target that could be modified. Available Fixes1. Do not assign by referenceforeach (getArray() as $itemValue) { }
2. Assign to a local variable first$array = getArray();
foreach ($array as &$itemValue) {}
3. Return a referencefunction &getArray() { $array = array(); return $array; }
foreach (getArray() as &$itemValue) { }
![]() |
|||||||
334 | if (!\in_array($data, $subset[$container], true)) { |
||||||
335 | $subset[$container][] = &$data; |
||||||
336 | } |
||||||
337 | } |
||||||
338 | unset($subset); |
||||||
339 | 880 | } |
|||||
340 | |||||||
341 | /** |
||||||
342 | * @throws ParserException |
||||||
343 | 880 | */ |
|||||
344 | 880 | protected function mergeData(string $index, array $criteria, array $data, bool $overwrite): void |
|||||
345 | 880 | { |
|||||
346 | if ($criteria === self::LAST_REFERENCE) { |
||||||
347 | if (!$this->indexedData->hasIndex($index)) { |
||||||
348 | return; |
||||||
349 | } |
||||||
350 | $criteria = $this->indexedData->getLastItemKeys($index); |
||||||
351 | } |
||||||
352 | |||||||
353 | if ($this->indexedData->getItemsCount($index, $criteria) === 0) { |
||||||
354 | throw new ParserException(\sprintf('Undefined reference `%s` "%s".', $index, \implode(':', $criteria))); |
||||||
355 | } |
||||||
356 | |||||||
357 | 6308 | foreach ($this->indexedData->getItemsSubset($index, $criteria) as &$subset) { |
|||||
0 ignored issues
–
show
The expression
$this->indexedData->getI...bset($index, $criteria) cannot be used as a reference.
Let?s assume that you have the following foreach ($array as &$itemValue) { }
However, if we were to replace foreach (getArray() as &$itemValue) { }
then assigning by reference is not possible anymore as there is no target that could be modified. Available Fixes1. Do not assign by referenceforeach (getArray() as $itemValue) { }
2. Assign to a local variable first$array = getArray();
foreach ($array as &$itemValue) {}
3. Return a referencefunction &getArray() { $array = array(); return $array; }
foreach (getArray() as &$itemValue) { }
![]() |
|||||||
358 | $subset = $overwrite ? \array_merge($subset, $data) : \array_merge($data, $subset); |
||||||
359 | unset($subset); |
||||||
360 | } |
||||||
361 | 6308 | } |
|||||
362 | 6308 | ||||||
363 | 6308 | /** |
|||||
364 | * Register data result. |
||||||
365 | 2 | */ |
|||||
366 | 2 | abstract protected function push(array &$data); |
|||||
367 | 2 | ||||||
368 | 2 | /** |
|||||
369 | * Fetch record columns from query row, must use data offset to slice required part of query. |
||||||
370 | */ |
||||||
371 | protected function fetchData(int $dataOffset, array $line): array |
||||||
372 | { |
||||||
373 | try { |
||||||
374 | 3798 | //Combine column names with sliced piece of row |
|||||
375 | return \array_combine( |
||||||
376 | 3798 | $this->columns, |
|||||
377 | 3798 | \array_slice($line, $dataOffset, \count($this->columns)), |
|||||
378 | 3798 | ); |
|||||
379 | } catch (\Throwable $e) { |
||||||
380 | 3798 | throw new ParserException( |
|||||
381 | 'Unable to parse incoming row: ' . $e->getMessage(), |
||||||
382 | $e->getCode(), |
||||||
383 | $e, |
||||||
384 | ); |
||||||
385 | } |
||||||
386 | } |
||||||
387 | |||||||
388 | protected function intersectData(array $keys, array $data): array |
||||||
389 | { |
||||||
390 | $result = []; |
||||||
391 | foreach ($keys as $key) { |
||||||
392 | $result[$key] = $data[$key]; |
||||||
393 | } |
||||||
394 | |||||||
395 | return $result; |
||||||
396 | } |
||||||
397 | |||||||
398 | /** |
||||||
399 | * @param array<string, mixed> $data |
||||||
400 | * |
||||||
401 | * @return bool True if any PK field is empty |
||||||
402 | */ |
||||||
403 | private function isEmptyPrimaryKey(array $data): bool |
||||||
404 | { |
||||||
405 | foreach ($this->duplicateCriteria as $key) { |
||||||
406 | if ($data[$key] === null) { |
||||||
407 | return true; |
||||||
408 | } |
||||||
409 | } |
||||||
410 | |||||||
411 | return false; |
||||||
412 | } |
||||||
413 | } |
||||||
414 |
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.
This is most likely a typographical error or the method has been renamed.