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