Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
58 / 58
100.00% covered (success)
100.00%
21 / 21
CRAP
100.00% covered (success)
100.00%
1 / 1
Treenode
100.00% covered (success)
100.00%
58 / 58
100.00% covered (success)
100.00%
21 / 21
39
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 isEmpty
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hydrate
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 setNodeId
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 setParent
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
7
 getNodeId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getParentId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getParent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTree
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTreeId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getChildren
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getChildrenArray
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasChildren
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getChild
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getSiblings
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 isRoot
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isAncestorOf
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isDescendantOf
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 isLeaf
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getIndex
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setIndex
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3/**
4 * @author: Doug Wilbourne (dougwilbourne@gmail.com)
5 */
6
7declare (strict_types=1);
8
9namespace pvc\struct\tree\node;
10
11use pvc\interfaces\struct\collection\CollectionInterface;
12use pvc\interfaces\struct\collection\IndexedElementInterface;
13use pvc\interfaces\struct\dto\DtoInterface;
14use pvc\interfaces\struct\tree\node\TreenodeInterface;
15use pvc\interfaces\struct\tree\tree\TreeInterface;
16use pvc\interfaces\struct\treesearch\NodeVisitableInterface;
17use pvc\struct\collection\Collection;
18use pvc\struct\tree\err\AlreadySetNodeidException;
19use pvc\struct\tree\err\ChildCollectionException;
20use pvc\struct\tree\err\CircularGraphException;
21use pvc\struct\tree\err\InvalidNodeIdException;
22use pvc\struct\tree\err\InvalidParentNodeException;
23use pvc\struct\tree\err\InvalidValueException;
24use pvc\struct\tree\err\NodeNotEmptyHydrationException;
25use pvc\struct\tree\err\RootCannotBeMovedException;
26use pvc\struct\tree\err\SetTreeIdException;
27use pvc\struct\tree\tree\Tree;
28use pvc\struct\treesearch\VisitationTrait;
29
30/**
31 * @phpstan-import-type TreenodeDtoShape from TreenodeInterface
32 *
33 *  The nodeid property is immutable - the only way to set the nodeid is at hydration.  The same applies to the tree property,
34 *  which is set at construction time.
35 *
36 *  This means that there are no setters for these properties.  Together, these two design points ensure that nodes
37 *  cannot be hydrated except in the context of belonging to a tree.  That in turn makes it a bit easier to enforce the
38 *  design point that all nodeids in a tree must be unique.
39 *
40 *  The same is almost true for the parent property, but the difference is that the nodes are allowed to move around
41 *  within the same tree, e.g. you can change a node's parent as long as the new parent is in the same tree. It is
42 *  important to know that not only does a node keep a reference to its parent, but it also keeps a list of its
43 *  children.  So the setParent method is responsible not only for setting the parent property, but it also takes
44 *  the parent and adds a node to its child list.
45 */
46class Treenode implements TreenodeInterface, NodeVisitableInterface, IndexedElementInterface
47{
48    /**
49     * implement NodeVisitableInterface, make Treenodes available for iterable depth first search
50     */
51    use VisitationTrait;
52
53    /**
54     * unique id for this node
55     * @var non-negative-int $nodeid
56     */
57    protected int $nodeid;
58
59    /**
60     * reference to parent
61     * @var TreenodeInterface|null
62     */
63    protected ?TreenodeInterface $parent;
64
65    /**
66     * reference to containing tree
67     * @var TreeInterface
68     */
69    protected TreeInterface $tree;
70
71    /**
72     * @var non-negative-int
73     */
74    protected int $index;
75
76    /**
77     * @var CollectionInterface<TreenodeInterface> $children
78     */
79    public CollectionInterface $children;
80
81    /**
82     * @param CollectionInterface<TreenodeInterface> $collection
83     * @param TreeInterface $tree
84     * @throws ChildCollectionException
85     */
86    public function __construct(CollectionInterface $collection, TreeInterface $tree)
87    {
88        /**
89         * set the child collection
90         */
91        if (!$collection->isEmpty()) {
92            throw new ChildCollectionException();
93        } else {
94            $this->children = $collection;
95        }
96
97        $this->tree = $tree;
98    }
99
100    /**
101     * isEmpty
102     * @return bool
103     */
104    public function isEmpty(): bool
105    {
106        return is_null($this->nodeid ?? null);
107    }
108
109    /**
110     * @param TreenodeDtoShape&DtoInterface $dto
111     * @return void
112     * @throws AlreadySetNodeidException
113     * @throws CircularGraphException
114     * @throws InvalidNodeIdException
115     * @throws InvalidParentNodeException
116     * @throws NodeNotEmptyHydrationException
117     * @throws RootCannotBeMovedException
118     * @throws SetTreeIdException
119     * @throws InvalidValueException
120     */
121    public function hydrate(DtoInterface $dto): void
122    {
123        /**
124         * cannot hydrate a node if it already has been hydrated
125         */
126        if (!$this->isEmpty()) {
127            throw new NodeNotEmptyHydrationException($this->getNodeId());
128        }
129
130        /**
131         * set the nodeId
132         */
133        $this->setNodeId($dto->nodeId);
134
135        /**
136         * recall that the node is constructed such that the reference to its containing tree is already set.
137         * If the treeId of the dto is not null, verify that the dto tree id matches the tree id of the containing tree.
138         */
139        if (!is_null($dto->treeId) && ($dto->treeId != $this->getTreeId())) {
140            throw new SetTreeIdException();
141        }
142
143        /**
144         * setParent also sets the "reciprocal pointer" by adding this node to the child collection of the parent.
145         * We have to set the index first (if it is in the dto) because in the case of ordered collections,
146         * the collection will need to know what index to use when adding this node to the child collection of the
147         * parent.
148         */
149        if (isset($dto->index)) $this->setIndex($dto->index);
150        $this->setParent($dto->parentId);
151    }
152
153    protected function setNodeId(int $nodeId): void
154    {
155        /**
156         * nodeid must be non-negative.
157         */
158        if ($nodeId < 0) {
159            throw new InvalidNodeIdException($nodeId);
160        }
161
162        /**
163         * node id cannot already exist in the tree
164         */
165        if ($this->getTree()->getNode($nodeId)) {
166            throw new AlreadySetNodeidException($nodeId);
167        }
168        $this->nodeid = $nodeId;
169    }
170
171    /**
172     * @function setParent
173     * @param non-negative-int|null $parentId
174     */
175    public function setParent(?int $parentId): void
176    {
177        if (!is_null($parentId)) {
178            /**
179             * @var Treenode|null $parent
180             */
181            $parent = $this->getTree()->getNode($parentId);
182            if (is_null($parent)) {
183                /**
184                 * parent id is not null but there is no existing node in the tree with node id == parent id
185                 */
186                throw new InvalidParentNodeException($parentId);
187            }
188        } else {
189            /**
190             * parent id is null so parent is also null - it thinks it is the root node.  It is up to the containing
191             * tree to decide whether it really is or is not. See the setRoot method in Tree
192             */
193            $parent = null;
194        }
195
196        /**
197         * make sure we are not creating a circular graph
198         */
199        if ($parent && $parent->isDescendantOf($this)) {
200            throw new CircularGraphException($parent->getNodeId());
201        }
202
203        /**
204         * setParent is not just for construction - it is used to move nodes around in the tree as well.  If this
205         * node is the root node, then it cannot be moved in the tree
206         */
207        if ($this->tree->getRoot()?->getNodeId() === $this->getNodeId()) {
208            throw new RootCannotBeMovedException();
209        }
210
211        /**
212         * if parent is not null, add this node to the parent's child collection
213         */
214        if ($childCollection = $parent?->getChildren()) {
215            $childCollection->add($this->getNodeId(), $this);
216        }
217
218        /**
219         * set the parent
220         */
221        $this->parent = $parent;
222    }
223
224    /**
225     * @function getNodeId
226     * @return non-negative-int
227     */
228    public function getNodeId(): int
229    {
230        return $this->nodeid;
231    }
232
233    /**
234     * @function getParentId
235     * @return non-negative-int|null
236     */
237    public function getParentId(): ?int
238    {
239        return $this->getParent()?->getNodeId();
240    }
241
242    /**
243     * @function getParent
244     * @return TreenodeInterface|null
245     */
246    public function getParent(): ?TreenodeInterface
247    {
248        return $this->parent ?? null;
249    }
250
251    /**
252     * @function getTree
253     * @return TreeInterface
254     */
255    public function getTree(): TreeInterface
256    {
257        return $this->tree;
258    }
259
260    /**
261     * getTreeId
262     * @return non-negative-int
263     */
264    public function getTreeId(): int
265    {
266        return $this->getTree()->getTreeId();
267    }
268
269    /**
270     * @return CollectionInterface<TreenodeInterface>
271     */
272    public function getChildren(): CollectionInterface
273    {
274        return $this->children;
275    }
276
277    /**
278     * getChildrenAsArray
279     * @return array<TreenodeInterface>
280     */
281    public function getChildrenArray(): array
282    {
283        return $this->getChildren()->getElements();
284    }
285
286    /**
287     * @function hasChildren
288     * @return bool
289     */
290    public function hasChildren(): bool
291    {
292        return (!$this->children->isEmpty());
293    }
294
295    /**
296     * @function getChild
297     * @param non-negative-int $nodeid
298     * @return TreenodeInterface|null
299     */
300    public function getChild(int $nodeid): ?TreenodeInterface
301    {
302        foreach ($this->getChildren() as $child) {
303            if ($nodeid == $child->getNodeId()) {
304                return $child;
305            }
306        }
307        return null;
308    }
309
310    /**
311     * getSiblings returns a collection of this node's siblings
312     *
313     * @return CollectionInterface<TreenodeInterface>
314     */
315    public function getSiblings(): CollectionInterface
316    {
317        /**
318         * the root has no parent, so there is no existing child collection to get from a parent. We do have to go a
319         * long way to get to the collection factory so we can make a collection and add this node to it.
320         */
321        if ($this->getTree()->rootTest($this)) {
322            /** @var Collection<TreenodeInterface> $collection */
323            $collection = $this->getTree()->getTreenodeFactory()->getTreenodeCollectionFactory()->makeCollection([]);
324            $collection->add($this->getNodeId(), $this);
325        } else {
326            $parent = $this->getParent();
327            assert(!is_null($parent));
328            $collection = $parent->getChildren();
329        }
330        return $collection;
331    }
332
333    /**
334     * @return bool
335     */
336    public function isRoot(): bool
337    {
338        return ($this->tree->getRoot() === $this);
339    }
340
341    /**
342     * @function isAncestorOf
343     * @param Treenode $node
344     * @return bool
345     */
346    public function isAncestorOf(TreenodeInterface $node): bool
347    {
348        return $node->isDescendantOf($this);
349    }
350
351    /**
352     * @function isDescendantOf
353     * @param Treenode $node
354     * @return bool
355     */
356    public function isDescendantOf(TreenodeInterface $node): bool
357    {
358        if ($this->getParent() === $node) {
359            return true;
360        }
361        if (is_null($this->getParent())) {
362            return false;
363        } else {
364            return $this->getParent()->isDescendantOf($node);
365        }
366    }
367
368    /**
369     * @function isLeaf
370     * @return bool
371     */
372    public function isLeaf(): bool
373    {
374        return ($this->children->isEmpty());
375    }
376
377    /**
378     * @function getIndex returns the ordinal position of this node in its list of siblings
379     * @return non-negative-int
380     */
381    public function getIndex(): int
382    {
383        return $this->index;
384    }
385
386    /**
387     * sets this node's index property.  This method should never be called
388     * by any other class than the collection which contains the node.  The collection bears the responsibility
389     * of rationalizing/ordering the indices within the collection.  Nodes are unaware of their siblings in
390     * a direct way - they need to ask the parent for the sibling collection
391     *
392     * @function setIndex
393     * @param non-negative-int $index
394     */
395    public function setIndex(int $index): void
396    {
397        $this->index = $index;
398    }
399}