Passed
Pull Request — master (#50)
by Anton
04:19
created

AbstractNode::mount()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 11
nc 8
nop 4
dl 0
loc 20
rs 9.6111
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Cycle DataMapper ORM
5
 *
6
 * @license   MIT
7
 * @author    Anton Titov (Wolfy-J)
8
 */
9
10
declare(strict_types=1);
11
12
namespace Cycle\ORM\Parser;
13
14
use Cycle\ORM\Exception\ParserException;
15
use Cycle\ORM\Parser\Traits\DuplicateTrait;
16
use Cycle\ORM\Parser\Traits\ReferenceTrait;
0 ignored issues
show
Bug introduced by
The type Cycle\ORM\Parser\Traits\ReferenceTrait was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
17
use Throwable;
18
19
/**
20
 * Represents data node in a tree with ability to parse line of results, split it into sub
21
 * relations, aggregate reference keys and etc.
22
 *
23
 * Nodes can be used as to parse one big and flat query, or when multiple queries provide their
24
 * data into one dataset, in both cases flow is identical from standpoint of Nodes (but offsets are
25
 * different).
26
 *
27
 * @internal
28
 */
29
abstract class AbstractNode
30
{
31
    use DuplicateTrait;
32
33
    // Indicates tha data must be placed at the last registered reference
34
    protected const LAST_REFERENCE = ['~'];
35
36
    /**
37
     * Indicates that node data is joined to parent row and must receive part of incoming row
38
     * subset.
39
     *
40
     * @var bool
41
     */
42
    protected $joined = false;
43
44
    /**
45
     * List of columns node must fetch from the row.
46
     *
47
     * @var array
48
     */
49
    protected $columns = [];
50
51
    /**
52
     * Declared column which must be aggregated in a parent node. i.e. Parent Key
53
     *
54
     * @var null|string
55
     */
56
    protected $outerKey;
57
58
    /**
59
     * Node location in a tree. Set when node is registered.
60
     *
61
     * @internal
62
     * @var string
63
     */
64
    protected $container;
65
66
    /**
67
     * @internal
68
     * @var AbstractNode
69
     */
70
    protected $parent;
71
72
    /**
73
     * @internal
74
     * @var TypecastInterface|null
75
     */
76
    protected $typecast;
77
78
    /** @var AbstractNode[] */
79
    protected $nodes = [];
80
81
    /**
82
     * Tree parts associated with reference keys and key values:
83
     * $this->collectedReferences[id][ID_VALUE] = [ITEM1, ITEM2, ...].
84
     *
85
     * @internal
86
     * @var array
87
     */
88
    protected $references = [];
89
90
    /**
91
     * Set of keys to be aggregated by Parser while parsing results.
92
     *
93
     * @internal
94
     * @var array
95
     */
96
    protected $trackReferences = [];
97
98
    /**
99
     * @param array       $columns  When columns are empty original line will be returned as result.
100
     * @param string|null $outerKey Defines column name in parent Node to be aggregated.
101
     */
102
    public function __construct(array $columns, string $outerKey = null)
103
    {
104
        $this->columns = $columns;
105
        $this->outerKey = $outerKey;
106
    }
107
108
    /**
109
     * Destructing.
110
     */
111
    public function __destruct()
112
    {
113
        $this->parent = null;
114
        $this->nodes = [];
115
        $this->references = [];
116
        $this->trackReferences = [];
117
        $this->duplicates = [];
118
    }
119
120
    /**
121
     * @param TypecastInterface $typecast
122
     */
123
    public function setTypecast(TypecastInterface $typecast): void
124
    {
125
        $this->typecast = $typecast;
126
    }
127
128
    /**
129
     * Parse given row of data and populate reference tree.
130
     *
131
     * @param int   $offset
132
     * @param array $row
133
     * @return int Must return number of parsed columns.
134
     */
135
    public function parseRow(int $offset, array $row): int
136
    {
137
        $data = $this->fetchData($offset, $row);
138
139
        if ($this->deduplicate($data)) {
140
            foreach ($this->trackReferences as $key) {
141
                if (!empty($data[$key])) {
142
                    $this->references[$key][$data[$key]][] = &$data;
143
                }
144
            }
145
146
            //Let's force placeholders for every sub loaded
147
            foreach ($this->nodes as $name => $node) {
148
                $data[$name] = $node instanceof ArrayNode ? [] : null;
149
            }
150
151
            $this->push($data);
152
        } elseif ($this->parent !== null) {
153
            // register duplicate rows in each parent row
154
            $this->push($data);
155
        }
156
157
        $innerOffset = 0;
158
        foreach ($this->nodes as $container => $node) {
159
            if (!$node->joined) {
160
                continue;
161
            }
162
163
            /**
164
             * We are looking into branch like structure:
165
             * node
166
             *  - node
167
             *      - node
168
             *      - node
169
             * node
170
             *
171
             * This means offset has to be calculated using all nested nodes
172
             */
173
            $innerColumns = $node->parseRow(count($this->columns) + $offset, $row);
174
175
            //Counting next selection offset
176
            $offset += $innerColumns;
177
178
            //Counting nested tree offset
179
            $innerOffset += $innerColumns;
180
        }
181
182
        return count($this->columns) + $innerOffset;
183
    }
184
185
    /**
186
     * Get list of reference key values aggregated by parent.
187
     *
188
     * @return array
189
     *
190
     * @throws ParserException
191
     */
192
    public function getReferences(): array
193
    {
194
        if ($this->parent === null) {
195
            throw new ParserException('Unable to aggregate reference values, parent is missing');
196
        }
197
198
        if (empty($this->parent->references[$this->outerKey])) {
199
            return [];
200
        }
201
202
        return array_keys($this->parent->references[$this->outerKey]);
203
    }
204
205
    /**
206
     * Register new node into NodeTree. Nodes used to convert flat results into tree representation
207
     * using reference aggregations. Node would not be used to parse incoming row results.
208
     *
209
     * @param string       $container
210
     * @param AbstractNode $node
211
     *
212
     * @throws ParserException
213
     */
214
    public function linkNode(string $container, AbstractNode $node): void
215
    {
216
        $this->nodes[$container] = $node;
217
        $node->container = $container;
218
        $node->parent = $this;
219
220
        if ($node->outerKey !== null) {
221
            if (!in_array($node->outerKey, $this->columns, true)) {
222
                throw new ParserException(
223
                    "Unable to create reference, key `{$node->outerKey}` does not exist"
224
                );
225
            }
226
227
            if (!in_array($node->outerKey, $this->trackReferences, true)) {
228
                $this->trackReferences[] = $node->outerKey;
229
            }
230
        }
231
    }
232
233
    /**
234
     * Register new node into NodeTree. Nodes used to convert flat results into tree representation
235
     * using reference aggregations. Node will used to parse row results.
236
     *
237
     * @param string       $container
238
     * @param AbstractNode $node
239
     *
240
     * @throws ParserException
241
     */
242
    public function joinNode(string $container, AbstractNode $node): void
243
    {
244
        $node->joined = true;
245
        $this->linkNode($container, $node);
246
    }
247
248
    /**
249
     * Fetch sub node.
250
     *
251
     * @param string $container
252
     * @return AbstractNode
253
     *
254
     * @throws ParserException
255
     */
256
    public function getNode(string $container): AbstractNode
257
    {
258
        if (!isset($this->nodes[$container])) {
259
            throw new ParserException("Undefined node `{$container}`");
260
        }
261
262
        return $this->nodes[$container];
263
    }
264
265
    /**
266
     * Mount record data into internal data storage under specified container using reference key
267
     * (inner key) and reference criteria (outer key value).
268
     *
269
     * Example (default ORM Loaders):
270
     * $this->parent->mount('profile', 'id', 1, [
271
     *      'id' => 100,
272
     *      'user_id' => 1,
273
     *      ...
274
     * ]);
275
     *
276
     * 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
     * Attention, data WILL be referenced to new memory location!
280
     *
281
     * @param string $container
282
     * @param string $key
283
     * @param mixed  $criteria
284
     * @param array  $data
285
     *
286
     * @throws ParserException
287
     */
288
    protected function mount(string $container, string $key, $criteria, array &$data): void
289
    {
290
        if ($criteria === self::LAST_REFERENCE) {
291
            end($this->references[$key]);
292
            $criteria = key($this->references[$key]);
293
        }
294
295
        if (!array_key_exists($criteria, $this->references[$key])) {
296
            throw new ParserException("Undefined reference `{$key}`.`{$criteria}`");
297
        }
298
299
        foreach ($this->references[$key][$criteria] as &$subset) {
300
            if (isset($subset[$container])) {
301
                // back reference!
302
                $data = &$subset[$container];
303
            } else {
304
                $subset[$container] = &$data;
305
            }
306
307
            unset($subset);
308
        }
309
    }
310
311
    /**
312
     * Mount record data into internal data storage under specified container using reference key
313
     * (inner key) and reference criteria (outer key value).
314
     *
315
     * Example (default ORM Loaders):
316
     * $this->parent->mountArray('comments', 'id', 1, [
317
     *      'id' => 100,
318
     *      'user_id' => 1,
319
     *      ...
320
     * ]);
321
     *
322
     * In this example "id" argument is inner key of "user" record and it's linked to outer key
323
     * "user_id" in "profile" record, which defines reference criteria as 1.
324
     *
325
     * Add added records will be added as array items.
326
     *
327
     * @param string $container
328
     * @param string $key
329
     * @param mixed  $criteria
330
     * @param array  $data
331
     *
332
     * @throws ParserException
333
     */
334
    protected function mountArray(string $container, string $key, $criteria, array &$data): void
335
    {
336
        if (!array_key_exists($criteria, $this->references[$key])) {
337
            throw new ParserException("Undefined reference `{$key}`.`{$criteria}`");
338
        }
339
340
        foreach ($this->references[$key][$criteria] as &$subset) {
341
            if (!in_array($data, $subset[$container], true)) {
342
                $subset[$container][] = &$data;
343
            }
344
345
            unset($subset);
346
            continue;
347
        }
348
    }
349
350
    /**
351
     * Register data result.
352
     *
353
     * @param array $data
354
     */
355
    abstract protected function push(array &$data);
356
357
    /**
358
     * Fetch record columns from query row, must use data offset to slice required part of query.
359
     *
360
     * @param int   $dataOffset
361
     * @param array $line
362
     * @return array
363
     */
364
    protected function fetchData(int $dataOffset, array $line): array
365
    {
366
        try {
367
            //Combine column names with sliced piece of row
368
            $result = array_combine(
369
                $this->columns,
370
                array_slice($line, $dataOffset, count($this->columns))
371
            );
372
373
            if ($this->typecast !== null) {
374
                return $this->typecast->cast($result);
0 ignored issues
show
Bug introduced by
It seems like $result can also be of type false; however, parameter $values of Cycle\ORM\Parser\TypecastInterface::cast() does only seem to accept array, 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

374
                return $this->typecast->cast(/** @scrutinizer ignore-type */ $result);
Loading history...
375
            }
376
377
            return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result could return the type false which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
378
        } catch (Throwable $e) {
379
            throw new ParserException(
380
                'Unable to parse incoming row: ' . $e->getMessage(),
381
                $e->getCode(),
382
                $e
383
            );
384
        }
385
    }
386
}
387