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