Completed
Push — master ( 4a44e3...b0b1ab )
by Chris
02:27
created

VectorNode::appendClonedChildNodes()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 0
cts 3
cp 0
rs 9.6666
c 0
b 0
f 0
cc 3
eloc 5
nc 3
nop 1
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 163
    private function checkWritable(): void
28
    {
29 163
        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 153
    private function checkSubjectNodeIsOrphan(Node $node): void
38
    {
39 153
        if ($node->parent !== null) {
40 16
            throw new InvalidSubjectNodeException('Node already present in the document');
41
        }
42
    }
43
44
    /**
45
     * @throws InvalidSubjectNodeException
46
     */
47 163
    private function checkSubjectNodeHasSameOwner(Node $node): void
48
    {
49 163
        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 151
    private function updateFirstChildIfChanged(?Node $newNode, ?Node $oldNode): void
78
    {
79 151
        if ($this->firstChild === $oldNode) {
80 151
            $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 151
    private function setNodeNextSibling(?Node $node, ?Node $newSiblingNode): void
99
    {
100 151
        if ($node !== null) {
101 102
            $node->nextSibling = $newSiblingNode;
102
        }
103
    }
104
105
    private function resetChildReferenceProperties(): void
106
    {
107
        $this->firstChild = null;
108
        $this->lastChild = null;
109
        $this->children = [];
110
        $this->activeIteratorCount = 0;
111
    }
112
113
    private function appendClonedChildNodes(array $nodes): void
114
    {
115
        foreach ($nodes as $key => $node) {
116
            try {
117
                $this->appendNode(clone $node, $key);
118
                //@codeCoverageIgnoreStart
119
            } catch (\Exception $e) {
120
                /** @noinspection PhpInternalEntityUsedInspection */
121
                throw unexpected($e);
122
            }
123
            //@codeCoverageIgnoreEnd
124
        }
125
    }
126
127
    /**
128
     * @throws InvalidKeyException
129
     */
130 21
    protected function resolveNode($nodeOrKey): Node
131
    {
132 21
        if ($nodeOrKey instanceof Node) {
133 21
            return $nodeOrKey;
134
        }
135
136
        if (isset($this->children[$nodeOrKey])) {
137
            return $this->children[$nodeOrKey];
138
        }
139
140
        throw new InvalidKeyException("{$nodeOrKey} does not reference a valid child node");
141
    }
142
143
    /**
144
     * @throws WriteOperationForbiddenException
145
     * @throws InvalidSubjectNodeException
146
     */
147 159
    protected function appendNode(Node $node, $key): Node
148
    {
149
        // Prevent modifying a collection with an active iterator
150 159
        $this->checkWritable();
151
152
        // Validate arguments
153 159
        $this->checkSubjectNodeHasSameOwner($node);
154 151
        $this->checkSubjectNodeIsOrphan($node);
155
156
        // Update first/last child pointers
157 151
        $this->updateFirstChildIfChanged($node, null);
158 151
        $previousSibling = $this->lastChild;
159 151
        $this->lastChild = $node;
160
161
        // Update next sibling pointer of old $lastChild (no next sibling node to update)
162 151
        $this->setNodeNextSibling($previousSibling, $node);
163
164
        // Add the child to the key map
165 151
        $this->children[$key] = $node;
166
167
        // Set references on new child
168 151
        $node->setReferences($this, $key, $previousSibling, null);
169
170 151
        return $node;
171
    }
172
173
    /**
174
     * @throws WriteOperationForbiddenException
175
     * @throws InvalidSubjectNodeException
176
     * @throws InvalidReferenceNodeException
177
     */
178 34
    protected function insertNode(Node $node, $key, ?Node $before = null): Node
179
    {
180
        // A null $before reference means push the node on to the end of the list
181 34
        if ($before === null) {
182 13
            return $this->appendNode($node, $key);
183
        }
184
185
        // Prevent modifying a collection with an active iterator
186 30
        $this->checkWritable();
187
188
        // Validate arguments
189 27
        $this->checkSubjectNodeHasSameOwner($node);
190 21
        $this->checkSubjectNodeIsOrphan($node);
191 15
        $this->checkReferenceNodeIsChild($before);
192
193
        // Update first child pointer (last child pointer is not affected)
194 14
        $this->updateFirstChildIfChanged($node, $before);
195
196
        // Update next sibling pointer of previous sibling of $before
197 14
        $this->setNodeNextSibling($before->previousSibling, $node);
198
199
        // Replace the child in the key map
200 14
        $this->children[$key] = $node;
201
202
        // Set references on new child
203 14
        $node->setReferences($this, $key, $before->previousSibling, $before);
204
205
        // Update references on ref child
206 14
        $before->setReferences($before->parent, $before->key, $node, $before->nextSibling);
207
208 14
        return $node;
209
    }
210
211
    /**
212
     * @throws WriteOperationForbiddenException
213
     * @throws InvalidSubjectNodeException
214
     * @throws InvalidReferenceNodeException
215
     */
216 21
    protected function replaceNode(Node $newNode, Node $oldNode): Node
217
    {
218
        // Prevent modifying a collection with an active iterator
219 21
        $this->checkWritable();
220
221
        // Validate arguments
222 19
        $this->checkSubjectNodeHasSameOwner($newNode);
223 13
        $this->checkSubjectNodeIsOrphan($newNode);
224 8
        $this->checkReferenceNodeIsChild($oldNode);
225
226
        // Update first/last child pointers
227 7
        $this->updateFirstChildIfChanged($newNode, $oldNode);
228 7
        $this->updateLastChildIfChanged($newNode, $oldNode);
229
230
        // Update sibling pointers of sibling nodes
231 7
        $this->setNodeNextSibling($oldNode->previousSibling, $newNode);
232 7
        $this->setNodePreviousSibling($oldNode->nextSibling, $newNode);
233
234
        // Replace the node in the key map
235 7
        $this->children[$oldNode->key] = $newNode;
236
237
        // Copy references from old node to new node
238 7
        $newNode->setReferences($oldNode->parent, $oldNode->key, $oldNode->previousSibling, $oldNode->nextSibling);
239
240
        // Clear references from old node
241 7
        $oldNode->setReferences(null, null, null, null);
242
243 7
        return $oldNode;
244
    }
245
246
    /**
247
     * @throws WriteOperationForbiddenException
248
     * @throws InvalidSubjectNodeException
249
     */
250 22
    protected function removeNode(Node $node): Node
251
    {
252
        // Prevent modifying a collection with an active iterator
253 22
        $this->checkWritable();
254
255
        // Validate arguments
256 20
        $this->checkSubjectNodeIsChild($node);
257
258
        // Update first/last child pointers
259 20
        $this->updateFirstChildIfChanged($node->nextSibling, $node);
260 20
        $this->updateLastChildIfChanged($node->previousSibling, $node);
261
262
        // Update sibling pointers of sibling nodes
263 20
        $this->setNodeNextSibling($node->previousSibling, $node->nextSibling);
264 20
        $this->setNodePreviousSibling($node->nextSibling, $node->previousSibling);
265
266
        // Remove the node from the key map
267 20
        unset($this->children[$node->key]);
268
269
        // Clear references from node
270 20
        $node->setReferences(null, null, null, null);
271
272 20
        return $node;
273
    }
274
275
    final public function __clone()
276
    {
277
        parent::__clone();
278
279
        $originalParent = $this->firstChild !== null
280
            ? $this->firstChild->parent
281
            : null;
282
283
        // Get an iterator for the original collection's child nodes
284
        $children = $originalParent !== null
285
            ? $originalParent->getIterator()
286
            : [];
287
288
        $this->resetChildReferenceProperties();
289
        $this->appendClonedChildNodes($children);
0 ignored issues
show
Bug introduced by
It seems like $children can also be of type DaveRandom\Jom\NodeListIterator; however, parameter $nodes of DaveRandom\Jom\VectorNod...ppendClonedChildNodes() 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

289
        $this->appendClonedChildNodes(/** @scrutinizer ignore-type */ $children);
Loading history...
290
    }
291
292
    final public function hasChildren(): bool
293
    {
294
        return !empty($this->children);
295
    }
296
297 27
    public function containsChild(Node $child): bool
298
    {
299
        do {
300 27
            if ($child->parent === $this) {
301 27
                return true;
302
            }
303 21
        } while (null !== $child = $child->parent);
304
305
        return false;
306
    }
307
308 18
    final public function getFirstChild(): ?Node
309
    {
310 18
        return $this->firstChild;
311
    }
312
313 12
    final public function getLastChild(): ?Node
314
    {
315 12
        return $this->lastChild;
316
    }
317
318
    final public function clear(): void
319
    {
320
        try {
321
            while ($this->lastChild !== null) {
322
                $this->removeNode($this->lastChild);
323
            }
324
        //@codeCoverageIgnoreStart
325
        } catch (\Exception $e) {
326
            /** @noinspection PhpInternalEntityUsedInspection */
327
            throw unexpected($e);
328
        }
329
        //@codeCoverageIgnoreEnd
330
    }
331
332
    final public function getIterator(): NodeListIterator
333
    {
334 9
        return new NodeListIterator($this->firstChild, function($state) {
335 9
            $this->activeIteratorCount += $state;
336 9
            \assert($this->activeIteratorCount >= 0, new \Error('Vector node active iterator count is negative'));
337 9
        });
338
    }
339
340
    final public function count(): int
341
    {
342
        return \count($this->children);
343
    }
344
345
    final public function jsonSerialize(): array
346
    {
347
        return \iterator_to_array($this->getIterator());
348
    }
349
350
    final public function offsetExists($key): bool
351
    {
352
        return isset($this->children[$key]);
353
    }
354
355
    /**
356
     * @throws WriteOperationForbiddenException
357
     * @throws InvalidSubjectNodeException
358
     */
359
    final public function offsetUnset($key): void
360
    {
361
        if (isset($this->children[$key])) {
362
            $this->removeNode($this->children[$key]);
363
        }
364
    }
365
366
    abstract public function toArray(): array;
367
    abstract public function offsetGet($index): Node;
368
    abstract public function offsetSet($index, $value): void;
369
}
370