Completed
Push — master ( 035c9e...dee7d0 )
by Chris
02:59
created

VectorNode   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 350
Duplicated Lines 0 %

Test Coverage

Coverage 72.72%

Importance

Changes 0
Metric Value
wmc 46
dl 0
loc 350
ccs 88
cts 121
cp 0.7272
rs 8.3999
c 0
b 0
f 0

25 Methods

Rating   Name   Duplication   Size   Complexity  
A checkReferenceNodeIsChild() 0 4 2
B appendNode() 0 24 1
A checkSubjectNodeIsChild() 0 4 2
A checkWritable() 0 4 2
A setNodePreviousSibling() 0 4 2
A checkSubjectNodeIsOrphan() 0 4 2
A removeNode() 0 23 1
A setNodeNextSibling() 0 4 2
A checkSubjectNodeHasSameOwner() 0 4 2
B insertNode() 0 31 2
A updateFirstChildIfChanged() 0 4 2
B replaceNode() 0 28 1
A resolveNode() 0 11 3
A updateLastChildIfChanged() 0 4 2
A clear() 0 9 3
A getLastChild() 0 3 1
A jsonSerialize() 0 3 1
A getIterator() 0 5 1
A offsetUnset() 0 4 2
A containsChild() 0 9 3
A getFirstChild() 0 3 1
A hasChildren() 0 3 1
B __clone() 0 26 5
A count() 0 3 1
A offsetExists() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like VectorNode often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use VectorNode, and based on these observations, apply Extract Interface, too.

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
    /**
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 159
    protected function appendNode(Node $node, $key): Node
126
    {
127
        // Prevent modifying a collection with an active iterator
128 159
        $this->checkWritable();
129
130
        // Validate arguments
131 159
        $this->checkSubjectNodeHasSameOwner($node);
132 151
        $this->checkSubjectNodeIsOrphan($node);
133
134
        // Update first/last child pointers
135 151
        $this->updateFirstChildIfChanged($node, null);
136 151
        $previousSibling = $this->lastChild;
137 151
        $this->lastChild = $node;
138
139
        // Update next sibling pointer of old $lastChild (no next sibling node to update)
140 151
        $this->setNodeNextSibling($previousSibling, $node);
141
142
        // Add the child to the key map
143 151
        $this->children[$key] = $node;
144
145
        // Set references on new child
146 151
        $node->setReferences($this, $key, $previousSibling, null);
147
148 151
        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
    final public function __clone()
254
    {
255
        parent::__clone();
256
257
        $originalParent = $this->firstChild !== null
258
            ? $this->firstChild->parent
259
            : null;
260
261
        // Get an iterator for the original collection's child nodes
262
        $children = $originalParent !== null
263
            ? $originalParent->getIterator()
264
            : [];
265
266
        // Reset the child ref properties
267
        $this->firstChild = null;
268
        $this->lastChild = null;
269
        $this->children = [];
270
        $this->activeIteratorCount = 0;
271
272
        // Loop the original child nodes and append clones of them
273
        foreach ($children as $key => $child) {
274
            try {
275
                $this->appendNode(clone $child, $key);
276
            //@codeCoverageIgnoreStart
277
            } catch (\Exception $e) {
278
                throw unexpected($e);
279
            }
280
            //@codeCoverageIgnoreEnd
281
        }
282
    }
283
284
    final public function hasChildren(): bool
285
    {
286
        return !empty($this->children);
287
    }
288
289 27
    public function containsChild(Node $child): bool
290
    {
291
        do {
292 27
            if ($child->parent === $this) {
293 27
                return true;
294
            }
295 21
        } while (null !== $child = $child->parent);
296
297
        return false;
298
    }
299
300 18
    final public function getFirstChild(): ?Node
301
    {
302 18
        return $this->firstChild;
303
    }
304
305 12
    final public function getLastChild(): ?Node
306
    {
307 12
        return $this->lastChild;
308
    }
309
310
    final public function clear(): void
311
    {
312
        try {
313
            while ($this->lastChild !== null) {
314
                $this->removeNode($this->lastChild);
315
            }
316
        //@codeCoverageIgnoreStart
317
        } catch (\Exception $e) {
318
            throw unexpected($e);
319
        }
320
        //@codeCoverageIgnoreEnd
321
    }
322
323
    final public function getIterator(): NodeListIterator
324
    {
325 9
        return new NodeListIterator($this->firstChild, function($state) {
326 9
            $this->activeIteratorCount += $state;
327 9
            \assert($this->activeIteratorCount >= 0, new \Error('Vector node active iterator count is negative'));
328 9
        });
329
    }
330
331
    final public function count(): int
332
    {
333
        return \count($this->children);
334
    }
335
336
    final public function jsonSerialize(): array
337
    {
338
        return \iterator_to_array($this->getIterator());
339
    }
340
341
    final public function offsetExists($key): bool
342
    {
343
        return isset($this->children[$key]);
344
    }
345
346
    /**
347
     * @throws WriteOperationForbiddenException
348
     * @throws InvalidSubjectNodeException
349
     */
350
    final public function offsetUnset($key): void
351
    {
352
        if (isset($this->children[$key])) {
353
            $this->removeNode($this->children[$key]);
354
        }
355
    }
356
357
    abstract public function toArray(): array;
358
    abstract public function offsetGet($index): Node;
359
    abstract public function offsetSet($index, $value): void;
360
}
361