Completed
Branch feature/pre-split (4c50c1)
by Anton
03:17
created

AbstractNode   A

Complexity

Total Complexity 29

Size/Duplication

Total Lines 357
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Importance

Changes 0
Metric Value
dl 0
loc 357
rs 10
c 0
b 0
f 0
wmc 29
lcom 1
cbo 2

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 1
A asJoined() 0 7 1
A getReferences() 0 12 3
A registerNode() 0 11 2
A fetchNode() 0 8 2
B parseRow() 0 22 4
deduplicate() 0 1 ?
registerData() 0 1 ?
A mount() 0 17 3
A mountArray() 0 15 3
A fetchData() 0 12 2
A collectReferences() 0 7 2
A ensurePlaceholders() 0 7 3
A trackReference() 0 11 3
1
<?php
2
/**
3
 * components
4
 *
5
 * @author    Wolfy-J
6
 */
7
namespace Spiral\ORM\Entities\Nodes;
8
9
use Spiral\ORM\Exceptions\LoaderException;
10
use Spiral\ORM\Exceptions\NodeException;
11
12
/**
13
 * Represents data node in a tree with ability to parse line of results, split it into sub
14
 * relations, aggregate reference keys and etc.
15
 */
16
abstract class AbstractNode
17
{
18
    /**
19
     * Indicates that node data is joined to parent row.
20
     *
21
     * @var bool
22
     */
23
    private $joined = false;
24
25
    /**
26
     * Column names to be used to hydrate based on given query rows.
27
     *
28
     * @var array
29
     */
30
    private $columns = [];
31
32
    /**
33
     * @var int
34
     */
35
    private $countColumns = 0;
36
37
    /**
38
     * Set of keys to be aggregated by Parser while parsing results.
39
     *
40
     * @var array
41
     */
42
    private $trackReferences = [];
43
44
    /**
45
     * Tree parts associated with reference keys and key values.
46
     *
47
     * $this->collectedReferences[id][ID_VALUE] = [ITEM1, ITEM2, ...].
48
     *
49
     * @var array
50
     */
51
    private $references = [];
52
53
    /**
54
     * Node location in a tree. Set when node is registered.
55
     *
56
     * @var string
57
     */
58
    protected $container;
59
60
    /**
61
     * Declared column which must be aggregated in a parent node. i.e. Parent Key
62
     *
63
     * @var null|string
64
     */
65
    protected $referenceKey = null;
66
67
    /**
68
     * @invisible
69
     * @var AbstractNode
70
     */
71
    protected $parent;
72
73
    /**
74
     * @var AbstractNode[]
75
     */
76
    protected $nodes = [];
77
78
    /**
79
     * @param array       $columns
80
     * @param string|null $referenceKey Defines column name in parent Node to be aggregated.
81
     * @param array       $columns
82
     * @param string|null $referenceKey
83
     */
84
    public function __construct(
85
        array $columns = [],
86
        string $referenceKey = null
87
    ) {
88
        $this->columns = $columns;
89
        $this->countColumns = count($columns);
90
        $this->referenceKey = $referenceKey;
91
    }
92
93
    /**
94
     * Convert node into joined form (node will automatically parse parent row).
95
     *
96
     * @param bool $joined
97
     *
98
     * @return AbstractNode
99
     */
100
    public function asJoined(bool $joined = true)
101
    {
102
        $node = clone $this;
103
        $node->joined = $joined;
104
105
        return $node;
106
    }
107
108
    /**
109
     * Get list of reference key values aggregated by parent.
110
     *
111
     * @return array
112
     *
113
     * @throws LoaderException
114
     */
115
    public function getReferences(): array
116
    {
117
        if (empty($this->parent)) {
118
            throw new LoaderException("Unable to aggregate reference values, parent is missing");
119
        }
120
121
        if (empty($this->parent->references[$this->referenceKey])) {
122
            return [];
123
        }
124
125
        return array_keys($this->parent->references[$this->referenceKey]);
126
    }
127
128
    /**
129
     * Register new node into NodeTree. Nodes used to convert flat results into tree representation
130
     * using reference aggregations.
131
     *
132
     * @param string       $container
133
     * @param AbstractNode $node
134
     *
135
     * @throws NodeException
136
     */
137
    final public function registerNode(string $container, AbstractNode $node)
138
    {
139
        $node->container = $container;
140
        $node->parent = $this;
141
        $this->nodes[$container] = $node;
142
143
        if (!empty($node->referenceKey)) {
144
            //This will make parser to aggregate such key in order to be used in later statement
145
            $this->trackReference($node->referenceKey);
146
        }
147
    }
148
149
    /**
150
     * Fetch sub node.
151
     *
152
     * @param string $container
153
     *
154
     * @return AbstractNode
155
     *
156
     * @throws NodeException
157
     */
158
    final public function fetchNode(string $container): AbstractNode
159
    {
160
        if (!isset($this->nodes[$container])) {
161
            throw new NodeException("Undefined node {$container}");
162
        }
163
164
        return $this->nodes[$container];
165
    }
166
167
    /**
168
     * Parser result work, fetch data and mount it into parent tree.
169
     *
170
     * @param int   $dataOffset
171
     * @param array $row
172
     */
173
    final public function parseRow(int $dataOffset, array $row)
174
    {
175
        //Fetching Node specific data from resulted row
176
        $data = $this->fetchData($dataOffset, $row);
177
178
        if ($this->deduplicate($data)) {
179
            //Create reference keys
180
            $this->collectReferences($data);
181
182
            //Make sure that all nested relations are registered
183
            $this->ensurePlaceholders($data);
184
185
            //Add data into result set
186
            $this->registerData($data);
187
        }
188
189
        foreach ($this->nodes as $container => $node) {
190
            if ($node->joined) {
191
                $node->parseRow($this->countColumns + $dataOffset, $row);
192
            }
193
        }
194
    }
195
196
    /**
197
     * In many cases (for example if you have inload of HAS_MANY relation) record data can be
198
     * replicated by many result rows (duplicated). To prevent wrong data linking we have to
199
     * deduplicate such records. This is only internal loader functionality and required due data
200
     * tree are built using php references.
201
     *
202
     * Method will return true if data is unique handled before and false in opposite case.
203
     * Provided data array will be automatically linked with it's unique state using references.
204
     *
205
     * @param array $data Reference to parsed record data, reference will be pointed to valid and
206
     *                    existed data segment if such data was already parsed.
207
     *
208
     * @return bool Must return TRUE what data is unique in this selection.
209
     */
210
    abstract protected function deduplicate(array &$data): bool;
211
212
    /**
213
     * Register data result.
214
     *
215
     * @param array $data
216
     */
217
    abstract protected function registerData(array &$data);
218
219
    /**
220
     * Mount record data into internal data storage under specified container using reference key
221
     * (inner key) and reference criteria (outer key value).
222
     *
223
     * Example (default ORM Loaders):
224
     * $this->parent->mount('profile', 'id', 1, [
225
     *      'id' => 100,
226
     *      'user_id' => 1,
227
     *      ...
228
     * ]);
229
     *
230
     * In this example "id" argument is inner key of "user" record and it's linked to outer key
231
     * "user_id" in "profile" record, which defines reference criteria as 1.
232
     *
233
     * Attention, data WILL be referenced to new memory location!
234
     *
235
     * @param string $container
236
     * @param string $key
237
     * @param mixed  $criteria
238
     * @param array  $data      Data must be referenced to existed set if it was registered
239
     *                          previously.
240
     *
241
     * @throws LoaderException
242
     */
243
    final protected function mount(
244
        string $container,
245
        string $key,
246
        $criteria,
247
        array &$data
248
    ) {
249
        foreach ($this->references[$key][$criteria] as &$subset) {
250
            if (isset($subset[$container])) {
251
                //Back reference!
252
                $data = &$subset[$container];
253
            } else {
254
                $subset[$container] = &$data;
255
            }
256
257
            unset($subset);
258
        }
259
    }
260
261
    /**
262
     * Mount record data into internal data storage under specified container using reference key
263
     * (inner key) and reference criteria (outer key value).
264
     *
265
     * Example (default ORM Loaders):
266
     * $this->parent->mountArray('comments', 'id', 1, [
267
     *      'id' => 100,
268
     *      'user_id' => 1,
269
     *      ...
270
     * ]);
271
     *
272
     * In this example "id" argument is inner key of "user" record and it's linked to outer key
273
     * "user_id" in "profile" record, which defines reference criteria as 1.
274
     *
275
     * Add added records will be added as array items.
276
     *
277
     * @param string $container
278
     * @param string $key
279
     * @param mixed  $criteria
280
     * @param array  $data      Data must be referenced to existed set if it was registered
281
     *                          previously.
282
     *
283
     * @throws LoaderException
284
     */
285
    final protected function mountArray(
286
        string $container,
287
        string $key,
288
        $criteria,
289
        array &$data
290
    ) {
291
        foreach ($this->references[$key][$criteria] as &$subset) {
292
            if (!in_array($data, $subset[$container])) {
293
                $subset[$container][] = &$data;
294
            }
295
296
            unset($subset);
297
            continue;
298
        }
299
    }
300
301
    /**
302
     * Fetch record columns from query row, must use data offset to slice required part of query.
303
     *
304
     * @param int   $dataOffset
305
     * @param array $line
306
     *
307
     * @return array
308
     */
309
    protected function fetchData(int $dataOffset, array $line): array
310
    {
311
        try {
312
            //Combine column names with sliced piece of row
313
            return array_combine(
314
                $this->columns,
315
                array_slice($line, $dataOffset, $this->countColumns)
316
            );
317
        } catch (\Exception $e) {
318
            throw new LoaderException("Unable to parse incoming row", $e->getCode(), $e);
319
        }
320
    }
321
322
    /**
323
     * Create internal references cache based on requested keys. For example, if we have request for
324
     * "id" as reference key, every record will create following structure:
325
     * $this->references[id][ID_VALUE] = ITEM.
326
     *
327
     * Only deduplicated data must be collected!
328
     *
329
     * @see deduplicate()
330
     *
331
     * @param array $data
332
     */
333
    private function collectReferences(array &$data)
334
    {
335
        foreach ($this->trackReferences as $key) {
336
            //Adding reference(s)
337
            $this->references[$key][$data[$key]][] = &$data;
338
        }
339
    }
340
341
    /**
342
     * Create placeholders for each of sub nodes.
343
     *
344
     * @param array $data
345
     */
346
    private function ensurePlaceholders(array &$data)
347
    {
348
        //Let's force placeholders for every sub loaded
349
        foreach ($this->nodes as $name => $node) {
350
            $data[$name] = $node instanceof ArrayNode ? [] : null;
351
        }
352
    }
353
354
    /**
355
     * Add key to be tracked
356
     *
357
     * @param string $key
358
     *
359
     * @throws NodeException
360
     */
361
    private function trackReference(string $key)
362
    {
363
        if (!in_array($key, $this->columns)) {
364
            throw new NodeException("Unable to create reference, key {$key} does not exist");
365
        }
366
367
        if (!in_array($key, $this->trackReferences)) {
368
            //We are only tracking unique references
369
            $this->trackReferences[] = $key;
370
        }
371
    }
372
}