Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
63 / 63
100.00% covered (success)
100.00%
17 / 17
CRAP
100.00% covered (success)
100.00%
1 / 1
Tree
100.00% covered (success)
100.00%
63 / 63
100.00% covered (success)
100.00%
17 / 17
36
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 initialize
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 setTreeId
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getTreeId
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 rootTest
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setRoot
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getRoot
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isEmpty
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 makeNode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addNode
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 deleteNode
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 deleteNodeRecurse
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 hydrate
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 insertDtoRecurse
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 dehydrate
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getNodeCollection
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNode
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;
10
11use pvc\err\stock\Exception;
12use pvc\interfaces\struct\tree\dto\TreenodeDtoCollectionFactoryInterface;
13use pvc\interfaces\struct\tree\dto\TreenodeDtoCollectionInterface;
14use pvc\interfaces\struct\tree\dto\TreenodeDtoInterface;
15use pvc\interfaces\struct\tree\node\TreenodeCollectionFactoryInterface;
16use pvc\interfaces\struct\tree\node\TreenodeCollectionInterface;
17use pvc\interfaces\struct\tree\node\TreenodeFactoryInterface;
18use pvc\interfaces\struct\tree\node\TreenodeInterface;
19use pvc\interfaces\struct\tree\tree\TreeInterface;
20use pvc\interfaces\struct\types\id\IdTypeInterface;
21use pvc\struct\collection\err\DuplicateKeyException;
22use pvc\struct\tree\err\AddNodeException;
23use pvc\struct\tree\err\AlreadySetNodeidException;
24use pvc\struct\tree\err\AlreadySetRootException;
25use pvc\struct\tree\err\DeleteInteriorNodeException;
26use pvc\struct\tree\err\DtoCollectionException;
27use pvc\struct\tree\err\InvalidTreeidException;
28use pvc\struct\tree\err\NodeNotInitializedException;
29use pvc\struct\tree\err\NodeNotInTreeException;
30use pvc\struct\tree\err\RootCountException;
31use pvc\struct\tree\err\TreeNotInitializedException;
32use pvc\struct\types\id\IdTypeTrait;
33
34/**
35 * @class Tree
36 *
37 * @template NodeIdType of array-key
38 * @template NodeType of TreenodeInterface
39 * @template TreeIdType of array-key
40 * @template TreeType of TreeInterface
41 * @implements TreeInterface<NodeIdType, NodeType, TreeIdType, TreeType>
42 */
43class Tree implements TreeInterface, IdTypeInterface
44{
45    /**
46     * @var bool
47     */
48    protected bool $isInitialized;
49
50    /**
51     * @var TreeIdType
52     */
53    protected $treeId;
54
55    use IdTypeTrait;
56
57    /**
58     * @var NodeType|null
59     */
60    protected TreenodeInterface|null $root = null;
61
62    /**
63     * @var TreenodeCollectionInterface<NodeIdType, NodeType>
64     */
65    protected TreenodeCollectionInterface $nodeCollection;
66
67    /**
68     * @param TreenodeFactoryInterface<NodeType>  $treenodeFactory
69     * @param TreenodeCollectionFactoryInterface<TreenodeCollectionInterface<NodeIdType, NodeType>> $collectionFactory
70     * @param TreenodeDtoCollectionFactoryInterface<NodeIdType, TreeIdType> $dtoCollectionFactory
71     */
72    public function __construct(
73        protected TreenodeFactoryInterface $treenodeFactory,
74        protected TreenodeCollectionFactoryInterface $collectionFactory,
75        protected TreenodeDtoCollectionFactoryInterface $dtoCollectionFactory,
76    ) {
77        $this->isInitialized = false;
78    }
79
80    /**
81     * initialize
82     * initializes the tree, e.g. removes all the nodes, sets the root to null, sets the treeId
83     *
84     * @param  TreeIdType  $treeId
85     */
86    public function initialize($treeId): void
87    {
88        $this->nodeCollection = $this->collectionFactory->makeCollection();
89        $this->root = null;
90        $this->setTreeId($treeId);
91        /**
92         * at this point the tree is in a valid state and is therefore initialized, even if it does not have
93         * nodes yet
94         */
95        $this->isInitialized = true;
96    }
97
98    /**
99     * @param  TreeIdType  $treeId
100     *
101     * @return void
102     * @throws InvalidTreeidException
103     */
104    protected function setTreeId($treeId): void
105    {
106        if (!$this->testIdType($treeId)) {
107            throw new InvalidTreeidException();
108        }
109        $this->treeId = $treeId;
110    }
111
112    /**
113     * getTreeId
114     * @return TreeIdType
115     */
116    public function getTreeId()
117    {
118        if (null === $this->treeId) {
119            throw new TreeNotInitializedException();
120        }
121        return $this->treeId;
122    }
123
124    /**
125     * rootTest
126     * @param  NodeType|TreenodeDtoInterface<NodeIdType, TreeIdType>  $node
127     *
128     * @return bool
129     */
130    protected function rootTest(TreenodeInterface|TreenodeDtoInterface $node): bool
131    {
132        return $node->getParentId() === null;
133    }
134
135    /**
136     * @function setRoot sets a reference to the root node of the tree
137     *
138     * @param  NodeType  $node
139     *
140     * @throws AlreadySetRootException
141     */
142    protected function setRoot(TreenodeInterface $node): void
143    {
144        /**
145         * if the root is already set, throw an exception
146         */
147        if (null !== $this->getRoot()) {
148            throw new AlreadySetRootException();
149        }
150        $this->root = $node;
151    }
152
153    /**
154     * @function getRoot
155     * @return NodeType|null
156     * leave the return type unspecified because when we extend this class we
157     * want to be able to get a covariant return type
158     */
159    public function getRoot()
160    {
161        return $this->root ?? null;
162    }
163
164    public function isEmpty(): bool
165    {
166        return $this->nodeCollection->isEmpty();
167    }
168
169    /**
170     * makeNode
171     * @return NodeType
172     */
173    public function makeNode(): TreenodeInterface
174    {
175        return $this->treenodeFactory->makeNode();
176    }
177
178    /**
179     * addNode
180     *
181     * @param  NodeType  $node
182     */
183    public function addNode(TreenodeInterface $node): void
184    {
185        if (!$this->isInitialized) {
186            throw new TreeNotInitializedException();
187        }
188
189        if (!$node->isInitialized()) {
190            throw new NodeNotInitializedException();
191        }
192
193        /**
194         * add the node to the node collection
195         * @var NodeIdType $nodeId
196         */
197        $nodeId = $node->getNodeId();
198
199        try {
200            $this->nodeCollection->add($nodeId, $node);
201        } catch (Exception $e) {
202            throw new AddNodeException((string) $nodeId, $e);
203        }
204
205        /**
206         * if it is the root, set the property
207         */
208        if ($this->rootTest($node)) {
209            $this->setRoot($node);
210        }
211    }
212
213    /**
214     * @function deleteNode deletes a node from the tree.
215     *
216     * If deleteBranchOK is true then node and all its descendants will be deleted as well.  If deleteBranchOK is false
217     * and $node is an interior node, throw an exception.
218     *
219     * @param  NodeIdType  $nodeId
220     * @param  bool  $deleteBranchOK
221     *
222     * @throws DeleteInteriorNodeException
223     * @throws NodeNotInTreeException
224     */
225    public function deleteNode($nodeId, bool $deleteBranchOK = false): void
226    {
227        /**
228         * if the node is not in the tree, throw an exception
229         */
230        if (!$node = $this->getNode($nodeId)) {
231            throw new NodeNotInTreeException($this->treeId, $nodeId);
232        }
233
234        /**
235         * if this is an interior node and deleteBranchOK parameter is false, throw an exception
236         */
237        if (!$deleteBranchOK && $node->hasChildren()) {
238            throw new DeleteInteriorNodeException($nodeId);
239        }
240
241        /**
242         * delete children first if $deleteBranchOk is true and then delete this node
243         */
244        $this->deleteNodeRecurse($node, $deleteBranchOK);
245
246        /**
247         * If this node happens to be the root of the tree, delete the root reference.
248         */
249        if ($node === $this->getRoot()) {
250            $this->root = null;
251        }
252    }
253
254    /**
255     * @function deleteNodeRecurse does the actual work of deleting the node / branch
256     *
257     * @param  NodeType  $node
258     * @param bool $deleteBranch
259     *
260     * @throws DeleteInteriorNodeException
261     * @throws NodeNotInTreeException
262     */
263    protected function deleteNodeRecurse(
264        TreenodeInterface $node,
265        bool $deleteBranch
266    ): void {
267        /**
268         * delete all the children first.
269         */
270        if ($deleteBranch) {
271            /** @var NodeType $child */
272            foreach ($node->getChildren() as $child) {
273                $this->deleteNodeRecurse($child, true);
274            }
275        }
276
277        /** @var NodeIdType $nodeId */
278        $nodeId = $node->getNodeId();
279        $this->nodeCollection->delete($nodeId);
280    }
281
282    /**
283     * hydrate
284     * @param  TreenodeDtoCollectionInterface<NodeIdType, TreeIdType>  $dtoCollection
285     *
286     * @return void
287     */
288    public function hydrate(TreenodeDtoCollectionInterface $dtoCollection): void
289    {
290        if (!$this->isInitialized) {
291            throw new TreeNotInitializedException();
292        }
293
294        /**
295         * If empty, just return - nothing to do.
296         */
297        if ($dtoCollection->isEmpty()) return;
298
299        /**
300         * bail out if there is no root
301         */
302        if (null === ($root = $dtoCollection->getRoot())) {
303            throw new RootCountException((string) 0);
304        }
305
306        /**
307         * recurse down from the root
308         *  Using a pure, depth-first algorithm, there is the possibility that the dtoCollection contains
309         *  an undetected, self-contained circular graph which is orphaned from the rest of the tree.
310         *  At the end, check to ensure that the number of nodes inserted into the tree equals
311         *  the number of nodes in the dtoCollection
312         */
313        $insertCount = 0;
314        $this->insertDtoRecurse($root->getNodeId(), $dtoCollection, $insertCount);
315
316        if ($insertCount !== $dtoCollection->count()) {
317            throw new DtoCollectionException();
318        }
319
320    }
321
322    /**
323     * insertNodeRecurse recursively inserts nodes into the tree using a depth first algorithm.
324     *
325     *
326     * @param  NodeIdType  $nodeId
327     * @param  TreenodeDtoCollectionInterface<NodeIdType, TreeIdType>  $dtoCollection
328     * @param non-negative-int $insertCount
329     *
330     * @return void
331     */
332    protected function insertDtoRecurse($nodeId, TreenodeDtoCollectionInterface $dtoCollection, int $insertCount): void
333    {
334        $dto = $dtoCollection->getDto($nodeId);
335
336        $node = $this->makeNode();
337        $node->hydrate($dto);
338        $this->addNode($node);
339
340        $insertCount++;
341
342        /**
343         * recurse down through the children to hydrate the tree.
344         */
345        foreach ($dtoCollection->getChildKeysOf($dto) as $key) {
346            $this->insertDtoRecurse($key, $dtoCollection, $insertCount);
347        }
348    }
349
350    /**
351     * @return TreenodeDtoCollectionInterface<NodeIdType, TreeIdType>
352     */
353    public function dehydrate(): TreenodeDtoCollectionInterface
354    {
355        $dtoCollection = $this->dtoCollectionFactory->makeTreenodeDtoCollection();
356
357        foreach ($this->nodeCollection as $node) {
358            $dto = $node->dehydrate();
359            $dtoCollection->add($dto);
360        }
361        return $dtoCollection;
362    }
363
364    /**
365     * @function getNodeCollection
366     * @return TreenodeCollectionInterface<NodeIdType, NodeType>
367     */
368    public function getNodeCollection(): TreenodeCollectionInterface
369    {
370        return $this->nodeCollection;
371    }
372
373    /**
374     * @function getNode
375     *
376     * @param  NodeIdType  $nodeId
377     *
378     * @return NodeType|null
379     */
380    public function getNode($nodeId): ?TreenodeInterface
381    {
382        return $this->nodeCollection->getNode($nodeId);
383    }
384
385
386}