Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
45 / 45 |
|
100.00% |
19 / 19 |
CRAP | |
100.00% |
1 / 1 |
Treenode | |
100.00% |
45 / 45 |
|
100.00% |
19 / 19 |
29 | |
100.00% |
1 / 1 |
getNodeId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setNodeId | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
setTree | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
setParent | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
5 | |||
getParent | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getChildren | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getIndex | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setIndex | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isDescendantOf | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
isAncestorOf | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isRoot | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFirstChild | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getLastChild | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getNthChild | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getChildrenArray | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
hasChildren | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getChild | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getSiblings | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 |
1 | <?php |
2 | |
3 | /** |
4 | * @author: Doug Wilbourne (dougwilbourne@gmail.com) |
5 | */ |
6 | |
7 | declare (strict_types=1); |
8 | |
9 | namespace pvc\struct\tree\node; |
10 | |
11 | use pvc\interfaces\struct\tree\node\TreenodeChildCollectionFactoryInterface; |
12 | use pvc\interfaces\struct\tree\node\TreenodeChildCollectionInterface; |
13 | use pvc\interfaces\struct\tree\node\TreenodeInterface; |
14 | use pvc\interfaces\struct\tree\tree\TreeInterface; |
15 | use pvc\struct\tree\err\CircularGraphException; |
16 | use pvc\struct\tree\err\InvalidNodeIdException; |
17 | use pvc\struct\tree\err\InvalidParentNodeIdException; |
18 | use pvc\struct\tree\err\NodeNotEmptyHydrationException; |
19 | use pvc\struct\tree\err\RootCannotBeMovedException; |
20 | use pvc\struct\tree\err\SetTreeException; |
21 | use pvc\struct\treesearch\VisitationTrait; |
22 | |
23 | /** |
24 | * nodes are generic. In order to make them useful, you will need to extend this class to create a specific |
25 | * node type (and extend the tree and treenodefactory classes as well). Node types typically have a specific |
26 | * kind of payload. |
27 | * |
28 | * The nodeId property is immutable - the only way to set the nodeId is at hydration. |
29 | * |
30 | * nodes are allowed to move around |
31 | * 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 |
32 | * important to know that not only does a node keep a reference to its parent, but it also keeps a list of its |
33 | * children. So the setParent method is responsible not only for setting the parent property, but it also takes |
34 | * the parent and adds a node to its child list. |
35 | * |
36 | * @template TreenodeType of TreenodeInterface |
37 | * @implements TreenodeInterface<TreenodeType> |
38 | */ |
39 | class Treenode implements TreenodeInterface |
40 | { |
41 | /** |
42 | * implement NodeVisitableInterface, make Treenodes available for iterable depth first search |
43 | */ |
44 | use VisitationTrait; |
45 | |
46 | /** |
47 | * unique id for this node |
48 | * |
49 | * @var non-negative-int $nodeId |
50 | */ |
51 | protected int $nodeId; |
52 | |
53 | /** |
54 | * @function getNodeId |
55 | * @return non-negative-int |
56 | */ |
57 | public function getNodeId(): int |
58 | { |
59 | return $this->nodeId; |
60 | } |
61 | |
62 | public function setNodeId(int $nodeId): void |
63 | { |
64 | /** |
65 | * nodeId must be non-negative. |
66 | */ |
67 | if ($nodeId < 0) { |
68 | throw new InvalidNodeIdException($nodeId); |
69 | } |
70 | |
71 | /** |
72 | * nodeId is immutable |
73 | */ |
74 | if (isset($this->nodeId)) { |
75 | throw new NodeNotEmptyHydrationException($nodeId); |
76 | } |
77 | |
78 | $this->nodeId = $nodeId; |
79 | } |
80 | |
81 | /** |
82 | * @var TreeInterface<TreenodeType> |
83 | */ |
84 | protected TreeInterface $tree; |
85 | |
86 | /** |
87 | * @param TreeInterface<TreenodeType> $tree |
88 | * |
89 | * @return void |
90 | * @throws SetTreeException |
91 | */ |
92 | public function setTree(TreeInterface $tree): void |
93 | { |
94 | /** |
95 | * $tree property is immutable |
96 | */ |
97 | if (isset($this->tree)) { |
98 | throw new SetTreeException($this->nodeId); |
99 | } |
100 | $this->tree = $tree; |
101 | } |
102 | |
103 | /** |
104 | * reference to parent |
105 | * |
106 | * @var TreenodeType|null |
107 | */ |
108 | protected ?TreenodeInterface $parent; |
109 | |
110 | /** |
111 | * @param ?TreenodeType $parent |
112 | * |
113 | * @return void |
114 | * |
115 | * two cases: the first is this is called as part of this node being added |
116 | * to the tree. In this case, the parent property is currently null. |
117 | * |
118 | * The second case is when you are trying to move this node within the tree. |
119 | * In this case, the parent is already set and the argument is intended |
120 | * to be the new parent. |
121 | */ |
122 | public function setParent(?TreenodeInterface $parent): void |
123 | { |
124 | if ($parent) { |
125 | $parentId = $parent->getNodeId(); |
126 | |
127 | /** |
128 | * ensure parent is in the tree |
129 | */ |
130 | if ($this->tree->getNode($parentId) === null) { |
131 | throw new InvalidParentNodeIdException($parentId); |
132 | } |
133 | |
134 | /** |
135 | * ensure we are not creating a circular graph |
136 | */ |
137 | if ($parent->isDescendantOf($this)) { |
138 | throw new CircularGraphException($parent->getNodeId()); |
139 | } |
140 | |
141 | /** |
142 | * ensure we are not trying to move the root node |
143 | */ |
144 | if ($this->tree->getRoot() === $this) { |
145 | throw new RootCannotBeMovedException(); |
146 | } |
147 | |
148 | /** |
149 | * if parent is not null, add this node to the parent's child collection |
150 | */ |
151 | $childCollection = $parent->getChildren(); |
152 | $childCollection->add($this->getNodeId(), $this); |
153 | } |
154 | |
155 | /** |
156 | * set the parent. If the parent is null, the tree will handle setting it |
157 | * as the root of the tree |
158 | */ |
159 | $this->parent = $parent; |
160 | } |
161 | |
162 | /** |
163 | * @function getParent |
164 | * @return TreenodeType|null |
165 | */ |
166 | public function getParent(): ?TreenodeInterface |
167 | { |
168 | return $this->parent ?? null; |
169 | } |
170 | |
171 | |
172 | /** |
173 | * @var TreenodeChildCollectionInterface<TreenodeType> $children |
174 | */ |
175 | protected TreenodeChildCollectionInterface $children; |
176 | |
177 | /** |
178 | * @return TreenodeChildCollectionInterface<TreenodeType> |
179 | */ |
180 | public function getChildren(): TreenodeChildCollectionInterface |
181 | { |
182 | return $this->children; |
183 | } |
184 | |
185 | /** |
186 | * @var non-negative-int |
187 | */ |
188 | protected int $index; |
189 | |
190 | public function getIndex(): int |
191 | { |
192 | return $this->index; |
193 | } |
194 | |
195 | public function setIndex(int $index): void |
196 | { |
197 | $this->index = $index; |
198 | } |
199 | |
200 | /** |
201 | * @param TreenodeChildCollectionFactoryInterface<TreenodeType> $collectionFactory |
202 | */ |
203 | public function __construct(protected TreenodeChildCollectionFactoryInterface $collectionFactory) |
204 | { |
205 | $this->children = $this->collectionFactory->makeChildCollection(); |
206 | } |
207 | |
208 | /** |
209 | * methods describing the nature of the node |
210 | */ |
211 | |
212 | /** |
213 | * @function isDescendantOf |
214 | * |
215 | * @param TreenodeType $node |
216 | * |
217 | * @return bool |
218 | */ |
219 | public function isDescendantOf(TreenodeInterface $node): bool |
220 | { |
221 | if ($this->getParent() === $node) { |
222 | return true; |
223 | } |
224 | if (is_null($this->getParent())) { |
225 | return false; |
226 | } else { |
227 | return $this->getParent()->isDescendantOf($node); |
228 | } |
229 | } |
230 | |
231 | /** |
232 | * @function isAncestorOf |
233 | * |
234 | * @param TreenodeType $node |
235 | * |
236 | * @return bool |
237 | */ |
238 | public function isAncestorOf(TreenodeInterface $node): bool |
239 | { |
240 | return $node->isDescendantOf($this); |
241 | } |
242 | |
243 | public function isRoot(): bool |
244 | { |
245 | return $this->tree->getRoot() === $this; |
246 | } |
247 | |
248 | |
249 | /** |
250 | * @return TreenodeType|null |
251 | */ |
252 | public function getFirstChild(): ?TreenodeInterface |
253 | { |
254 | return $this->getChildren()->getFirst(); |
255 | } |
256 | |
257 | /** |
258 | * @return TreenodeType|null |
259 | */ |
260 | public function getLastChild(): ?TreenodeInterface |
261 | { |
262 | return $this->getChildren()->getLast(); |
263 | } |
264 | |
265 | /** |
266 | * @param non-negative-int $n |
267 | * |
268 | * @return TreenodeType|null |
269 | */ |
270 | public function getNthChild(int $n): ?TreenodeInterface |
271 | { |
272 | return $this->getChildren()->getNth($n); |
273 | } |
274 | |
275 | /** |
276 | * getChildrenArray |
277 | * |
278 | * @return array<non-negative-int, TreenodeType> |
279 | */ |
280 | public function getChildrenArray(): array |
281 | { |
282 | return $this->getChildren()->getElements(); |
283 | } |
284 | |
285 | /** |
286 | * @function hasChildren |
287 | * @return bool |
288 | */ |
289 | public function hasChildren(): bool |
290 | { |
291 | return (!$this->children->isEmpty()); |
292 | } |
293 | |
294 | /** |
295 | * @function getChild |
296 | * |
297 | * @param non-negative-int $nodeId |
298 | * |
299 | * @return TreenodeType|null |
300 | */ |
301 | public function getChild(int $nodeId): ?TreenodeInterface |
302 | { |
303 | return $this->children->getElement($nodeId); |
304 | } |
305 | |
306 | /** |
307 | * getSiblings returns a collection of this node's siblings |
308 | * |
309 | * @return TreenodeChildCollectionInterface<TreenodeType> |
310 | */ |
311 | public function getSiblings(): TreenodeChildCollectionInterface |
312 | { |
313 | /** |
314 | * the root has no parent, so there is no existing child collection to get from a parent. |
315 | * Not sure why phpstan needs the type hinting....... |
316 | */ |
317 | if ($this->isRoot()) { |
318 | /** @var TreenodeChildCollection<TreenodeType> $collection */ |
319 | $collection = $this->collectionFactory->makeChildCollection(); |
320 | $collection->add($this->getNodeId(), $this); |
321 | } else { |
322 | $parent = $this->getParent(); |
323 | assert(!is_null($parent)); |
324 | /** @var TreenodeChildCollection<TreenodeType> $collection */ |
325 | $collection = $parent->getChildren(); |
326 | } |
327 | return $collection; |
328 | } |
329 | } |