Passed
Push — master ( 49b81d...965bb5 )
by SignpostMarv
06:46
created

ModifyDaftNestedObjectTreeInsertAdjacent()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 38
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 6

Importance

Changes 0
Metric Value
cc 6
eloc 19
nc 6
nop 3
dl 0
loc 38
ccs 20
cts 20
cp 1
crap 6
rs 9.0111
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 60
    protected function ObtainLastLeafInTree() : DaftNestedWriteableObject
271
    {
272 60
        $tree = $this->RecallDaftNestedObjectFullTree();
273
274
        /**
275
        * @var DaftNestedWriteableObject
276
        *
277
        * @psalm-var T
278
        */
279 60
        $end = end($tree);
280
281 60
        return $end;
282
    }
283
284
    /**
285
    * @psalm-param T $newLeaf
286
    * @psalm-param T $referenceLeaf
287
    */
288 52
    protected function ModifyDaftNestedObjectTreeInsertAdjacent(
289
        DaftNestedWriteableObject $newLeaf,
290
        DaftNestedWriteableObject $referenceLeaf,
291
        bool $before
292
    ) : void {
293 52
        $siblings = $this->SiblingsExceptLeaf($newLeaf, $referenceLeaf);
294
295 52
        $siblingIds = [];
296 52
        $siblingSort = [];
297 52
        $j = count($siblings);
298
299 52
        foreach ($siblings as $leaf) {
300
            /**
301
            * @var scalar|(scalar|array|object|null)[]
302
            */
303 28
            $siblingId = $leaf->GetId();
304 28
            $siblingIds[] = $siblingId;
305 28
            $siblingSort[] = $leaf->GetIntNestedSortOrder();
306
        }
307
308 52
        $pos = array_search($referenceLeaf->GetId(), $siblingIds, true);
309
310 52
        if (false === $pos) {
311 24
            throw new RuntimeException('Reference leaf not found in siblings tree!');
312
        }
313
314 28
        for ($i = 0; $i < $j; ++$i) {
315 28
            $siblings[$i]->SetIntNestedSortOrder(
316 28
                $siblingSort[$i] +
317 28
                (($before ? ($i < $pos) : ($i <= $pos)) ? DaftNestedObjectTree::DECREMENT : DaftNestedObjectTree::INCREMENT)
318
            );
319 28
            $this->StoreThenRetrieveFreshLeaf($siblings[$i]);
320
        }
321
322 28
        $newLeaf->SetIntNestedSortOrder($siblingSort[$pos]);
323 28
        $newLeaf->AlterDaftNestedObjectParentId($referenceLeaf->ObtainDaftNestedObjectParentId());
324
325 28
        $this->StoreThenRetrieveFreshLeaf($newLeaf);
326 28
    }
327
328 60
    protected function RebuildTreeInefficiently() : void
329
    {
330 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

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