Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.36% covered (warning)
86.36%
57 / 66
84.21% covered (warning)
84.21%
16 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
Treenode
86.36% covered (warning)
86.36%
57 / 66
84.21% covered (warning)
84.21%
16 / 19
38.11
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
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getNodeIdTester
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setNodeIdTester
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNodeId
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setNodeId
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 setParent
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
7
 getParent
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getParentId
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 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        if (!$this->isInitialized) {
226            throw new NodeNotInitializedException();
227        }
228        return $this->parent ?? null;
229    }
230
231    /**
232     * getParentId
233     * @return NodeIdType|null
234     */
235    public function getParentId(): int|string|null
236    {
237        /** @var NodeIdType|null $parentId */
238        $parentId = $this->getParent()?->getNodeId();
239        return $parentId;
240    }
241
242    /**
243     * @var CollectionType $children
244     */
245    protected $children;
246
247
248    /**
249     * hydrate
250     * @param  TreenodeDtoInterface<NodeIdType, TreeIdType>  $dto
251     *
252     * @return void
253     */
254    public function hydrate(TreenodeDtoInterface $dto): void
255    {
256        if ($dto->getTreeId() !== $this->tree->getTreeId()) {
257            throw new InvalidTreeIdException();
258        }
259        $this->setNodeId($dto->getNodeId());
260        /**
261         * once tree reference is validated and nodeId is set, node is
262         * initialized.
263         */
264        $this->isInitialized = true;
265        $this->setParent($dto->getParentId());
266
267    }
268
269    /**
270     * dehydrate
271     * @return TreenodeDtoInterface<NodeIdType, TreeIdType>
272     */
273    public function dehydrate(): TreenodeDtoInterface
274    {
275        $dto = $this->dtoFactory->makeTreenodeDto();
276        $dto->setNodeId($this->getNodeId());
277
278        /** @var NodeIdType $parentId */
279        $parentId = $this->getParent()?->getNodeId();
280        $dto->setParentId($parentId);
281
282        /** @var TreeIdType $treeId */
283        $treeId = $this->tree->getTreeId();
284        $dto->setTreeId($treeId);
285
286        return $dto;
287    }
288
289    /**
290     * @param  TreenodeCollectionFactoryInterface<CollectionType>  $collectionFactory
291     * @param TreenodeDtoFactoryInterface<NodeIdType, TreeIdType> $dtoFactory
292     */
293    public function __construct(
294        protected TreenodeCollectionFactoryInterface $collectionFactory,
295        protected TreenodeDtoFactoryInterface $dtoFactory,
296    )
297    {
298        $this->children = $this->collectionFactory->makeCollection();
299    }
300
301    /**
302     * methods describing the nature of the node
303     */
304
305    /**
306     * @function isDescendantOf
307     *
308     * @param  NodeType  $node
309     *
310     * @return bool
311     */
312    public function isDescendantOf($node): bool
313    {
314        if ($this->getParent() === $node) {
315            return true;
316        }
317        if (is_null($this->getParent())) {
318            return false;
319        } else {
320            return $this->getParent()->isDescendantOf($node);
321        }
322    }
323
324    /**
325     * @function isAncestorOf
326     *
327     * @param  NodeType  $node
328     *
329     * @return bool
330     */
331    public function isAncestorOf($node): bool
332    {
333        return $node->isDescendantOf($this);
334    }
335
336    public function isRoot(): bool
337    {
338        return $this->tree->getRoot() === $this;
339    }
340
341    /**
342     * @function hasChildren
343     * @return bool
344     */
345    public function hasChildren(): bool
346    {
347        return !$this->children->isEmpty();
348    }
349
350    /**
351     * @function getChild
352     *
353     * @param  NodeIdType  $nodeId
354     *
355     * @return NodeType|null
356     */
357    public function getChild($nodeId)
358    {
359        return $this->children->getNode($nodeId);
360    }
361
362    /**
363     * @return CollectionType
364     */
365    public function getChildren()
366    {
367        return $this->children;
368    }
369
370    /**
371     * getSiblings returns a collection of this node's siblings
372     *
373     * @return CollectionType
374     */
375    public function getSiblings()
376    {
377        /**
378         * the root has no parent, so there is no existing child collection to get from a parent.
379         * Not sure why phpstan needs the type hinting.......
380         */
381        if ($this->isRoot()) {
382            /** @var CollectionType $collection */
383            $collection = $this->collectionFactory->makeCollection();
384            $collection->add($this->getNodeId(), $this);
385        } else {
386            $parent = $this->getParent();
387            assert(!is_null($parent));
388            /** @var CollectionType $collection */
389            $collection = $parent->getChildren();
390        }
391        return $collection;
392    }
393
394}