Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
45 / 45
100.00% covered (success)
100.00%
19 / 19
CRAP
100.00% covered (success)
100.00%
1 / 1
Treenode
100.00% covered (success)
100.00%
45 / 45
100.00% covered (success)
100.00%
19 / 19
29
100.00% covered (success)
100.00%
1 / 1
 getNodeId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setNodeId
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 setTree
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setParent
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 getParent
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
 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
 __construct
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
 isAncestorOf
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isRoot
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFirstChild
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLastChild
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNthChild
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%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSiblings
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
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\tree\node\TreenodeChildCollectionFactoryInterface;
12use pvc\interfaces\struct\tree\node\TreenodeChildCollectionInterface;
13use pvc\interfaces\struct\tree\node\TreenodeInterface;
14use pvc\interfaces\struct\tree\tree\TreeInterface;
15use pvc\struct\tree\err\CircularGraphException;
16use pvc\struct\tree\err\InvalidNodeIdException;
17use pvc\struct\tree\err\InvalidParentNodeIdException;
18use pvc\struct\tree\err\NodeNotEmptyHydrationException;
19use pvc\struct\tree\err\RootCannotBeMovedException;
20use pvc\struct\tree\err\SetTreeException;
21use pvc\struct\treesearch\VisitationTrait;
22
23/**
24 * nodes are generic.  In order to make them useful, you will need to extend this class to create a specific
25 * node type (and extend the tree and treenodefactory classes as well).  Node types typically have a specific
26 * kind of payload.
27 *
28 *  The nodeId property is immutable - the only way to set the nodeId is at hydration.
29 *
30 *  nodes are allowed to move around
31 *  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
32 *  important to know that not only does a node keep a reference to its parent, but it also keeps a list of its
33 *  children.  So the setParent method is responsible not only for setting the parent property, but it also takes
34 *  the parent and adds a node to its child list.
35 *
36 * @template TreenodeType of TreenodeInterface
37 * @implements TreenodeInterface<TreenodeType>
38 */
39class Treenode implements TreenodeInterface
40{
41    /**
42     * implement NodeVisitableInterface, make Treenodes available for iterable depth first search
43     */
44    use VisitationTrait;
45
46    /**
47     * unique id for this node
48     *
49     * @var non-negative-int $nodeId
50     */
51    protected int $nodeId;
52
53    /**
54     * @function getNodeId
55     * @return non-negative-int
56     */
57    public function getNodeId(): int
58    {
59        return $this->nodeId;
60    }
61
62    public function setNodeId(int $nodeId): void
63    {
64        /**
65         * nodeId must be non-negative.
66         */
67        if ($nodeId < 0) {
68            throw new InvalidNodeIdException($nodeId);
69        }
70
71        /**
72         * nodeId is immutable
73         */
74        if (isset($this->nodeId)) {
75            throw new NodeNotEmptyHydrationException($nodeId);
76        }
77
78        $this->nodeId = $nodeId;
79    }
80
81    /**
82     * @var TreeInterface<TreenodeType>
83     */
84    protected TreeInterface $tree;
85
86    /**
87     * @param  TreeInterface<TreenodeType>  $tree
88     *
89     * @return void
90     * @throws SetTreeException
91     */
92    public function setTree(TreeInterface $tree): void
93    {
94        /**
95         * $tree property is immutable
96         */
97        if (isset($this->tree)) {
98            throw new SetTreeException($this->nodeId);
99        }
100        $this->tree = $tree;
101    }
102
103    /**
104     * reference to parent
105     *
106     * @var TreenodeType|null
107     */
108    protected ?TreenodeInterface $parent;
109
110    /**
111     * @param ?TreenodeType  $parent
112     *
113     * @return void
114     *
115     * two cases:  the first is this is called as part of this node being added
116     * to the tree.  In this case, the parent property is currently null.
117     *
118     * The second case is when you are trying to move this node within the tree.
119     * In this case, the parent is already set and the argument is intended
120     * to be the new parent.
121     */
122    public function setParent(?TreenodeInterface $parent): void
123    {
124        if ($parent) {
125            $parentId = $parent->getNodeId();
126
127            /**
128             * ensure parent is in the tree
129             */
130            if ($this->tree->getNode($parentId) === null) {
131                throw new InvalidParentNodeIdException($parentId);
132            }
133
134            /**
135             * ensure we are not creating a circular graph
136             */
137            if ($parent->isDescendantOf($this)) {
138                throw new CircularGraphException($parent->getNodeId());
139            }
140
141            /**
142             * ensure we are not trying to move the root node
143             */
144            if ($this->tree->getRoot() === $this) {
145                throw new RootCannotBeMovedException();
146            }
147
148            /**
149             * if parent is not null, add this node to the parent's child collection
150             */
151            $childCollection = $parent->getChildren();
152            $childCollection->add($this->getNodeId(), $this);
153        }
154
155        /**
156         * set the parent.  If the parent is null, the tree will handle setting it
157         * as the root of the tree
158         */
159        $this->parent = $parent;
160    }
161
162    /**
163     * @function getParent
164     * @return TreenodeType|null
165     */
166    public function getParent(): ?TreenodeInterface
167    {
168        return $this->parent ?? null;
169    }
170
171
172    /**
173     * @var TreenodeChildCollectionInterface<TreenodeType> $children
174     */
175    protected TreenodeChildCollectionInterface $children;
176
177    /**
178     * @return TreenodeChildCollectionInterface<TreenodeType>
179     */
180    public function getChildren(): TreenodeChildCollectionInterface
181    {
182        return $this->children;
183    }
184
185    /**
186     * @var non-negative-int
187     */
188    protected int $index;
189
190    public function getIndex(): int
191    {
192        return $this->index;
193    }
194
195    public function setIndex(int $index): void
196    {
197        $this->index = $index;
198    }
199
200    /**
201     * @param  TreenodeChildCollectionFactoryInterface<TreenodeType>  $collectionFactory
202     */
203    public function __construct(protected TreenodeChildCollectionFactoryInterface $collectionFactory)
204    {
205        $this->children = $this->collectionFactory->makeChildCollection();
206    }
207
208    /**
209     * methods describing the nature of the node
210     */
211
212    /**
213     * @function isDescendantOf
214     *
215     * @param  TreenodeType  $node
216     *
217     * @return bool
218     */
219    public function isDescendantOf(TreenodeInterface $node): bool
220    {
221        if ($this->getParent() === $node) {
222            return true;
223        }
224        if (is_null($this->getParent())) {
225            return false;
226        } else {
227            return $this->getParent()->isDescendantOf($node);
228        }
229    }
230
231    /**
232     * @function isAncestorOf
233     *
234     * @param  TreenodeType  $node
235     *
236     * @return bool
237     */
238    public function isAncestorOf(TreenodeInterface $node): bool
239    {
240        return $node->isDescendantOf($this);
241    }
242
243    public function isRoot(): bool
244    {
245        return $this->tree->getRoot() === $this;
246    }
247
248
249    /**
250     * @return TreenodeType|null
251     */
252    public function getFirstChild(): ?TreenodeInterface
253    {
254        return $this->getChildren()->getFirst();
255    }
256
257    /**
258     * @return TreenodeType|null
259     */
260    public function getLastChild(): ?TreenodeInterface
261    {
262        return $this->getChildren()->getLast();
263    }
264
265    /**
266     * @param  non-negative-int  $n
267     *
268     * @return TreenodeType|null
269     */
270    public function getNthChild(int $n): ?TreenodeInterface
271    {
272        return $this->getChildren()->getNth($n);
273    }
274
275    /**
276     * getChildrenArray
277     *
278     * @return array<non-negative-int, TreenodeType>
279     */
280    public function getChildrenArray(): array
281    {
282        return $this->getChildren()->getElements();
283    }
284
285    /**
286     * @function hasChildren
287     * @return bool
288     */
289    public function hasChildren(): bool
290    {
291        return (!$this->children->isEmpty());
292    }
293
294    /**
295     * @function getChild
296     *
297     * @param  non-negative-int  $nodeId
298     *
299     * @return TreenodeType|null
300     */
301    public function getChild(int $nodeId): ?TreenodeInterface
302    {
303        return $this->children->getElement($nodeId);
304    }
305
306    /**
307     * getSiblings returns a collection of this node's siblings
308     *
309     * @return TreenodeChildCollectionInterface<TreenodeType>
310     */
311    public function getSiblings(): TreenodeChildCollectionInterface
312    {
313        /**
314         * the root has no parent, so there is no existing child collection to get from a parent.
315         * Not sure why phpstan needs the type hinting.......
316         */
317        if ($this->isRoot()) {
318            /** @var TreenodeChildCollection<TreenodeType> $collection */
319            $collection = $this->collectionFactory->makeChildCollection();
320            $collection->add($this->getNodeId(), $this);
321        } else {
322            $parent = $this->getParent();
323            assert(!is_null($parent));
324            /** @var TreenodeChildCollection<TreenodeType> $collection */
325            $collection = $parent->getChildren();
326        }
327        return $collection;
328    }
329}