Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
70.59% covered (warning)
70.59%
24 / 34
82.35% covered (warning)
82.35%
14 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
SearchAbstract
70.59% covered (warning)
70.59%
24 / 34
82.35% covered (warning)
82.35%
14 / 17
38.65
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%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 isInitialized
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 key
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 current
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 valid
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 rewind
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 next
n/a
0 / 0
n/a
0 / 0
0
 setCurrent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getStartNode
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setStartNode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMaxLevels
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setMaxLevels
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 atMaxLevels
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCurrentLevel
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setCurrentLevel
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 invalidate
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getNodes
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3/**
4 * @author: Doug Wilbourne (dougwilbourne@gmail.com)
5 */
6
7declare(strict_types=1);
8
9namespace pvc\struct\treesearch;
10
11use pvc\interfaces\struct\collection\CollectionFactoryInterface;
12use pvc\interfaces\struct\treesearch\NodeSearchableCollectionFactoryInterface;
13use pvc\interfaces\struct\treesearch\NodeSearchableCollectionInterface;
14use pvc\interfaces\struct\treesearch\NodeSearchableInterface;
15use pvc\interfaces\struct\treesearch\SearchInterface;
16use pvc\struct\treesearch\err\SetMaxSearchLevelsException;
17use pvc\struct\treesearch\err\StartNodeUnsetException;
18
19/**
20 * Class SearchAbstract
21 * @template NodeIdType of array-key
22 * @template NodeType of NodeSearchableInterface
23 * @template CollectionType of NodeSearchableCollectionInterface
24 * @implements SearchInterface<NodeIdType, NodeType, CollectionType>
25 */
26abstract class SearchAbstract implements SearchInterface
27{
28    /**
29     * @var NodeType
30     */
31    protected mixed $startNode;
32
33    /**
34     * @var NodeType|null
35     * mark it private - subclasses should use the current() method
36     */
37    private mixed $currentNode = null;
38
39    /**
40     * @var int
41     *
42     * maximum depth/height to which we are allowed to traverse the tree.
43     * Searching children goes down the tree, searching ancestors goes up the
44     * tree
45     */
46    private int $maxLevels = PHP_INT_MAX;
47
48    /**
49     * @var non-negative-int int
50     */
51    private int $currentLevel = 0;
52
53    /**
54     * @param  NodeSearchableCollectionFactoryInterface<NodeIdType, NodeType>  $collectionFactory
55     *
56     */
57    public function __construct(protected NodeSearchableCollectionFactoryInterface $collectionFactory) {}
58
59    /**
60     * initialize - combines setting the startnode and rewinding the iterator.
61     *
62     * Startnode must be set after this method executes.  But if
63     * startnode is already set and we are running a new search, the startnode
64     * parameter is optional.
65     *
66     * @param  NodeType|null  $startNode
67     *
68     * @return void
69     */
70    public function initialize(mixed $startNode = null): void
71    {
72        if (!isset($this->startNode) && $startNode === null) {
73            throw new StartNodeUnsetException();
74        }
75
76        if ($startNode !== null) {
77            $this->startNode = $startNode;
78        }
79
80        $this->rewind();
81    }
82
83    /**
84     * isInitialized
85     * @return bool
86     */
87    public function isInitialized(): bool
88    {
89        return isset($this->startNode);
90    }
91
92
93
94    /**
95     * methods to implement Iterator
96     */
97
98    /**
99     * key
100     *
101     * @return NodeIdType
102     *
103     * Iterator::key() returns mixed so we need a hint that $key is
104     * of type NodeIdType
105     */
106    public function key(): int|string|null
107    {
108        /** @var NodeIdType $key */
109        $key = $this->current()?->getNodeId();
110        return (!is_null($key) ? $key : null);
111    }
112
113    /**
114     * current
115     *
116     * @return NodeType|null
117     */
118    public function current(): NodeSearchableInterface|null
119    {
120        return $this->currentNode;
121    }
122
123    /**
124     * valid
125     *
126     * @return bool
127     */
128    public function valid(): bool
129    {
130        return !is_null($this->currentNode);
131    }
132
133    /**
134     * rewind
135     *
136     * @throws StartNodeUnsetException
137     */
138    public function rewind(): void
139    {
140        $this->setCurrent($this->getStartNode());
141        $this->currentLevel = 0;
142    }
143
144    /**
145     * @return void
146     */
147    abstract public function next(): void;
148
149    /**
150     * setCurrent
151     *
152     * @param  NodeType|null  $currentNode
153     * nullable because you want to set the current node to null at the end of a search, after the last node has been
154     * returned and have it initialized as null
155     */
156    protected function setCurrent(NodeSearchableInterface|null $currentNode): void
157    {
158        $this->currentNode = $currentNode;
159    }
160
161    /**
162     * getStartNode
163     *
164     * @return NodeType
165     * startNode must be set before the class can do anything so throw an exception if it is not set
166     * @throws StartNodeUnsetException
167     */
168    protected function getStartNode(): mixed
169    {
170        if (!isset($this->startNode)) {
171            throw new StartNodeUnsetException();
172        }
173        return $this->startNode;
174    }
175
176    /**
177     * setStartNode
178     *
179     * @param  NodeType  $startNode
180     */
181    public function setStartNode($startNode): void
182    {
183        $this->startNode = $startNode;
184    }
185
186
187    /**
188     * methods that control how deep in the tree a search can go
189     */
190
191    /**
192     * getMaxLevels
193     *
194     * @return int
195     */
196    public function getMaxLevels(): int
197    {
198        return $this->maxLevels;
199    }
200
201    /**
202     * setMaxLevels
203     *
204     * @param  int  $maxLevels
205     *
206     * @throws SetMaxSearchLevelsException
207     *
208     * it is easy to get confused about this, but startNode is at level 0, meaning that current level property uses
209     * zero-based counting.  BUT, max levels is one-based.  So if you set max levels = 3, you will get three levels
210     * of nodes which are at levels 0, 1 and 2 and includes the start node.
211     */
212    public function setMaxLevels(int $maxLevels): void
213    {
214        if ($maxLevels < 1) {
215            throw new SetMaxSearchLevelsException($maxLevels);
216        } else {
217            $this->maxLevels = $maxLevels;
218        }
219    }
220
221    /**
222     * @return bool
223     *
224     * as an example, max levels of 2 means the start node is at level 0 and the level
225     * below that is on level 1.  So if the current level goes to 1 then we are at the max-levels
226     * threshold.
227     *
228     * the current level is an absolute value.  If we are doing a search of ancestors, then the level
229     * above the start node is level 1.
230     */
231    public function atMaxLevels(): bool
232    {
233        return ($this->getCurrentLevel() == $this->getMaxLevels() - 1);
234    }
235
236    /**
237     * getCurrentLevel
238     *
239     * @return int<-1, max>
240     *
241     * it is conceivable someone could want to know what level of the nodes the search is currently on while
242     * in the middle of iteration so keep this method public
243     */
244    public function getCurrentLevel(): int
245    {
246        return $this->currentLevel;
247    }
248
249    /**
250     * @param  int  $increment
251     *
252     * @return void
253     * we only want subclasses to be able to modify the current level of the search
254     */
255    protected function setCurrentLevel(int $increment): void
256    {
257        /**
258         * by using an absolute value for the current level, we are tracking the "vertical distance away
259         * from the start node".
260         *
261         * Type checker cannot know that the logic in the searches does not permit a current level of
262         * less than zero so we use the assertion
263         */
264        $newLevel = $this->currentLevel + $increment;
265        assert($newLevel >= 0);
266        $this->currentLevel = $newLevel;
267    }
268
269    protected function invalidate(): void
270    {
271        $this->currentNode = null;
272        $this->currentLevel = 0;
273    }
274
275    /**
276     * @return CollectionType
277     */
278    public function getNodes(): NodeSearchableCollectionInterface
279    {
280        $collection = $this->collectionFactory->makeCollection();
281        /** @var NodeType $node */
282        foreach($this as $node) {
283            /** @var NodeIdType $nodeId */
284            $nodeId = $node->getNodeId();
285            $collection->add($nodeId, $node);
286        }
287        return $collection;
288    }
289
290
291}