AbstractNode::parseRow()   C
last analyzed

Complexity

Conditions 13
Paths 10

Size

Total Lines 70
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 13.0085

Importance

Changes 2
Bugs 0 Features 1
Metric Value
cc 13
eloc 31
c 2
b 0
f 1
nc 10
nop 2
dl 0
loc 70
ccs 26
cts 27
cp 0.963
crap 13.0085
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
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;
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);
113
                } catch (\Throwable) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
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
Bug introduced by
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 ignore-type  annotation

168
        if (!$this->parent->indexedData->hasIndex(/** @scrutinizer ignore-type */ $this->indexName)) {
Loading history...
169
            return [];
170
        }
171
172
        return $this->parent->indexedData->getCriteria($this->indexName, true);
0 ignored issues
show
Bug introduced by
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 ignore-type  annotation

172
        return $this->parent->indexedData->getCriteria(/** @scrutinizer ignore-type */ $this->indexName, true);
Loading history...
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
Bug Best Practice introduced by
The expression return $this->mergeParent could return the type null which is incompatible with the type-hinted return Cycle\ORM\Parser\ParentMergeNode. Consider adding an additional type-check to rule them out.
Loading history...
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
        unset($this->indexedData);
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
Bug introduced by
The expression $this->indexedData->getI...bset($index, $criteria) cannot be used as a reference.

Let?s assume that you have the following foreach statement:

foreach ($array as &$itemValue) { }

$itemValue is assigned by reference. This is possible because the expression (in the example $array) can be used as a reference target.

However, if we were to replace $array with something different like the result of a function call as in

foreach (getArray() as &$itemValue) { }

then assigning by reference is not possible anymore as there is no target that could be modified.

Available Fixes

1. Do not assign by reference
foreach (getArray() as $itemValue) { }
2. Assign to a local variable first
$array = getArray();
foreach ($array as &$itemValue) {}
3. Return a reference
function &getArray() { $array = array(); return $array; }

foreach (getArray() as &$itemValue) { }
Loading history...
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
Bug introduced by
The expression $this->indexedData->getI...bset($index, $criteria) cannot be used as a reference.

Let?s assume that you have the following foreach statement:

foreach ($array as &$itemValue) { }

$itemValue is assigned by reference. This is possible because the expression (in the example $array) can be used as a reference target.

However, if we were to replace $array with something different like the result of a function call as in

foreach (getArray() as &$itemValue) { }

then assigning by reference is not possible anymore as there is no target that could be modified.

Available Fixes

1. Do not assign by reference
foreach (getArray() as $itemValue) { }
2. Assign to a local variable first
$array = getArray();
foreach ($array as &$itemValue) {}
3. Return a reference
function &getArray() { $array = array(); return $array; }

foreach (getArray() as &$itemValue) { }
Loading history...
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
Bug introduced by
The expression $this->indexedData->getI...bset($index, $criteria) cannot be used as a reference.

Let?s assume that you have the following foreach statement:

foreach ($array as &$itemValue) { }

$itemValue is assigned by reference. This is possible because the expression (in the example $array) can be used as a reference target.

However, if we were to replace $array with something different like the result of a function call as in

foreach (getArray() as &$itemValue) { }

then assigning by reference is not possible anymore as there is no target that could be modified.

Available Fixes

1. Do not assign by reference
foreach (getArray() as $itemValue) { }
2. Assign to a local variable first
$array = getArray();
foreach ($array as &$itemValue) {}
3. Return a reference
function &getArray() { $array = array(); return $array; }

foreach (getArray() as &$itemValue) { }
Loading history...
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