Passed
Push — master ( 965bb5...36ec1a )
by SignpostMarv
06:04
created

WriteableTreeTrait::MaybeGetLeafOrThrow()   A

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|(scalar|array|object|null)[]
67
    */
68
    abstract public function GetNestedObjectTreeRootId();
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
                $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|(scalar|array|object|null)[] $root
192
    * @param scalar|(scalar|array|object|null)[]|null $replacementRoot
193
    */
194 12
    public function ModifyDaftNestedObjectTreeRemoveWithId($root, $replacementRoot) : int
195
    {
196 12
        $rootObject = $this->RecallDaftObject($root);
197
198 12
        $resp = null;
199
200 12
        if ($rootObject instanceof DaftNestedWriteableObject) {
201 12
            $resp = $this->ModifyDaftNestedObjectTreeRemoveWithIdUsingRootObject(
202 12
                $replacementRoot,
203 12
                $rootObject
204
            );
205
        }
206
207 8
        return is_int($resp) ? $resp : $this->CountDaftNestedObjectFullTree();
208
    }
209
210
    /**
211
    * @psalm-param T $leaf
212
    *
213
    * @psalm-return T
214
    */
215 62
    public function StoreThenRetrieveFreshLeaf(
216
        DaftNestedWriteableObject $leaf
217
    ) : DaftNestedWriteableObject {
218 62
        $this->RememberDaftObject($leaf);
219 62
        $this->ForgetDaftObject($leaf);
220 62
        $this->ForgetDaftObjectById($leaf->GetId());
221
222
        /**
223
        * @psalm-var class-string<T>
224
        */
225 62
        $type = get_class($leaf);
226
227
        /**
228
        * @var DaftNestedWriteableObject
229
        *
230
        * @psalm-var T
231
        */
232 62
        $out = $this->RecallDaftObjectOrThrow($leaf->GetId(), $type);
233
234 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...
235
    }
236
237
    /**
238
    * @psalm-param T $object
239
    */
240 88
    public function RememberDaftObject(SuitableForRepositoryType $object) : void
241
    {
242
        /**
243
        * @var DaftNestedWriteableObject
244
        *
245
        * @psalm-var T
246
        */
247 88
        $object = $object;
248
249 88
        if (NestedTypeParanoia::NotYetAppendedToTree($object)) {
250 86
            $fullTreeCount = $this->CountDaftNestedObjectFullTree();
251
252 86
            if ($fullTreeCount > AbstractArrayBackedDaftNestedObject::COUNT_EXPECT_NON_EMPTY) {
253 60
                $end = $this->ObtainLastLeafInTree();
254
255 60
                $left = $end->GetIntNestedRight() + 1;
256
            } else {
257 86
                $left = $fullTreeCount + $fullTreeCount;
258
            }
259
260 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\...yBackedDaftNestedObject. ( Ignorable by Annotation )

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

260
            $object->/** @scrutinizer ignore-call */ 
261
                     SetIntNestedLeft($left);
Loading history...
261 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\...yBackedDaftNestedObject. ( Ignorable by Annotation )

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

261
            $object->/** @scrutinizer ignore-call */ 
262
                     SetIntNestedRight($left + 1);
Loading history...
262
        }
263
264 88
        parent::RememberDaftObject($object);
265 88
    }
266
267
    /**
268
    * @psalm-return T
269
    */
270
    abstract protected function ObtainLastLeafInTree() : DaftNestedWriteableObject;
271
272
    /**
273
    * @psalm-param T $newLeaf
274
    * @psalm-param T $referenceLeaf
275
    */
276 52
    protected function ModifyDaftNestedObjectTreeInsertAdjacent(
277
        DaftNestedWriteableObject $newLeaf,
278
        DaftNestedWriteableObject $referenceLeaf,
279
        bool $before
280
    ) : void {
281 52
        $siblings = $this->SiblingsExceptLeaf($newLeaf, $referenceLeaf);
282
283 52
        $siblingIds = [];
284 52
        $siblingSort = [];
285 52
        $j = count($siblings);
286
287 52
        foreach ($siblings as $leaf) {
288
            /**
289
            * @var scalar|(scalar|array|object|null)[]
290
            */
291 28
            $siblingId = $leaf->GetId();
292 28
            $siblingIds[] = $siblingId;
293 28
            $siblingSort[] = $leaf->GetIntNestedSortOrder();
294
        }
295
296 52
        $pos = array_search($referenceLeaf->GetId(), $siblingIds, true);
297
298 52
        if (false === $pos) {
299 24
            throw new RuntimeException('Reference leaf not found in siblings tree!');
300
        }
301
302 28
        for ($i = 0; $i < $j; ++$i) {
303 28
            $siblings[$i]->SetIntNestedSortOrder(
304 28
                $siblingSort[$i] +
305 28
                (($before ? ($i < $pos) : ($i <= $pos)) ? DaftNestedObjectTree::DECREMENT : DaftNestedObjectTree::INCREMENT)
306
            );
307 28
            $this->StoreThenRetrieveFreshLeaf($siblings[$i]);
308
        }
309
310 28
        $newLeaf->SetIntNestedSortOrder($siblingSort[$pos]);
311 28
        $newLeaf->AlterDaftNestedObjectParentId($referenceLeaf->ObtainDaftNestedObjectParentId());
312
313 28
        $this->StoreThenRetrieveFreshLeaf($newLeaf);
314 28
    }
315
316 60
    protected function RebuildTreeInefficiently() : void
317
    {
318 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

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