Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
75.00% covered (warning)
75.00%
48 / 64
52.63% covered (warning)
52.63%
10 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
Treenode
75.00% covered (warning)
75.00%
48 / 64
52.63% covered (warning)
52.63%
10 / 19
52.06
0.00% covered (danger)
0.00%
0 / 1
 isInitialized
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setTree
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getNodeIdTester
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setNodeIdTester
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getNodeId
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 setNodeId
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
4.05
 setParent
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
7.01
 getParent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getParentId
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 hydrate
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 dehydrate
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 __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
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isRoot
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
 getChildren
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;
10
11use pvc\interfaces\struct\tree\dto\TreenodeDtoFactoryInterface;
12use pvc\interfaces\struct\tree\dto\TreenodeDtoInterface;
13use pvc\interfaces\struct\tree\node\TreenodeCollectionFactoryInterface;
14use pvc\interfaces\struct\tree\node\TreenodeCollectionInterface;
15use pvc\interfaces\struct\tree\node\TreenodeInterface;
16use pvc\interfaces\struct\tree\tree\TreeInterface;
17use pvc\interfaces\struct\types\id\IdTesterInterface;
18use pvc\struct\tree\err\AlreadySetNodeidException;
19use pvc\struct\tree\err\CircularGraphException;
20use pvc\struct\tree\err\InvalidNodeIdException;
21use pvc\struct\tree\err\InvalidParentNodeIdException;
22use pvc\struct\tree\err\InvalidTreeidException;
23use pvc\struct\tree\err\NodeNotEmptyHydrationException;
24use pvc\struct\tree\err\RootCannotBeMovedException;
25use pvc\struct\tree\err\SetTreeException;
26use pvc\struct\tree\err\NodeNotInitializedException;
27
28/**
29 * nodes are generic.  In order to make them useful, you will need to extend this class to create a specific
30 * node type (and extend the tree class as well).  Node types typically have a specific
31 * kind of 'payload'.  You will also need to implement factories and collection types.  See
32 * the example for guidance.
33 *
34 * The nodeId property is immutable - the only way to set the nodeId is at hydration.  NodeIds are
35 * typed as array-keys in this class
36 *
37 * nodes are allowed to move around
38 * 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
39 * important to know that not only does a node keep a reference to its parent, but it also keeps a list of its
40 * children.  So the setParent method is responsible not only for setting the parent property, but it also takes
41 * the parent and adds a node to its child list.
42 *
43 * Nodes cannot move between trees, so the tree property is immutable also.
44 *
45 * @template NodeIdType of array-key
46 * @template NodeType of TreenodeInterface
47 * @template TreeIdType of array-key
48 * @template TreeType of TreeInterface
49 * @template CollectionType of TreenodeCollectionInterface
50 * @implements TreenodeInterface<NodeIdType, NodeType, TreeIdType, TreeType, CollectionType>
51 */
52class Treenode implements TreenodeInterface
53{
54    protected bool $isInitialized = false;
55
56    public function isInitialized(): bool
57    {
58        return $this->isInitialized;
59    }
60
61    /**
62     * @var TreeType
63     */
64    protected $tree;
65
66    /**
67     * @param  TreeType  $tree
68     *
69     * @return void
70     * @throws SetTreeException
71     */
72    public function setTree($tree): void
73    {
74        /**
75         * $tree property is immutable
76         */
77        if ($this->tree !== null) {
78            throw new SetTreeException((string) $this->nodeId);
79        }
80        $this->tree = $tree;
81    }
82
83
84    /**
85     * unique id for this node
86     *
87     * @var NodeIdType $nodeId
88     */
89    protected $nodeId;
90
91    /**
92     * @var IdTesterInterface
93     * set this if you do not want to rely on a static type checker to ensure the consistency
94     * of the data type of the nodeId.  NodeIds can be either strings or non-negative integers.
95     */
96    protected IdTesterInterface $nodeIdTester;
97
98    public function getNodeIdTester(): ?IdTesterInterface
99    {
100        return $this->nodeIdTester ?? null;
101    }
102
103    public function setNodeIdTester(IdTesterInterface $nodeIdTester): void
104    {
105        $this->nodeIdTester = $nodeIdTester;
106    }
107
108    /**
109     * getNodeId
110     * @return NodeIdType
111     */
112    public function getNodeId(): int|string
113    {
114        if (!$this->isInitialized) {
115            throw new NodeNotInitializedException();
116        }
117        return $this->nodeId;
118    }
119
120    /**
121     * setNodeId
122     * @param NodeIdType $nodeId
123     *
124     * @return void
125     */
126    protected function setNodeId($nodeId): void
127    {
128        if ($this->getNodeIdTester()?->testIdType($nodeId) === false) {
129            throw new InvalidNodeIdException();
130        }
131
132        /**
133         * nodeId is immutable
134         */
135        if ($this->nodeId !== null) {
136            throw new NodeNotEmptyHydrationException($nodeId);
137        }
138
139        /**
140         * node cannot already exist in the tree
141         */
142        if ($this->tree->getNode($nodeId) !== null) {
143            throw new AlreadySetNodeidException((string) $nodeId);
144        }
145
146        $this->nodeId = $nodeId;
147    }
148
149
150    /**
151     * reference to parent
152     *
153     * @var NodeType|null
154     */
155    protected ?TreenodeInterface $parent;
156
157    /**
158     * @param ?NodeIdType  $parentId
159     *
160     * @return void
161     *
162     * two cases:  the first is when this is called as part of this node being added
163     * to the tree.  In this case, the parent property is currently null.
164     *
165     * The second case is when you are trying to move this node within the tree.
166     * In this case, the parent is already set and the argument is intended
167     * to be the new parent.
168     */
169    public function setParent($parentId): void
170    {
171        if (!$this->isInitialized()) {
172            throw new NodeNotInitializedException();
173        }
174
175        if ($parentId !== null) {
176
177            /**
178             * ensure parent is in the tree
179             */
180            if ($this->tree->getNode($parentId) === null) {
181                throw new InvalidParentNodeIdException((string)$parentId);
182            }
183
184            /** @var NodeType $parent */
185            $parent = $this->tree->getNode($parentId);
186        } else {
187            $parent = null;
188        }
189
190        if ($parent !== null) {
191            /**
192             * ensure we are not creating a circular graph
193             */
194            if ($parent->isDescendantOf($this)) {
195                throw new CircularGraphException((string) $parentId);
196            }
197
198            /**
199             * ensure we are not trying to move the root node
200             */
201            if ($this->tree->getRoot() === $this) {
202                throw new RootCannotBeMovedException();
203            }
204
205            /**
206             * if parent is not null, add this node to the parent's child collection
207             */
208            $childCollection = $parent->getChildren();
209            $childCollection->add($this->getNodeId(), $this);
210        }
211
212        /**
213         * set the parent.  If the parent is null, the tree will handle setting it
214         * as the root of the tree
215         */
216        $this->parent = $parent;
217    }
218
219    /**
220     * @function getParent
221     * @return NodeType|null
222     */
223    public function getParent(): ?TreenodeInterface
224    {
225        return $this->parent ?? null;
226    }
227
228    /**
229     * getParentId
230     * @return NodeIdType|null
231     */
232    public function getParentId(): int|string|null
233    {
234        /** @var NodeIdType|null $parentId */
235        $parentId = $this->getParent()?->getNodeId();
236        return $parentId;
237    }
238
239    /**
240     * @var CollectionType $children
241     */
242    protected $children;
243
244
245    /**
246     * hydrate
247     * @param  TreenodeDtoInterface<NodeIdType, TreeIdType>  $dto
248     *
249     * @return void
250     */
251    public function hydrate(TreenodeDtoInterface $dto): void
252    {
253        if ($dto->getTreeId() !== $this->tree->getTreeId()) {
254            throw new InvalidTreeIdException();
255        }
256        $this->setNodeId($dto->getNodeId());
257        /**
258         * once tree reference is validated and nodeId is set, node is
259         * initialized.
260         */
261        $this->isInitialized = true;
262        $this->setParent($dto->getParentId());
263
264    }
265
266    /**
267     * dehydrate
268     * @return TreenodeDtoInterface<NodeIdType, TreeIdType>
269     */
270    public function dehydrate(): TreenodeDtoInterface
271    {
272        $dto = $this->dtoFactory->makeTreenodeDto();
273        $dto->setNodeId($this->getNodeId());
274
275        /** @var NodeIdType $parentId */
276        $parentId = $this->getParent()?->getNodeId();
277        $dto->setParentId($parentId);
278
279        /** @var TreeIdType $treeId */
280        $treeId = $this->tree->getTreeId();
281        $dto->setTreeId($treeId);
282
283        return $dto;
284    }
285
286    /**
287     * @param  TreenodeCollectionFactoryInterface<CollectionType>  $collectionFactory
288     * @param TreenodeDtoFactoryInterface<NodeIdType, TreeIdType> $dtoFactory
289     */
290    public function __construct(
291        protected TreenodeCollectionFactoryInterface $collectionFactory,
292        protected TreenodeDtoFactoryInterface $dtoFactory,
293    )
294    {
295        $this->children = $this->collectionFactory->makeCollection();
296    }
297
298    /**
299     * methods describing the nature of the node
300     */
301
302    /**
303     * @function isDescendantOf
304     *
305     * @param  NodeType  $node
306     *
307     * @return bool
308     */
309    public function isDescendantOf($node): bool
310    {
311        if ($this->getParent() === $node) {
312            return true;
313        }
314        if (is_null($this->getParent())) {
315            return false;
316        } else {
317            return $this->getParent()->isDescendantOf($node);
318        }
319    }
320
321    /**
322     * @function isAncestorOf
323     *
324     * @param  NodeType  $node
325     *
326     * @return bool
327     */
328    public function isAncestorOf($node): bool
329    {
330        return $node->isDescendantOf($this);
331    }
332
333    public function isRoot(): bool
334    {
335        return $this->tree->getRoot() === $this;
336    }
337
338    /**
339     * @function hasChildren
340     * @return bool
341     */
342    public function hasChildren(): bool
343    {
344        return !$this->children->isEmpty();
345    }
346
347    /**
348     * @function getChild
349     *
350     * @param  NodeIdType  $nodeId
351     *
352     * @return NodeType|null
353     */
354    public function getChild($nodeId)
355    {
356        return $this->children->getNode($nodeId);
357    }
358
359    /**
360     * @return CollectionType
361     */
362    public function getChildren()
363    {
364        return $this->children;
365    }
366
367    /**
368     * getSiblings returns a collection of this node's siblings
369     *
370     * @return CollectionType
371     */
372    public function getSiblings()
373    {
374        /**
375         * the root has no parent, so there is no existing child collection to get from a parent.
376         * Not sure why phpstan needs the type hinting.......
377         */
378        if ($this->isRoot()) {
379            /** @var CollectionType $collection */
380            $collection = $this->collectionFactory->makeCollection();
381            $collection->add($this->getNodeId(), $this);
382        } else {
383            $parent = $this->getParent();
384            assert(!is_null($parent));
385            /** @var CollectionType $collection */
386            $collection = $parent->getChildren();
387        }
388        return $collection;
389    }
390
391}