Completed
Push — master ( 5d9a5d...e6d18b )
by Chris
02:53
created

VectorNode::checkWritable()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 2
eloc 2
nc 2
nop 0
crap 2
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
    protected $activeIteratorCount = 0;
22
23
    /**
24
     * @throws WriteOperationForbiddenException
25
     */
26 68
    private function checkWritable(): void
27
    {
28 68
        if ($this->activeIteratorCount !== 0) {
29 2
            throw new WriteOperationForbiddenException('Cannot modify a vector with an active iterator');
30
        }
31
    }
32
33
    /**
34
     * @throws InvalidSubjectNodeException
35
     */
36 60
    private function checkSubjectNodeIsOrphan(Node $node): void
37
    {
38 60
        if ($node->parent !== null) {
39 11
            throw new InvalidSubjectNodeException('Node already present in the document');
40
        }
41
    }
42
43
    /**
44
     * @throws InvalidSubjectNodeException
45
     */
46 68
    private function checkSubjectNodeHasSameOwner(Node $node): void
47
    {
48 68
        if ($node->ownerDocument !== $this->ownerDocument) {
49 12
            throw new InvalidSubjectNodeException('Node belongs to a different document');
50
        }
51
52
    }
53
54
    /**
55
     * @throws InvalidSubjectNodeException
56
     */
57 17
    private function checkSubjectNodeIsChild(Node $node): void
58
    {
59 17
        if ($node->parent !== $this) {
60
            throw new InvalidSubjectNodeException('Node not present in children of this node');
61
        }
62
63
    }
64
65
    /**
66
     * @throws InvalidReferenceNodeException
67
     */
68 14
    private function checkReferenceNodeIsChild(Node $node): void
69
    {
70 14
        if ($node->parent !== $this) {
71 1
            throw new InvalidReferenceNodeException('Reference node not present in children of this node');
72
        }
73
74
    }
75
76
    /**
77
     * @throws InvalidKeyException
78
     */
79
    protected function resolveNode($nodeOrKey): Node
80
    {
81
        if ($nodeOrKey instanceof Node) {
82
            return $nodeOrKey;
83
        }
84
85
        if (isset($this->children[$nodeOrKey])) {
86
            return $this->children[$nodeOrKey];
87
        }
88
89
        throw new InvalidKeyException("{$nodeOrKey} does not reference a valid child node");
90
    }
91
92
    /**
93
     * @throws WriteOperationForbiddenException
94
     * @throws InvalidSubjectNodeException
95
     */
96 66
    protected function appendNode(Node $node, $key): Node
97
    {
98 66
        $this->checkWritable();
99 66
        $this->checkSubjectNodeHasSameOwner($node);
100 59
        $this->checkSubjectNodeIsOrphan($node);
101
102 59
        $node->parent = $this;
103 59
        $node->key = $key;
104 59
        $this->children[$key] = $node;
105
106 59
        $previous = $this->lastChild;
107
108 59
        $this->lastChild = $node;
109 59
        $this->firstChild = $this->firstChild ?? $node;
110
111 59
        $node->previousSibling = $previous;
112
113 59
        if ($previous) {
114 30
            $previous->nextSibling = $node;
115
        }
116
117 59
        return $node;
118
    }
119
120
    /**
121
     * @throws WriteOperationForbiddenException
122
     * @throws InvalidSubjectNodeException
123
     * @throws InvalidReferenceNodeException
124
     */
125 29
    protected function insertNode(Node $node, $key, Node $beforeNode = null): Node
126
    {
127 29
        if ($beforeNode === null) {
128 13
            return $this->appendNode($node, $key);
129
        }
130
131 25
        $this->checkWritable();
132 24
        $this->checkSubjectNodeHasSameOwner($node);
133 20
        $this->checkSubjectNodeIsOrphan($node);
134 14
        $this->checkReferenceNodeIsChild($beforeNode);
135
136 13
        $node->parent = $this;
137 13
        $node->key = $key;
138 13
        $this->children[$key] = $node;
139
140 13
        $node->nextSibling = $beforeNode;
141 13
        $beforeNode->previousSibling = $node;
142
143 13
        if ($this->firstChild === $beforeNode) {
144 13
            $this->firstChild = $node;
145
        }
146
147 13
        return $node;
148
    }
149
150
    /**
151
     * @throws WriteOperationForbiddenException
152
     * @throws InvalidSubjectNodeException
153
     * @throws InvalidReferenceNodeException
154
     */
155
    protected function replaceNode(Node $newNode, Node $oldNode): Node
156
    {
157
        $this->checkWritable();
158
        $this->checkSubjectNodeHasSameOwner($newNode);
159
        $this->checkSubjectNodeIsOrphan($newNode);
160
        $this->checkReferenceNodeIsChild($oldNode);
161
162
        $newNode->parent = $oldNode->parent;
163
        $newNode->previousSibling = $oldNode->previousSibling;
164
        $newNode->nextSibling = $oldNode->nextSibling;
165
166
        $newNode->key = $oldNode->key;
167
        $this->children[$oldNode->key] = $newNode;
168
169
        if ($oldNode->previousSibling) {
170
            $oldNode->previousSibling->nextSibling = $newNode;
171
        }
172
173
        if ($oldNode->nextSibling) {
174
            $oldNode->nextSibling->previousSibling = $newNode;
175
        }
176
177
        $oldNode->key = null;
178
        $oldNode->parent = null;
179
        $oldNode->previousSibling = null;
180
        $oldNode->nextSibling = null;
181
182
        return $oldNode;
183
    }
184
185
    /**
186
     * @throws WriteOperationForbiddenException
187
     * @throws InvalidSubjectNodeException
188
     */
189 17
    protected function removeNode(Node $node): Node
190
    {
191 17
        $this->checkWritable();
192 17
        $this->checkSubjectNodeIsChild($node);
193
194 17
        if ($this->firstChild === $node) {
195 15
            $this->firstChild = $node->nextSibling;
196
        }
197
198 17
        if ($this->lastChild === $node) {
199 15
            $this->lastChild = $node->previousSibling;
200
        }
201
202 17
        if ($node->previousSibling) {
203 8
            $node->previousSibling->nextSibling = $node->nextSibling;
204
        }
205
206 17
        if ($node->nextSibling) {
207 8
            $node->nextSibling->previousSibling = $node->previousSibling;
208
        }
209
210 17
        $node->parent = null;
211 17
        $node->previousSibling = null;
212 17
        $node->nextSibling = null;
213
214 17
        unset($this->children[$node->key]);
215 17
        $node->key = null;
216
217 17
        return $node;
218
    }
219
220
    public function hasChildren(): bool
221
    {
222
        return !empty($this->children);
223
    }
224
225 8
    public function getFirstChild(): ?Node
226
    {
227 8
        return $this->firstChild;
228
    }
229
230 8
    public function getLastChild(): ?Node
231
    {
232 8
        return $this->lastChild;
233
    }
234
235
    final public function clear(): void
236
    {
237
        try {
238
            while ($this->lastChild !== null) {
239
                $this->removeNode($this->lastChild);
240
            }
241
        //@codeCoverageIgnoreStart
242
        } catch (\Exception $e) {
243
            throw new \Error('Unexpected ' . \get_class($e) . ": {$e->getMessage()}", 0, $e);
244
        }
245
        //@codeCoverageIgnoreEnd
246
    }
247
248
    final public function getIterator(): NodeListIterator
249
    {
250 2
        return new NodeListIterator($this->firstChild, function($state) {
251 2
            $this->activeIteratorCount += $state;
252 2
            \assert($this->activeIteratorCount >= 0, new \Error('Vector node active iterator count is negative'));
253 2
        });
254
    }
255
256
    final public function count(): int
257
    {
258
        return \count($this->children);
259
    }
260
261
    final public function jsonSerialize(): array
262
    {
263
        return \iterator_to_array($this->getIterator());
264
    }
265
266
    public function offsetExists($key): bool
267
    {
268
        return isset($this->children[$key]);
269
    }
270
271
    /**
272
     * @throws WriteOperationForbiddenException
273
     * @throws InvalidSubjectNodeException
274
     */
275
    public function offsetUnset($key): void
276
    {
277
        if (isset($this->children[$key])) {
278
            $this->removeNode($this->children[$key]);
279
        }
280
    }
281
282
    abstract public function toArray(): array;
283
    abstract public function offsetGet($index): Node;
284
    abstract public function offsetSet($index, $value): void;
285
}
286