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