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