Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
58 / 58 |
|
100.00% |
21 / 21 |
CRAP | |
100.00% |
1 / 1 |
Treenode | |
100.00% |
58 / 58 |
|
100.00% |
21 / 21 |
39 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
isEmpty | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
hydrate | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
5 | |||
setNodeId | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
setParent | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
7 | |||
getNodeId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getParentId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getParent | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getTree | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getTreeId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getChildren | |
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% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
getSiblings | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
isRoot | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isAncestorOf | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isDescendantOf | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
isLeaf | |
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 |
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\collection\CollectionInterface; |
12 | use pvc\interfaces\struct\collection\IndexedElementInterface; |
13 | use pvc\interfaces\struct\dto\DtoInterface; |
14 | use pvc\interfaces\struct\tree\node\TreenodeInterface; |
15 | use pvc\interfaces\struct\tree\tree\TreeInterface; |
16 | use pvc\interfaces\struct\treesearch\NodeVisitableInterface; |
17 | use pvc\struct\collection\Collection; |
18 | use pvc\struct\tree\err\AlreadySetNodeidException; |
19 | use pvc\struct\tree\err\ChildCollectionException; |
20 | use pvc\struct\tree\err\CircularGraphException; |
21 | use pvc\struct\tree\err\InvalidNodeIdException; |
22 | use pvc\struct\tree\err\InvalidParentNodeException; |
23 | use pvc\struct\tree\err\InvalidValueException; |
24 | use pvc\struct\tree\err\NodeNotEmptyHydrationException; |
25 | use pvc\struct\tree\err\RootCannotBeMovedException; |
26 | use pvc\struct\tree\err\SetTreeIdException; |
27 | use pvc\struct\tree\tree\Tree; |
28 | use pvc\struct\treesearch\VisitationTrait; |
29 | |
30 | /** |
31 | * @phpstan-import-type TreenodeDtoShape from TreenodeInterface |
32 | * |
33 | * The nodeid property is immutable - the only way to set the nodeid is at hydration. The same applies to the tree property, |
34 | * which is set at construction time. |
35 | * |
36 | * This means that there are no setters for these properties. Together, these two design points ensure that nodes |
37 | * cannot be hydrated except in the context of belonging to a tree. That in turn makes it a bit easier to enforce the |
38 | * design point that all nodeids in a tree must be unique. |
39 | * |
40 | * The same is almost true for the parent property, but the difference is that the nodes are allowed to move around |
41 | * 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 |
42 | * important to know that not only does a node keep a reference to its parent, but it also keeps a list of its |
43 | * children. So the setParent method is responsible not only for setting the parent property, but it also takes |
44 | * the parent and adds a node to its child list. |
45 | */ |
46 | class Treenode implements TreenodeInterface, NodeVisitableInterface, IndexedElementInterface |
47 | { |
48 | /** |
49 | * implement NodeVisitableInterface, make Treenodes available for iterable depth first search |
50 | */ |
51 | use VisitationTrait; |
52 | |
53 | /** |
54 | * unique id for this node |
55 | * @var non-negative-int $nodeid |
56 | */ |
57 | protected int $nodeid; |
58 | |
59 | /** |
60 | * reference to parent |
61 | * @var TreenodeInterface|null |
62 | */ |
63 | protected ?TreenodeInterface $parent; |
64 | |
65 | /** |
66 | * reference to containing tree |
67 | * @var TreeInterface |
68 | */ |
69 | protected TreeInterface $tree; |
70 | |
71 | /** |
72 | * @var non-negative-int |
73 | */ |
74 | protected int $index; |
75 | |
76 | /** |
77 | * @var CollectionInterface<TreenodeInterface> $children |
78 | */ |
79 | public CollectionInterface $children; |
80 | |
81 | /** |
82 | * @param CollectionInterface<TreenodeInterface> $collection |
83 | * @param TreeInterface $tree |
84 | * @throws ChildCollectionException |
85 | */ |
86 | public function __construct(CollectionInterface $collection, TreeInterface $tree) |
87 | { |
88 | /** |
89 | * set the child collection |
90 | */ |
91 | if (!$collection->isEmpty()) { |
92 | throw new ChildCollectionException(); |
93 | } else { |
94 | $this->children = $collection; |
95 | } |
96 | |
97 | $this->tree = $tree; |
98 | } |
99 | |
100 | /** |
101 | * isEmpty |
102 | * @return bool |
103 | */ |
104 | public function isEmpty(): bool |
105 | { |
106 | return is_null($this->nodeid ?? null); |
107 | } |
108 | |
109 | /** |
110 | * @param TreenodeDtoShape&DtoInterface $dto |
111 | * @return void |
112 | * @throws AlreadySetNodeidException |
113 | * @throws CircularGraphException |
114 | * @throws InvalidNodeIdException |
115 | * @throws InvalidParentNodeException |
116 | * @throws NodeNotEmptyHydrationException |
117 | * @throws RootCannotBeMovedException |
118 | * @throws SetTreeIdException |
119 | * @throws InvalidValueException |
120 | */ |
121 | public function hydrate(DtoInterface $dto): void |
122 | { |
123 | /** |
124 | * cannot hydrate a node if it already has been hydrated |
125 | */ |
126 | if (!$this->isEmpty()) { |
127 | throw new NodeNotEmptyHydrationException($this->getNodeId()); |
128 | } |
129 | |
130 | /** |
131 | * set the nodeId |
132 | */ |
133 | $this->setNodeId($dto->nodeId); |
134 | |
135 | /** |
136 | * recall that the node is constructed such that the reference to its containing tree is already set. |
137 | * If the treeId of the dto is not null, verify that the dto tree id matches the tree id of the containing tree. |
138 | */ |
139 | if (!is_null($dto->treeId) && ($dto->treeId != $this->getTreeId())) { |
140 | throw new SetTreeIdException(); |
141 | } |
142 | |
143 | /** |
144 | * setParent also sets the "reciprocal pointer" by adding this node to the child collection of the parent. |
145 | * We have to set the index first (if it is in the dto) because in the case of ordered collections, |
146 | * the collection will need to know what index to use when adding this node to the child collection of the |
147 | * parent. |
148 | */ |
149 | if (isset($dto->index)) $this->setIndex($dto->index); |
150 | $this->setParent($dto->parentId); |
151 | } |
152 | |
153 | protected function setNodeId(int $nodeId): void |
154 | { |
155 | /** |
156 | * nodeid must be non-negative. |
157 | */ |
158 | if ($nodeId < 0) { |
159 | throw new InvalidNodeIdException($nodeId); |
160 | } |
161 | |
162 | /** |
163 | * node id cannot already exist in the tree |
164 | */ |
165 | if ($this->getTree()->getNode($nodeId)) { |
166 | throw new AlreadySetNodeidException($nodeId); |
167 | } |
168 | $this->nodeid = $nodeId; |
169 | } |
170 | |
171 | /** |
172 | * @function setParent |
173 | * @param non-negative-int|null $parentId |
174 | */ |
175 | public function setParent(?int $parentId): void |
176 | { |
177 | if (!is_null($parentId)) { |
178 | /** |
179 | * @var Treenode|null $parent |
180 | */ |
181 | $parent = $this->getTree()->getNode($parentId); |
182 | if (is_null($parent)) { |
183 | /** |
184 | * parent id is not null but there is no existing node in the tree with node id == parent id |
185 | */ |
186 | throw new InvalidParentNodeException($parentId); |
187 | } |
188 | } else { |
189 | /** |
190 | * parent id is null so parent is also null - it thinks it is the root node. It is up to the containing |
191 | * tree to decide whether it really is or is not. See the setRoot method in Tree |
192 | */ |
193 | $parent = null; |
194 | } |
195 | |
196 | /** |
197 | * make sure we are not creating a circular graph |
198 | */ |
199 | if ($parent && $parent->isDescendantOf($this)) { |
200 | throw new CircularGraphException($parent->getNodeId()); |
201 | } |
202 | |
203 | /** |
204 | * setParent is not just for construction - it is used to move nodes around in the tree as well. If this |
205 | * node is the root node, then it cannot be moved in the tree |
206 | */ |
207 | if ($this->tree->getRoot()?->getNodeId() === $this->getNodeId()) { |
208 | throw new RootCannotBeMovedException(); |
209 | } |
210 | |
211 | /** |
212 | * if parent is not null, add this node to the parent's child collection |
213 | */ |
214 | if ($childCollection = $parent?->getChildren()) { |
215 | $childCollection->add($this->getNodeId(), $this); |
216 | } |
217 | |
218 | /** |
219 | * set the parent |
220 | */ |
221 | $this->parent = $parent; |
222 | } |
223 | |
224 | /** |
225 | * @function getNodeId |
226 | * @return non-negative-int |
227 | */ |
228 | public function getNodeId(): int |
229 | { |
230 | return $this->nodeid; |
231 | } |
232 | |
233 | /** |
234 | * @function getParentId |
235 | * @return non-negative-int|null |
236 | */ |
237 | public function getParentId(): ?int |
238 | { |
239 | return $this->getParent()?->getNodeId(); |
240 | } |
241 | |
242 | /** |
243 | * @function getParent |
244 | * @return TreenodeInterface|null |
245 | */ |
246 | public function getParent(): ?TreenodeInterface |
247 | { |
248 | return $this->parent ?? null; |
249 | } |
250 | |
251 | /** |
252 | * @function getTree |
253 | * @return TreeInterface |
254 | */ |
255 | public function getTree(): TreeInterface |
256 | { |
257 | return $this->tree; |
258 | } |
259 | |
260 | /** |
261 | * getTreeId |
262 | * @return non-negative-int |
263 | */ |
264 | public function getTreeId(): int |
265 | { |
266 | return $this->getTree()->getTreeId(); |
267 | } |
268 | |
269 | /** |
270 | * @return CollectionInterface<TreenodeInterface> |
271 | */ |
272 | public function getChildren(): CollectionInterface |
273 | { |
274 | return $this->children; |
275 | } |
276 | |
277 | /** |
278 | * getChildrenAsArray |
279 | * @return array<TreenodeInterface> |
280 | */ |
281 | public function getChildrenArray(): array |
282 | { |
283 | return $this->getChildren()->getElements(); |
284 | } |
285 | |
286 | /** |
287 | * @function hasChildren |
288 | * @return bool |
289 | */ |
290 | public function hasChildren(): bool |
291 | { |
292 | return (!$this->children->isEmpty()); |
293 | } |
294 | |
295 | /** |
296 | * @function getChild |
297 | * @param non-negative-int $nodeid |
298 | * @return TreenodeInterface|null |
299 | */ |
300 | public function getChild(int $nodeid): ?TreenodeInterface |
301 | { |
302 | foreach ($this->getChildren() as $child) { |
303 | if ($nodeid == $child->getNodeId()) { |
304 | return $child; |
305 | } |
306 | } |
307 | return null; |
308 | } |
309 | |
310 | /** |
311 | * getSiblings returns a collection of this node's siblings |
312 | * |
313 | * @return CollectionInterface<TreenodeInterface> |
314 | */ |
315 | public function getSiblings(): CollectionInterface |
316 | { |
317 | /** |
318 | * the root has no parent, so there is no existing child collection to get from a parent. We do have to go a |
319 | * long way to get to the collection factory so we can make a collection and add this node to it. |
320 | */ |
321 | if ($this->getTree()->rootTest($this)) { |
322 | /** @var Collection<TreenodeInterface> $collection */ |
323 | $collection = $this->getTree()->getTreenodeFactory()->getTreenodeCollectionFactory()->makeCollection([]); |
324 | $collection->add($this->getNodeId(), $this); |
325 | } else { |
326 | $parent = $this->getParent(); |
327 | assert(!is_null($parent)); |
328 | $collection = $parent->getChildren(); |
329 | } |
330 | return $collection; |
331 | } |
332 | |
333 | /** |
334 | * @return bool |
335 | */ |
336 | public function isRoot(): bool |
337 | { |
338 | return ($this->tree->getRoot() === $this); |
339 | } |
340 | |
341 | /** |
342 | * @function isAncestorOf |
343 | * @param Treenode $node |
344 | * @return bool |
345 | */ |
346 | public function isAncestorOf(TreenodeInterface $node): bool |
347 | { |
348 | return $node->isDescendantOf($this); |
349 | } |
350 | |
351 | /** |
352 | * @function isDescendantOf |
353 | * @param Treenode $node |
354 | * @return bool |
355 | */ |
356 | public function isDescendantOf(TreenodeInterface $node): bool |
357 | { |
358 | if ($this->getParent() === $node) { |
359 | return true; |
360 | } |
361 | if (is_null($this->getParent())) { |
362 | return false; |
363 | } else { |
364 | return $this->getParent()->isDescendantOf($node); |
365 | } |
366 | } |
367 | |
368 | /** |
369 | * @function isLeaf |
370 | * @return bool |
371 | */ |
372 | public function isLeaf(): bool |
373 | { |
374 | return ($this->children->isEmpty()); |
375 | } |
376 | |
377 | /** |
378 | * @function getIndex returns the ordinal position of this node in its list of siblings |
379 | * @return non-negative-int |
380 | */ |
381 | public function getIndex(): int |
382 | { |
383 | return $this->index; |
384 | } |
385 | |
386 | /** |
387 | * sets this node's index property. This method should never be called |
388 | * by any other class than the collection which contains the node. The collection bears the responsibility |
389 | * of rationalizing/ordering the indices within the collection. Nodes are unaware of their siblings in |
390 | * a direct way - they need to ask the parent for the sibling collection |
391 | * |
392 | * @function setIndex |
393 | * @param non-negative-int $index |
394 | */ |
395 | public function setIndex(int $index): void |
396 | { |
397 | $this->index = $index; |
398 | } |
399 | } |