WriteableTreeTrait::MaybeGetLeafOrThrow()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 23
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 3
nop 2
dl 0
loc 23
ccs 8
cts 8
cp 1
crap 3
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
* Base daft objects.
4
*
5
* @author SignpostMarv
6
*/
7
declare(strict_types=1);
8
9
namespace SignpostMarv\DaftObject;
10
11
use BadMethodCallException;
12
use InvalidArgumentException;
13
use RuntimeException;
14
15
/**
16
* @template T as DaftNestedWriteableObject
17
*/
18
trait WriteableTreeTrait
19
{
20
    /**
21
    * @psalm-param T $root
22
    */
23
    abstract public function CountDaftNestedObjectTreeWithObject(
24
        DaftNestedObject $root,
25
        bool $includeRoot,
26
        ? int $relativeDepthLimit
27
    ) : int;
28
29
    abstract public function RemoveDaftObject(SuitableForRepositoryType $object) : void;
30
31
    /**
32
    * @param scalar|(scalar|array|object|null)[] $id
33
    *
34
    * @psalm-return T|null
35
    */
36
    abstract public function RecallDaftObject($id) : ? SuitableForRepositoryType;
37
38
    abstract public function ForgetDaftObject(SuitableForRepositoryType $object) : void;
39
40
    /**
41
    * @param scalar|(scalar|array|object|null)[] $id
42
    */
43
    abstract public function ForgetDaftObjectById($id) : void;
44
45
    /**
46
    * @return array<int, DaftNestedObject>
47
    *
48
    * @psalm-return array<int, T>
49
    */
50
    abstract public function RecallDaftNestedObjectFullTree(int $relativeDepthLimit = null) : array;
51
52
    /**
53
    * @psalm-param T $root
54
    *
55
    * @return array<int, DaftNestedObject>
56
    *
57
    * @psalm-return array<int, T>
58
    */
59
    abstract public function RecallDaftNestedObjectTreeWithObject(
60
        DaftNestedObject $root,
61
        bool $includeRoot,
62
        ? int $relativeDepthLimit
63
    ) : array;
64
65
    /**
66
    * @return scalar[]
67
    */
68
    abstract public function GetNestedObjectTreeRootId() : array;
69
70
    /**
71
    * @param scalar|(scalar|array|object|null)[] $id
72
    *
73
    * @return array<int, DaftNestedObject>
74
    *
75
    * @psalm-return array<int, T>
76
    */
77
    abstract public function RecallDaftNestedObjectTreeWithId(
78
        $id,
79
        bool $includeRoot,
80
        ? int $relativeDepthLimit
81
    ) : array;
82
83
    abstract public function CountDaftNestedObjectFullTree(int $relativeDepthLimit = null) : int;
84
85
    /**
86
    * {@inheritdoc}
87
    *
88
    * @psalm-param class-string<T> $type
89
    *
90
    * @psalm-return T
91
    */
92
    abstract public function RecallDaftObjectOrThrow(
93
        $id,
94
        string $type = DaftNestedObject::class
95
    ) : SuitableForRepositoryType;
96
97
    /**
98
    * @psalm-param T $newLeaf
99
    * @psalm-param T $referenceLeaf
100
    *
101
    * @psalm-return T
102
    */
103 84
    public function ModifyDaftNestedObjectTreeInsert(
104
        DaftNestedWriteableObject $newLeaf,
105
        DaftNestedWriteableObject $referenceLeaf,
106
        bool $before = DaftNestedWriteableObjectTree::INSERT_AFTER,
107
        bool $above = null
108
    ) : DaftNestedWriteableObject {
109 84
        if ($newLeaf->GetId() === $referenceLeaf->GetId()) {
110 40
            throw new InvalidArgumentException('Cannot modify leaf relative to itself!');
111
        }
112
113 60
        if ((bool) $above) {
114 48
            $this->ModifyDaftNestedObjectTreeInsertAbove($newLeaf, $referenceLeaf);
115 32
        } elseif (DaftNestedWriteableObjectTree::DEFINITELY_BELOW === $above) {
116 6
            $this->ModifyDaftNestedObjectTreeInsertBelow($newLeaf, $referenceLeaf);
117
        } else {
118 28
            $this->ModifyDaftNestedObjectTreeInsertAdjacent($newLeaf, $referenceLeaf, $before);
119
        }
120
121 60
        return $this->RebuildAfterInsert($newLeaf);
122
    }
123
124
    /**
125
    * @param DaftNestedWriteableObject|scalar|(scalar|array|object|null)[] $leaf
126
    * @param DaftNestedWriteableObject|scalar|(scalar|array|object|null)[] $referenceId
127
    *
128
    * @psalm-param T|scalar|(scalar|array|object|null)[] $leaf
129
    * @psalm-param T|scalar|(scalar|array|object|null)[] $referenceId
130
    *
131
    * @psalm-return T
132
    */
133 48
    public function ModifyDaftNestedObjectTreeInsertLoose(
134
        $leaf,
135
        $referenceId,
136
        bool $before = DaftNestedWriteableObjectTree::INSERT_AFTER,
137
        bool $above = null
138
    ) : DaftNestedWriteableObject {
139
        /**
140
        * @var DaftNestedWriteableObject
141
        *
142
        * @psalm-var T
143
        */
144 48
        $leaf = $this->MaybeGetLeafOrThrow($leaf);
145
146 24
        $reference = $this->MaybeRecallLoose($referenceId);
147
148 24
        if ( ! is_null($reference)) {
149 18
            return $this->ModifyDaftNestedObjectTreeInsert($leaf, $reference, $before, $above);
150
        }
151
152 24
        return $this->ModifyDaftNestedObjectTreeInsertLooseIntoTree($leaf, $before, $above);
153
    }
154
155
    /**
156
    * @psalm-param T $root
157
    * @psalm-param T|null $replacementRoot
158
    */
159 8
    public function ModifyDaftNestedObjectTreeRemoveWithObject(
160
        DaftNestedWriteableObject $root,
161
        ? DaftNestedWriteableObject $replacementRoot
162
    ) : int {
163
        if (
164 8
            $this->CountDaftNestedObjectTreeWithObject(
165 8
                $root,
166 8
                false,
167 8
                null
168 8
            ) > AbstractArrayBackedDaftNestedObject::COUNT_EXPECT_NON_EMPTY &&
169 8
            is_null($replacementRoot)
170
        ) {
171 2
            throw new BadMethodCallException('Cannot leave orphan objects in a tree');
172
        }
173
174 6
        $root = $this->StoreThenRetrieveFreshLeaf($root);
175
176 6
        if ( ! is_null($replacementRoot)) {
177 4
            $this->UpdateRoots(
178 4
                $root,
179 4
                (array) $this->StoreThenRetrieveFreshLeaf($replacementRoot)->GetId()
180
            );
181
        }
182
183 6
        $this->RemoveDaftObject($root);
184
185 6
        $this->RebuildTreeInefficiently();
186
187 6
        return $this->CountDaftNestedObjectFullTree();
188
    }
189
190
    /**
191
    * @param scalar[] $root
192
    * @param scalar[] $replacementRoot
193
    */
194 12
    public function ModifyDaftNestedObjectTreeRemoveWithId(
195
        array $root,
196
        array $replacementRoot
197
    ) : int {
198 12
        $rootObject = $this->RecallDaftObject($root);
199
200 12
        $resp = null;
201
202 12
        if ($rootObject instanceof DaftNestedWriteableObject) {
203 12
            $resp = $this->ModifyDaftNestedObjectTreeRemoveWithIdUsingRootObject(
204 12
                $replacementRoot,
205 12
                $rootObject
206
            );
207
        }
208
209 8
        return is_int($resp) ? $resp : $this->CountDaftNestedObjectFullTree();
210
    }
211
212
    /**
213
    * @psalm-param T $leaf
214
    *
215
    * @psalm-return T
216
    */
217 62
    public function StoreThenRetrieveFreshLeaf(
218
        DaftNestedWriteableObject $leaf
219
    ) : DaftNestedWriteableObject {
220 62
        $this->RememberDaftObject($leaf);
221 62
        $this->ForgetDaftObject($leaf);
222 62
        $this->ForgetDaftObjectById($leaf->GetId());
223
224
        /**
225
        * @psalm-var class-string<T>
226
        */
227 62
        $type = get_class($leaf);
228
229
        /**
230
        * @var DaftNestedWriteableObject
231
        *
232
        * @psalm-var T
233
        */
234 62
        $out = $this->RecallDaftObjectOrThrow($leaf->GetId(), $type);
235
236 60
        return $out;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $out returns the type SignpostMarv\DaftObject\SuitableForRepositoryType which includes types incompatible with the type-hinted return SignpostMarv\DaftObject\DaftNestedWriteableObject.
Loading history...
237
    }
238
239
    /**
240
    * @psalm-param T $object
241
    */
242 90
    public function RememberDaftObject(SuitableForRepositoryType $object) : void
243
    {
244
        /**
245
        * @var DaftNestedWriteableObject
246
        *
247
        * @psalm-var T
248
        */
249 90
        $object = $object;
250
251 90
        if (NestedTypeParanoia::NotYetAppendedToTree($object)) {
252 86
            $fullTreeCount = $this->CountDaftNestedObjectFullTree();
253
254 86
            if ($fullTreeCount > AbstractArrayBackedDaftNestedObject::COUNT_EXPECT_NON_EMPTY) {
255 60
                $end = $this->ObtainLastLeafInTree();
256
257 60
                $left = $end->GetIntNestedRight() + 1;
258
            } else {
259 86
                $left = $fullTreeCount + $fullTreeCount;
260
            }
261
262 86
            $object->SetIntNestedLeft($left);
0 ignored issues
show
Bug introduced by
The method SetIntNestedLeft() does not exist on SignpostMarv\DaftObject\SuitableForRepositoryType. It seems like you code against a sub-type of SignpostMarv\DaftObject\SuitableForRepositoryType such as SignpostMarv\DaftObject\DaftNestedWriteableObject or SignpostMarv\DaftObject\...estedWriteableIntObject. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

262
            $object->/** @scrutinizer ignore-call */ 
263
                     SetIntNestedLeft($left);
Loading history...
263 86
            $object->SetIntNestedRight($left + 1);
0 ignored issues
show
Bug introduced by
The method SetIntNestedRight() does not exist on SignpostMarv\DaftObject\SuitableForRepositoryType. It seems like you code against a sub-type of SignpostMarv\DaftObject\SuitableForRepositoryType such as SignpostMarv\DaftObject\DaftNestedWriteableObject or SignpostMarv\DaftObject\...estedWriteableIntObject. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

263
            $object->/** @scrutinizer ignore-call */ 
264
                     SetIntNestedRight($left + 1);
Loading history...
264
        }
265
266 90
        parent::RememberDaftObject($object);
267 90
    }
268
269
    /**
270
    * @psalm-return T
271
    */
272
    abstract protected function ObtainLastLeafInTree() : DaftNestedWriteableObject;
273
274
    /**
275
    * @psalm-param T $newLeaf
276
    * @psalm-param T $referenceLeaf
277
    */
278 52
    protected function ModifyDaftNestedObjectTreeInsertAdjacent(
279
        DaftNestedWriteableObject $newLeaf,
280
        DaftNestedWriteableObject $referenceLeaf,
281
        bool $before
282
    ) : void {
283 52
        $siblings = $this->SiblingsExceptLeaf($newLeaf, $referenceLeaf);
284
285 52
        $siblingIds = [];
286 52
        $siblingSort = [];
287 52
        $j = count($siblings);
288
289 52
        foreach ($siblings as $leaf) {
290
            /**
291
            * @var scalar|(scalar|array|object|null)[]
292
            */
293 28
            $siblingId = $leaf->GetId();
294 28
            $siblingIds[] = $siblingId;
295 28
            $siblingSort[] = $leaf->GetIntNestedSortOrder();
296
        }
297
298 52
        $pos = array_search($referenceLeaf->GetId(), $siblingIds, true);
299
300 52
        if (false === $pos) {
301 24
            throw new RuntimeException('Reference leaf not found in siblings tree!');
302
        }
303
304 28
        for ($i = 0; $i < $j; ++$i) {
305 28
            $siblings[$i]->SetIntNestedSortOrder(
306 28
                $siblingSort[$i] +
307 28
                (($before ? ($i < $pos) : ($i <= $pos)) ? DaftNestedObjectTree::DECREMENT : DaftNestedObjectTree::INCREMENT)
308
            );
309 28
            $this->StoreThenRetrieveFreshLeaf($siblings[$i]);
310
        }
311
312 28
        $newLeaf->SetIntNestedSortOrder($siblingSort[$pos]);
313 28
        $newLeaf->SetDaftNestedObjectParentId($referenceLeaf->GetDaftNestedObjectParentId());
314
315 28
        $this->StoreThenRetrieveFreshLeaf($newLeaf);
316 28
    }
317
318 60
    protected function RebuildTreeInefficiently() : void
319
    {
320 60
        $rebuilder = new InefficientDaftNestedRebuild($this);
0 ignored issues
show
Bug introduced by
$this of type SignpostMarv\DaftObject\WriteableTreeTrait is incompatible with the type SignpostMarv\DaftObject\...stedWriteableObjectTree expected by parameter $tree of SignpostMarv\DaftObject\...dRebuild::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

320
        $rebuilder = new InefficientDaftNestedRebuild(/** @scrutinizer ignore-type */ $this);
Loading history...
321 60
        $rebuilder->RebuildTree();
322 60
    }
323
324
    /**
325
    * @psalm-param T $newLeaf
326
    *
327
    * @psalm-return T
328
    */
329 60
    private function RebuildAfterInsert(
330
        DaftNestedWriteableObject $newLeaf
331
    ) : DaftNestedWriteableObject {
332 60
        $this->RebuildTreeInefficiently();
333
334
        /**
335
        * @psalm-var class-string<T>
336
        */
337 60
        $type = get_class($newLeaf);
338
339
        /**
340
        * @var DaftNestedWriteableObject
341
        *
342
        * @psalm-var T
343
        */
344 60
        $out = $this->RecallDaftObjectOrThrow($newLeaf->GetId(), $type);
345
346 48
        return $out;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $out returns the type SignpostMarv\DaftObject\SuitableForRepositoryType which includes types incompatible with the type-hinted return SignpostMarv\DaftObject\DaftNestedWriteableObject.
Loading history...
347
    }
348
349
    /**
350
    * @param scalar[] $replacementRootId
351
    *
352
    * @psalm-param T $root
353
    */
354 10
    private function UpdateRoots(DaftNestedWriteableObject $root, array $replacementRootId) : void
355
    {
356
        /**
357
        * @var array<int, DaftNestedWriteableObject>
358
        *
359
        * @psalm-var array<int, T>
360
        */
361 10
        $alterThese = $this->RecallDaftNestedObjectTreeWithObject($root, false, DaftNestedWriteableObjectTree::LIMIT_ONE);
362
363 10
        foreach ($alterThese as $alter) {
364 4
            $alter->SetDaftNestedObjectParentId($replacementRootId);
365 4
            $this->RememberDaftObject($alter);
366
        }
367 10
    }
368
369
    /**
370
    * @param DaftNestedWriteableObject|scalar|(scalar|array|object|null)[] $leaf
371
    *
372
    * @psalm-param T|scalar|(scalar|array|object|null)[] $leaf
373
    * @psalm-param class-string<T> $type
374
    *
375
    * @psalm-return T
376
    */
377 48
    private function MaybeGetLeafOrThrow(
378
        $leaf,
379
        string $type = DaftNestedWriteableObject::class
380
    ) : DaftNestedWriteableObject {
381 48
        if ($leaf === $this->GetNestedObjectTreeRootId()) {
382 24
            throw new InvalidArgumentException('Cannot pass root id as new leaf');
383 24
        } elseif ($leaf instanceof DaftNestedWriteableObject) {
384 24
            return $this->StoreThenRetrieveFreshLeaf($leaf);
385
        }
386
387
        /**
388
        * @psalm-var scalar|(scalar|array|object|null)[]
389
        */
390 16
        $leaf = $leaf;
391
392
        /**
393
        * @var DaftNestedWriteableObject
394
        *
395
        * @psalm-var T
396
        */
397 16
        $out = $this->RecallDaftObjectOrThrow($leaf, $type);
398
399 16
        return $out;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $out returns the type SignpostMarv\DaftObject\SuitableForRepositoryType which includes types incompatible with the type-hinted return SignpostMarv\DaftObject\DaftNestedWriteableObject.
Loading history...
400
    }
401
402
    /**
403
    * @param DaftNestedWriteableObject|scalar|(scalar|array|object|null)[] $leaf
404
    *
405
    * @psalm-param T|scalar|(scalar|array|object|null)[] $leaf
406
    */
407 24
    private function MaybeRecallLoose($leaf) : ? DaftNestedWriteableObject
408
    {
409 24
        if ($leaf instanceof DaftNestedWriteableObject) {
410 2
            return $leaf;
411
        }
412
413
        /**
414
        * @var scalar|(scalar|array|object|null)[]
415
        */
416 24
        $leaf = $leaf;
417
418
        /**
419
        * @var DaftNestedWriteableObject|null
420
        *
421
        * @psalm-var T|null
422
        */
423 24
        $out = $this->RecallDaftObject($leaf);
424
425 24
        return $out;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $out could return the type SignpostMarv\DaftObject\SuitableForRepositoryType which includes types incompatible with the type-hinted return SignpostMarv\DaftObject\...tedWriteableObject|null. Consider adding an additional type-check to rule them out.
Loading history...
426
    }
427
428
    /**
429
    * @psalm-param T $leaf
430
    *
431
    * @psalm-return T
432
    */
433 24
    private function ModifyDaftNestedObjectTreeInsertLooseIntoTree(
434
        DaftNestedWriteableObject $leaf,
435
        bool $before,
436
        ? bool $above
437
    ) : DaftNestedWriteableObject {
438
        /**
439
        * @var array<int, DaftNestedWriteableObject>
440
        *
441
        * @psalm-var array<int, T>
442
        */
443 24
        $leaves = array_filter(
444 24
            $this->RecallDaftNestedObjectFullTree(DaftNestedWriteableObjectTree::RELATIVE_DEPTH_SAME),
445
            /**
446
            * @psalm-param T $e
447
            */
448
            function (DaftNestedWriteableObject $e) use ($leaf) : bool {
449 24
                return $e->GetId() !== $leaf->GetId();
450 24
            }
451
        );
452
453 24
        if (count($leaves) < 1) {
454 24
            $leaf->SetIntNestedLeft(0);
455 24
            $leaf->SetIntNestedRight(1);
456 24
            $leaf->SetIntNestedLevel(0);
457 24
            $leaf->SetDaftNestedObjectParentId($this->GetNestedObjectTreeRootId());
458
459 24
            return $this->StoreThenRetrieveFreshLeaf($leaf);
460
        }
461
462 24
        return $this->ModifyDaftNestedObjectTreeInsert(
463 24
            $leaf,
464 24
            NestedTypeParanoia::ObtainFirstOrLast($before, ...$leaves),
465 24
            $before,
466 24
            $above
467
        );
468
    }
469
470
    /**
471
    * @psalm-param T $newLeaf
472
    * @psalm-param T $referenceLeaf
473
    */
474 48
    private function ModifyDaftNestedObjectTreeInsertAbove(
475
        DaftNestedWriteableObject $newLeaf,
476
        DaftNestedWriteableObject $referenceLeaf
477
    ) : void {
478 48
        $newLeaf->SetDaftNestedObjectParentId($referenceLeaf->GetDaftNestedObjectParentId());
479 48
        $referenceLeaf->SetDaftNestedObjectParentId((array) $newLeaf->GetId());
480
481 48
        $this->StoreThenRetrieveFreshLeaf($newLeaf);
482 48
        $this->StoreThenRetrieveFreshLeaf($referenceLeaf);
483 48
    }
484
485
    /**
486
    * @psalm-param T $newLeaf
487
    * @psalm-param T $referenceLeaf
488
    */
489 6
    private function ModifyDaftNestedObjectTreeInsertBelow(
490
        DaftNestedWriteableObject $newLeaf,
491
        DaftNestedWriteableObject $referenceLeaf
492
    ) : void {
493 6
        $newLeaf->SetDaftNestedObjectParentId((array) $referenceLeaf->GetId());
494 6
        $this->StoreThenRetrieveFreshLeaf($newLeaf);
495 6
    }
496
497
    /**
498
    * @psalm-param T $newLeaf
499
    * @psalm-param T $referenceLeaf
500
    *
501
    * @return array<int, DaftNestedWriteableObject>
502
    *
503
    * @psalm-return array<int, T>
504
    */
505 52
    private function SiblingsExceptLeaf(
506
        DaftNestedWriteableObject $newLeaf,
507
        DaftNestedWriteableObject $referenceLeaf
508
    ) : array {
509
        /**
510
        * @var array<int, DaftNestedWriteableObject>
511
        *
512
        * @psalm-var array<int, T>
513
        */
514 52
        $out = array_values(array_filter(
515 52
            $this->RecallDaftNestedObjectTreeWithId(
516 52
                $referenceLeaf->GetDaftNestedObjectParentId(),
517 52
                DaftNestedWriteableObjectTree::EXCLUDE_ROOT,
518 52
                DaftNestedWriteableObjectTree::RELATIVE_DEPTH_SAME
519
            ),
520
            /**
521
            * @psalm-param T $leaf
522
            */
523
            function (DaftNestedWriteableObject $leaf) use ($newLeaf) : bool {
524 28
                return $leaf->GetId() !== $newLeaf->GetId();
525 52
            }
526
        ));
527
528 52
        return $out;
529
    }
530
531
    /**
532
    * @param scalar[] $replacementRoot
533
    *
534
    * @psalm-param T $rootObject
535
    */
536 12
    private function ModifyDaftNestedObjectTreeRemoveWithIdUsingRootObject(
537
        array $replacementRoot,
538
        DaftNestedWriteableObject $rootObject
539
    ) : ? int {
540
        if (
541 12
            $replacementRoot !== $this->GetNestedObjectTreeRootId()
542
        ) {
543
            /**
544
            * @psalm-var class-string<T>
545
            */
546 6
            $type = get_class($rootObject);
547
548
            /**
549
            * @var DaftNestedWriteableObject
550
            *
551
            * @psalm-var T
552
            */
553 6
            $replacement = $this->RecallDaftObjectOrThrow($replacementRoot, $type);
554
555 2
            return $this->ModifyDaftNestedObjectTreeRemoveWithObject(
556 2
                $rootObject,
557 2
                $replacement
558
            );
559
        }
560
561 6
        $this->UpdateRoots($rootObject, $replacementRoot);
562
563 6
        $this->RemoveDaftObject($rootObject);
564
565 6
        $this->RebuildTreeInefficiently();
566
567 6
        return null;
568
    }
569
}
570