Completed
Push — master ( bc1dad...026574 )
by Chris
03:13
created

VectorNode::__clone()   B

Complexity

Conditions 3
Paths 4

Size

Total Lines 44
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 44
ccs 0
cts 22
cp 0
rs 8.8571
c 0
b 0
f 0
cc 3
eloc 21
nc 4
nop 0
crap 12
1
<?php declare(strict_types=1);
2
3
namespace DaveRandom\Jom;
4
5
use DaveRandom\Jom\Exceptions\InvalidKeyException;
6
use DaveRandom\Jom\Exceptions\InvalidReferenceNodeException;
7
use DaveRandom\Jom\Exceptions\InvalidSubjectNodeException;
8
use DaveRandom\Jom\Exceptions\WriteOperationForbiddenException;
9
10
abstract class VectorNode extends Node implements \Countable, \IteratorAggregate, \ArrayAccess
11
{
12
    /** @var Node|null */
13
    protected $firstChild;
14
15
    /** @var Node|null */
16
    protected $lastChild;
17
18
    /** @var Node[] */
19
    protected $children = [];
20
21
    /** @var int */
22
    protected $activeIteratorCount = 0;
23
24
    /**
25
     * @throws WriteOperationForbiddenException
26
     */
27 99
    private function checkWritable(): void
28
    {
29 99
        if ($this->activeIteratorCount !== 0) {
30 9
            throw new WriteOperationForbiddenException('Cannot modify a vector with an active iterator');
31
        }
32
    }
33
34
    /**
35
     * @throws InvalidSubjectNodeException
36
     */
37 89
    private function checkSubjectNodeIsOrphan(Node $node): void
38
    {
39 89
        if ($node->parent !== null) {
40 16
            throw new InvalidSubjectNodeException('Node already present in the document');
41
        }
42
    }
43
44
    /**
45
     * @throws InvalidSubjectNodeException
46
     */
47 99
    private function checkSubjectNodeHasSameOwner(Node $node): void
48
    {
49 99
        if ($node->ownerDocument !== $this->ownerDocument) {
50 21
            throw new InvalidSubjectNodeException('Node belongs to a different document');
51
        }
52
53
    }
54
55
    /**
56
     * @throws InvalidSubjectNodeException
57
     */
58 20
    private function checkSubjectNodeIsChild(Node $node): void
59
    {
60 20
        if ($node->parent !== $this) {
61
            throw new InvalidSubjectNodeException('Node not present in children of this node');
62
        }
63
64
    }
65
66
    /**
67
     * @throws InvalidReferenceNodeException
68
     */
69 23
    private function checkReferenceNodeIsChild(Node $node): void
70
    {
71 23
        if ($node->parent !== $this) {
72 2
            throw new InvalidReferenceNodeException('Reference node not present in children of this node');
73
        }
74
75
    }
76
77 87
    private function updateFirstChildIfChanged(?Node $newNode, ?Node $oldNode): void
78
    {
79 87
        if ($this->firstChild === $oldNode) {
80 87
            $this->firstChild = $newNode;
81
        }
82
    }
83
84 27
    private function updateLastChildIfChanged(?Node $newNode, ?Node $oldNode): void
85
    {
86 27
        if ($this->lastChild === $oldNode) {
87 24
            $this->lastChild = $newNode;
88
        }
89
    }
90
91 27
    private function setNodePreviousSibling(?Node $node, ?Node $newSiblingNode): void
92
    {
93 27
        if ($node !== null) {
94 12
            $node->previousSibling = $newSiblingNode;
95
        }
96
    }
97
98 87
    private function setNodeNextSibling(?Node $node, ?Node $newSiblingNode): void
99
    {
100 87
        if ($node !== null) {
101 38
            $node->nextSibling = $newSiblingNode;
102
        }
103
    }
104
105
    /**
106
     * @throws InvalidKeyException
107
     */
108 21
    protected function resolveNode($nodeOrKey): Node
109
    {
110 21
        if ($nodeOrKey instanceof Node) {
111 21
            return $nodeOrKey;
112
        }
113
114
        if (isset($this->children[$nodeOrKey])) {
115
            return $this->children[$nodeOrKey];
116
        }
117
118
        throw new InvalidKeyException("{$nodeOrKey} does not reference a valid child node");
119
    }
120
121
    /**
122
     * @throws WriteOperationForbiddenException
123
     * @throws InvalidSubjectNodeException
124
     */
125 95
    protected function appendNode(Node $node, $key): Node
126
    {
127
        // Prevent modifying a collection with an active iterator
128 95
        $this->checkWritable();
129
130
        // Validate arguments
131 95
        $this->checkSubjectNodeHasSameOwner($node);
132 87
        $this->checkSubjectNodeIsOrphan($node);
133
134
        // Update first/last child pointers
135 87
        $this->updateFirstChildIfChanged($node, null);
136 87
        $previousSibling = $this->lastChild;
137 87
        $this->lastChild = $node;
138
139
        // Update next sibling pointer of old $lastChild (no next sibling node to update)
140 87
        $this->setNodeNextSibling($previousSibling, $node);
141
142
        // Add the child to the key map
143 87
        $this->children[$key] = $node;
144
145
        // Set references on new child
146 87
        $node->setReferences($this, $key, $previousSibling, null);
147
148 87
        return $node;
149
    }
150
151
    /**
152
     * @throws WriteOperationForbiddenException
153
     * @throws InvalidSubjectNodeException
154
     * @throws InvalidReferenceNodeException
155
     */
156 34
    protected function insertNode(Node $node, $key, Node $before = null): Node
157
    {
158
        // A null $before reference means push the node on to the end of the list
159 34
        if ($before === null) {
160 13
            return $this->appendNode($node, $key);
161
        }
162
163
        // Prevent modifying a collection with an active iterator
164 30
        $this->checkWritable();
165
166
        // Validate arguments
167 27
        $this->checkSubjectNodeHasSameOwner($node);
168 21
        $this->checkSubjectNodeIsOrphan($node);
169 15
        $this->checkReferenceNodeIsChild($before);
170
171
        // Update first child pointer (last child pointer is not affected)
172 14
        $this->updateFirstChildIfChanged($node, $before);
173
174
        // Update next sibling pointer of previous sibling of $before
175 14
        $this->setNodeNextSibling($before->previousSibling, $node);
176
177
        // Replace the child in the key map
178 14
        $this->children[$key] = $node;
179
180
        // Set references on new child
181 14
        $node->setReferences($this, $key, $before->previousSibling, $before);
182
183
        // Update references on ref child
184 14
        $before->setReferences($before->parent, $before->key, $node, $before->nextSibling);
185
186 14
        return $node;
187
    }
188
189
    /**
190
     * @throws WriteOperationForbiddenException
191
     * @throws InvalidSubjectNodeException
192
     * @throws InvalidReferenceNodeException
193
     */
194 21
    protected function replaceNode(Node $newNode, Node $oldNode): Node
195
    {
196
        // Prevent modifying a collection with an active iterator
197 21
        $this->checkWritable();
198
199
        // Validate arguments
200 19
        $this->checkSubjectNodeHasSameOwner($newNode);
201 13
        $this->checkSubjectNodeIsOrphan($newNode);
202 8
        $this->checkReferenceNodeIsChild($oldNode);
203
204
        // Update first/last child pointers
205 7
        $this->updateFirstChildIfChanged($newNode, $oldNode);
206 7
        $this->updateLastChildIfChanged($newNode, $oldNode);
207
208
        // Update sibling pointers of sibling nodes
209 7
        $this->setNodeNextSibling($oldNode->previousSibling, $newNode);
210 7
        $this->setNodePreviousSibling($oldNode->nextSibling, $newNode);
211
212
        // Replace the node in the key map
213 7
        $this->children[$oldNode->key] = $newNode;
214
215
        // Copy references from old node to new node
216 7
        $newNode->setReferences($oldNode->parent, $oldNode->key, $oldNode->previousSibling, $oldNode->nextSibling);
217
218
        // Clear references from old node
219 7
        $oldNode->setReferences(null, null, null, null);
220
221 7
        return $oldNode;
222
    }
223
224
    /**
225
     * @throws WriteOperationForbiddenException
226
     * @throws InvalidSubjectNodeException
227
     */
228 22
    protected function removeNode(Node $node): Node
229
    {
230
        // Prevent modifying a collection with an active iterator
231 22
        $this->checkWritable();
232
233
        // Validate arguments
234 20
        $this->checkSubjectNodeIsChild($node);
235
236
        // Update first/last child pointers
237 20
        $this->updateFirstChildIfChanged($node->nextSibling, $node);
238 20
        $this->updateLastChildIfChanged($node->previousSibling, $node);
239
240
        // Update sibling pointers of sibling nodes
241 20
        $this->setNodeNextSibling($node->previousSibling, $node->nextSibling);
242 20
        $this->setNodePreviousSibling($node->nextSibling, $node->previousSibling);
243
244
        // Remove the node from the key map
245 20
        unset($this->children[$node->key]);
246
247
        // Clear references from node
248 20
        $node->setReferences(null, null, null, null);
249
250 20
        return $node;
251
    }
252
253
    public function __clone()
254
    {
255
        parent::__clone();
256
257
        // Store a reference to the first child
258
        $currentOriginalChild = $this->firstChild;
259
260
        // Reset the child ref properties
261
        $this->firstChild = null;
262
        $this->lastChild = null;
263
        $this->children = [];
264
        $this->activeIteratorCount = 0;
265
266
        $previousNewChild = null;
267
268
        // If the node has children, clone the first one, set the first child pointer and advance to the next child
269
        if ($currentOriginalChild !== null) {
270
            $currentNewChild = clone $currentOriginalChild;
271
272
            // Set first child pointer to the new node and update the key map
273
            $this->firstChild = $currentNewChild;
274
            $this->children[$currentNewChild->key] = $currentNewChild;
275
276
            // Advance to the next child
277
            $previousNewChild = $currentNewChild;
278
            $currentOriginalChild = $currentOriginalChild->nextSibling;
279
        }
280
281
        while ($currentOriginalChild !== null) {
282
            $currentNewChild = clone $currentOriginalChild;
283
284
            // Update the key map
285
            $this->children[$currentNewChild->key] = $currentNewChild;
286
287
            // Set the sibling refs with the previous node
288
            $currentNewChild->previousSibling = $previousNewChild;
289
            $previousNewChild->nextSibling = $currentNewChild;
290
291
            // Advance to the next child
292
            $previousNewChild = $currentNewChild;
293
            $currentOriginalChild = $currentOriginalChild->nextSibling;
294
        }
295
296
        $this->lastChild = $previousNewChild ?? null;
297
    }
298
299
    public function hasChildren(): bool
300
    {
301
        return !empty($this->children);
302
    }
303
304 18
    public function getFirstChild(): ?Node
305
    {
306 18
        return $this->firstChild;
307
    }
308
309 12
    public function getLastChild(): ?Node
310
    {
311 12
        return $this->lastChild;
312
    }
313
314
    final public function clear(): void
315
    {
316
        try {
317
            while ($this->lastChild !== null) {
318
                $this->removeNode($this->lastChild);
319
            }
320
        //@codeCoverageIgnoreStart
321
        } catch (\Exception $e) {
322
            throw new \Error('Unexpected ' . \get_class($e) . ": {$e->getMessage()}", 0, $e);
323
        }
324
        //@codeCoverageIgnoreEnd
325
    }
326
327
    final public function getIterator(): NodeListIterator
328
    {
329 9
        return new NodeListIterator($this->firstChild, function($state) {
330 9
            $this->activeIteratorCount += $state;
331 9
            \assert($this->activeIteratorCount >= 0, new \Error('Vector node active iterator count is negative'));
332 9
        });
333
    }
334
335
    final public function count(): int
336
    {
337
        return \count($this->children);
338
    }
339
340
    final public function jsonSerialize(): array
341
    {
342
        return \iterator_to_array($this->getIterator());
343
    }
344
345
    public function offsetExists($key): bool
346
    {
347
        return isset($this->children[$key]);
348
    }
349
350
    /**
351
     * @throws WriteOperationForbiddenException
352
     * @throws InvalidSubjectNodeException
353
     */
354
    public function offsetUnset($key): void
355
    {
356
        if (isset($this->children[$key])) {
357
            $this->removeNode($this->children[$key]);
358
        }
359
    }
360
361
    abstract public function toArray(): array;
362
    abstract public function offsetGet($index): Node;
363
    abstract public function offsetSet($index, $value): void;
364
}
365