Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
75 / 75
100.00% covered (success)
100.00%
24 / 24
CRAP
100.00% covered (success)
100.00%
1 / 1
Treenode
100.00% covered (success)
100.00%
75 / 75
100.00% covered (success)
100.00%
24 / 24
48
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
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%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 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%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setParentId
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 setTreeId
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getIndex
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFirstChild
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getLastChild
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getNthChild
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getChildrenArray
100.00% covered (success)
100.00%
2 / 2
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
 getTree
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setTree
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 getParent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setParent
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
10
 isDescendantOf
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getChildren
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
 isAncestorOf
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isLeaf
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\tree\dto\TreenodeDtoInterface;
13use pvc\interfaces\struct\tree\node\TreenodeInterface;
14use pvc\interfaces\struct\tree\tree\TreeInterface;
15use pvc\struct\tree\err\ChildCollectionException;
16use pvc\struct\tree\err\CircularGraphException;
17use pvc\struct\tree\err\InvalidNodeIdException;
18use pvc\struct\tree\err\InvalidParentNodeIdException;
19use pvc\struct\tree\err\InvalidTreeidException;
20use pvc\struct\tree\err\NodeNotEmptyHydrationException;
21use pvc\struct\tree\err\RootCannotBeMovedException;
22use pvc\struct\tree\err\SetTreeException;
23use pvc\struct\treesearch\VisitationTrait;
24
25/**
26 *  The nodeid property is immutable - the only way to set the nodeid is at hydration.  The same applies to the treeId property,
27 *  which is set at construction time.
28 *
29 *  The same is almost true for the parent property, but the difference is that the nodes are allowed to move around
30 *  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
31 *  important to know that not only does a node keep a reference to its parent, but it also keeps a list of its
32 *  children.  So the setParent method is responsible not only for setting the parent property, but it also takes
33 *  the parent and adds a node to its child list.
34 *
35 * @template TreenodeType of TreenodeInterface
36 * @template CollectionType of CollectionInterface
37 * @template TreeType of TreeInterface
38 * @implements TreenodeInterface<TreenodeType, CollectionType, TreeType>
39 */
40class Treenode implements TreenodeInterface
41{
42    /**
43     * implement NodeVisitableInterface, make Treenodes available for iterable depth first search
44     */
45    use VisitationTrait;
46
47    /**
48     * @var CollectionType $children
49     */
50    public CollectionInterface $children;
51    /**
52     * unique id for this node
53     *
54     * @var non-negative-int $nodeid
55     */
56    protected int $nodeid;
57    /**
58     * @var non-negative-int|null
59     */
60    protected ?int $parentId = null;
61    /**
62     * @var non-negative-int
63     */
64    protected int $treeId;
65    /**
66     * reference to parent
67     *
68     * @var TreenodeType|null
69     */
70    protected ?TreenodeInterface $parent;
71    /**
72     * reference to containing tree
73     *
74     * @var TreeType
75     */
76    protected TreeInterface $tree;
77
78    /**
79     * @param  CollectionType  $collection
80     *
81     * @throws ChildCollectionException
82     */
83    public function __construct(CollectionInterface $collection)
84    {
85        /**
86         * set the child collection
87         */
88        if (!$collection->isEmpty()) {
89            throw new ChildCollectionException();
90        } else {
91            $this->children = $collection;
92        }
93    }
94
95    /**
96     * isEmpty
97     *
98     * @return bool
99     */
100    public function isEmpty(): bool
101    {
102        return is_null($this->nodeid ?? null);
103    }
104
105    /**
106     * @param  TreenodeDtoInterface  $dto
107     *
108     * @throws InvalidNodeIdException
109     * @throws InvalidParentNodeIdException
110     * @throws InvalidTreeidException
111     */
112    public function hydrate(TreenodeDtoInterface $dto): void
113    {
114        /**
115         * cannot hydrate a node if it already has been hydrated
116         */
117        if (!$this->isEmpty()) {
118            throw new NodeNotEmptyHydrationException($this->getNodeId());
119        }
120
121        /**
122         * set the nodeId, $parentId and $treeId
123         */
124        $this->setNodeId($dto->getNodeId());
125        $this->setParentId($dto->getParentId());
126        $this->setTreeId($dto->getTreeId());
127    }
128
129    /**
130     * @function getNodeId
131     * @return non-negative-int
132     */
133    public function getNodeId(): int
134    {
135        return $this->nodeid;
136    }
137
138    protected function setNodeId(int $nodeId): void
139    {
140        /**
141         * nodeid must be non-negative.
142         */
143        if ($nodeId < 0) {
144            throw new InvalidNodeIdException($nodeId);
145        }
146
147        $this->nodeid = $nodeId;
148    }
149
150    protected function setParentId(?int $parentId): void
151    {
152        if (($parentId !== null) && ($parentId < 0)) {
153            throw new InvalidParentNodeIdException($parentId);
154        }
155        $this->parentId = $parentId;
156    }
157
158    protected function setTreeId(?int $treeId): void
159    {
160        if ($treeId !== null) {
161            if ($treeId < 0) {
162                throw new InvalidTreeidException($treeId);
163            }
164            $this->treeId = $treeId;
165        }
166    }
167
168    public function getIndex(): ?int
169    {
170        return null;
171    }
172
173    /**
174     * @return TreenodeType|null
175     */
176    public function getFirstChild()
177    {
178        /** @var CollectionType<TreenodeType> $children */
179        $children = $this->getChildren();
180        return $children->getFirst();
181    }
182
183    /**
184     * @return TreenodeType|null
185     */
186    public function getLastChild()
187    {
188        /** @var CollectionType<TreenodeType> $children */
189        $children = $this->getChildren();
190        return $children->getLast();
191    }
192
193    /**
194     * @param  non-negative-int  $n
195     *
196     * @return TreenodeType|null
197     */
198    public function getNthChild(int $n)
199    {
200        /** @var CollectionType<TreenodeType> $children */
201        $children = $this->getChildren();
202        return $children->getNth($n);
203    }
204
205    /**
206     * getChildrenAsArray
207     *
208     * @return array<TreenodeType>
209     */
210    public function getChildrenArray(): array
211    {
212        /** @var array<TreenodeType> $result */
213        $result = $this->getChildren()->getElements();
214        return $result;
215    }
216
217    /**
218     * @function hasChildren
219     * @return bool
220     */
221    public function hasChildren(): bool
222    {
223        return (!$this->children->isEmpty());
224    }
225
226    /**
227     * @function getChild
228     *
229     * @param  non-negative-int  $nodeid
230     *
231     * @return TreenodeType|null
232     */
233    public function getChild(int $nodeid)
234    {
235        /** @var TreenodeType $child */
236        foreach ($this->getChildren() as $child) {
237            if ($nodeid == $child->getNodeId()) {
238                return $child;
239            }
240        }
241        return null;
242    }
243
244    /**
245     * getSiblings returns a collection of this node's siblings
246     *
247     * @return CollectionType
248     */
249    public function getSiblings(): CollectionInterface
250    {
251        /**
252         * the root has no parent, so there is no existing child collection to get from a parent. We do have to go a
253         * long way to get to the collection factory so we can make a collection and add this node to it.
254         */
255
256        if ($this->getTree()->rootTest($this)) {
257            /** @var CollectionType $collection */
258            $collection = $this->getTree()->makeCollection();
259            $collection->add($this->getNodeId(), $this);
260        } else {
261            $parent = $this->getParent();
262            assert(!is_null($parent));
263            /** @var CollectionType $collection */
264            $collection = $parent->getChildren();
265        }
266        return $collection;
267    }
268
269    /**
270     * @function getTree
271     * @return TreeType
272     */
273    public function getTree(): TreeInterface
274    {
275        return $this->tree;
276    }
277
278    /**
279     * @param  TreeType  $tree
280     *
281     * @return void
282     * @throws SetTreeException
283     */
284    public function setTree(TreeInterface $tree): void
285    {
286        /**
287         * tree property is immutable
288         */
289        if (isset($this->tree)) {
290            throw new SetTreeException($this->getNodeId());
291        }
292        /**
293         * if this node was hydrated from a dto, the treeId property might not
294         * be set.  if it is not, then adopt the treeId from the tree.
295         */
296        if (!isset($this->treeId)) {
297            $this->treeId = $tree->getTreeId();
298        }
299
300        /**
301         * ensure the treeId from the node matches the one from the tree
302         */
303        if ($this->treeId != $tree->getTreeId()) {
304            throw new SetTreeException($this->getNodeId());
305        }
306
307        /**
308         * set the reference
309         */
310        $this->tree = $tree;
311    }
312
313    /**
314     * @function getParent
315     * @return TreenodeType|null
316     * the parent reference is a convenience, a shortcut because we could
317     * always go to the tree and get the parent from the tree via the
318     * node's parentId property.  In a large tree, this reference could save
319     * a few cpu cycles....
320     */
321    public function getParent()
322    {
323        return $this->parent ?? null;
324    }
325
326    /**
327     * @param ?TreenodeType  $parent
328     *
329     * @return void
330     *
331     * two cases:  the first is this is called as part of this node being added
332     * to the tree.  In this case, the parent property is currently null.
333     *
334     * The second case is when you are trying to move this node within the tree.
335     * In this case, the parent is already set and the argument is intended
336     * to be the new parent.
337     */
338    public function setParent(?TreenodeInterface $parent): void
339    {
340        /**
341         * if parent is null, see if a parent node can be determined from the parentId
342         * property via the tree.  If not, throw an exception
343         */
344        if ($parent === null && $this->parentId !== null) {
345            /** @var TreenodeType|null $parent */
346            $parent = $this->tree->getNode($this->parentId);
347            if (!$parent) {
348                throw new InvalidParentNodeIdException($this->parentId);
349            }
350        }
351
352        /**
353         * if parent is not null, ensure parent is in the tree.  phpstan
354         * does not quite process this correctly unless it is written in a
355         * clumsy fashion with the typehint which appears redundant
356         */
357        if ($parent) {
358            /** @var non-negative-int $parentId */
359            $parentId = $parent->getNodeId();
360            if ($this->tree->getNode($parentId) === null) {
361                throw new InvalidParentNodeIdException($parentId);
362            }
363        }
364
365        /**
366         * ensure we are not creating a circular graph
367         */
368        if ($parent && $parent->isDescendantOf($this)) {
369            throw new CircularGraphException($parent->getNodeId());
370        }
371
372        /**
373         * setParent is not just for construction - it is used to move nodes around in the tree as well.  If this
374         * node is the root node, then it cannot be moved in the tree
375         */
376        if ($this->tree->getRoot()?->getNodeId() === $this->getNodeId()) {
377            throw new RootCannotBeMovedException();
378        }
379
380        /**
381         * if parent is not null, add this node to the parent's child collection
382         */
383        if ($childCollection = $parent?->getChildren()) {
384            $childCollection->add($this->getNodeId(), $this);
385        }
386
387        /**
388         * set the parent and the parentId
389         */
390        $this->parent = $parent;
391        $this->setParentId($parent?->getNodeId());
392    }
393
394    /**
395     * @function isDescendantOf
396     *
397     * @param  TreenodeType  $node
398     *
399     * @return bool
400     */
401    public function isDescendantOf(TreenodeInterface $node): bool
402    {
403        if ($this->getParent() === $node) {
404            return true;
405        }
406        if (is_null($this->getParent())) {
407            return false;
408        } else {
409            return $this->getParent()->isDescendantOf($node);
410        }
411    }
412
413    /**
414     * @return CollectionType
415     */
416    public function getChildren(): CollectionInterface
417    {
418        return $this->children;
419    }
420
421    /**
422     * @return bool
423     */
424    public function isRoot(): bool
425    {
426        return ($this->tree->getRoot() === $this);
427    }
428
429    /**
430     * @function isAncestorOf
431     *
432     * @param  TreenodeType  $node
433     *
434     * @return bool
435     */
436    public function isAncestorOf(TreenodeInterface $node): bool
437    {
438        return $node->isDescendantOf($this);
439    }
440
441    /**
442     * @function isLeaf
443     * @return bool
444     */
445    public function isLeaf(): bool
446    {
447        return ($this->children->isEmpty());
448    }
449}