Passed
Push — master ( 3401c7...bed608 )
by SignpostMarv
06:03
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
* @template-extends DaftObjectMemoryTree<T>
19
*
20
* @template-implements DaftNestedWriteableObjectTree<T>
21
*/
22
abstract class DaftWriteableObjectMemoryTree extends DaftObjectMemoryTree implements DaftNestedWriteableObjectTree
23
{
24
    const DEFINITELY_BELOW = false;
25
26
    const EXCLUDE_ROOT = false;
27
28
    const INSERT_AFTER = false;
29
30
    const LIMIT_ONE = 1;
31
32
    const RELATIVE_DEPTH_SAME = 0;
33
34
    const INT_ARG_INDEX_SECOND = 2;
35
36
    /**
37
    * @psalm-param T $newLeaf
38
    * @psalm-param T $referenceLeaf
39
    *
40
    * @psalm-return T
41
    */
42 84
    public function ModifyDaftNestedObjectTreeInsert(
43
        DaftNestedWriteableObject $newLeaf,
44
        DaftNestedWriteableObject $referenceLeaf,
45
        bool $before = self::INSERT_AFTER,
46
        bool $above = null
47
    ) : DaftNestedWriteableObject {
48 84
        if ($newLeaf->GetId() === $referenceLeaf->GetId()) {
49 40
            throw new InvalidArgumentException('Cannot modify leaf relative to itself!');
50
        }
51
52 60
        if ((bool) $above) {
53 48
            $this->ModifyDaftNestedObjectTreeInsertAbove($newLeaf, $referenceLeaf);
54 32
        } elseif (self::DEFINITELY_BELOW === $above) {
55 6
            $this->ModifyDaftNestedObjectTreeInsertBelow($newLeaf, $referenceLeaf);
56
        } else {
57 28
            $this->ModifyDaftNestedObjectTreeInsertAdjacent($newLeaf, $referenceLeaf, $before);
58
        }
59
60 60
        return $this->RebuildAfterInsert($newLeaf);
61
    }
62
63
    /**
64
    * @param DaftNestedWriteableObject|scalar|(scalar|array|object|null)[] $leaf
65
    * @param DaftNestedWriteableObject|scalar|(scalar|array|object|null)[] $referenceId
66
    *
67
    * @psalm-param T|scalar|(scalar|array|object|null)[] $leaf
68
    * @psalm-param T|scalar|(scalar|array|object|null)[] $referenceId
69
    *
70
    * @psalm-return T
71
    */
72 48
    public function ModifyDaftNestedObjectTreeInsertLoose(
73
        $leaf,
74
        $referenceId,
75
        bool $before = self::INSERT_AFTER,
76
        bool $above = null
77
    ) : DaftNestedWriteableObject {
78
        /**
79
        * @var DaftNestedWriteableObject
80
        *
81
        * @psalm-var T
82
        */
83 48
        $leaf = $this->MaybeGetLeafOrThrow($leaf);
84
85 24
        $reference = $this->MaybeRecallLoose($referenceId);
86
87 24
        if ($reference instanceof DaftNestedWriteableObject) {
0 ignored issues
show
introduced by
$reference is always a sub-type of SignpostMarv\DaftObject\DaftNestedWriteableObject.
Loading history...
88 18
            return $this->ModifyDaftNestedObjectTreeInsert($leaf, $reference, $before, $above);
89
        }
90
91 24
        return $this->ModifyDaftNestedObjectTreeInsertLooseIntoTree($leaf, $before, $above);
92
    }
93
94
    /**
95
    * @psalm-param T $root
96
    * @psalm-param T|null $replacementRoot
97
    */
98 8
    public function ModifyDaftNestedObjectTreeRemoveWithObject(
99
        DaftNestedWriteableObject $root,
100
        ? DaftNestedWriteableObject $replacementRoot
101
    ) : int {
102
        if (
103 8
            $this->CountDaftNestedObjectTreeWithObject(
104 8
                $root,
105 8
                false,
106 8
                null
107 8
            ) > AbstractArrayBackedDaftNestedObject::COUNT_EXPECT_NON_EMPTY &&
108 8
            is_null($replacementRoot)
109
        ) {
110 2
            throw new BadMethodCallException('Cannot leave orphan objects in a tree');
111
        }
112
113 6
        $root = $this->StoreThenRetrieveFreshLeaf($root);
114
115 6
        if ( ! is_null($replacementRoot)) {
116 4
            $this->ModifyDaftNestedObjectTreeRemoveWithObjectPrepareRemovalAndRebuild(
117 4
                $root,
118 4
                $replacementRoot
119
            );
120
        }
121
122 6
        $this->RemoveDaftObject($root);
123
124 6
        $this->RebuildTreeInefficiently();
125
126 6
        return $this->CountDaftNestedObjectFullTree();
127
    }
128
129
    /**
130
    * @param scalar|(scalar|array|object|null)[] $root
131
    * @param scalar|(scalar|array|object|null)[]|null $replacementRoot
132
    */
133 12
    public function ModifyDaftNestedObjectTreeRemoveWithId($root, $replacementRoot) : int
134
    {
135 12
        $rootObject = $this->RecallDaftObject($root);
136
137 12
        $resp = null;
138
139 12
        if ($rootObject instanceof DaftNestedWriteableObject) {
140 12
            $resp = $this->ModifyDaftNestedObjectTreeRemoveWithIdUsingRootObject(
141 12
                $replacementRoot,
142 12
                $rootObject
143
            );
144
        }
145
146 8
        return is_int($resp) ? $resp : $this->CountDaftNestedObjectFullTree();
147
    }
148
149
    /**
150
    * @psalm-param T $leaf
151
    *
152
    * @psalm-return T
153
    */
154 62
    public function StoreThenRetrieveFreshLeaf(
155
        DaftNestedWriteableObject $leaf
156
    ) : DaftNestedWriteableObject {
157 62
        $this->RememberDaftObject($leaf);
158 62
        $this->ForgetDaftObject($leaf);
159 62
        $this->ForgetDaftObjectById($leaf->GetId());
160
161
        /**
162
        * @psalm-var class-string<T>
163
        */
164 62
        $type = get_class($leaf);
165
166
        /**
167
        * @var DaftNestedWriteableObject
168
        *
169
        * @psalm-var T
170
        */
171 62
        $out = $this->RecallDaftObjectOrThrow($leaf->GetId(), $type);
172
173 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...
174
    }
175
176
    /**
177
    * @psalm-param T $object
178
    */
179 88
    public function RememberDaftObject(SuitableForRepositoryType $object) : void
180
    {
181
        /**
182
        * @var DaftNestedWriteableObject
183
        *
184
        * @psalm-var T
185
        */
186 88
        $object = $object;
187
188 88
        $left = $object->GetIntNestedLeft();
0 ignored issues
show
Bug introduced by
The method GetIntNestedLeft() does not exist on SignpostMarv\DaftObject\SuitableForRepositoryType. It seems like you code against a sub-type of SignpostMarv\DaftObject\SuitableForRepositoryType such as SignpostMarv\DaftObject\DaftNestedObject. ( Ignorable by Annotation )

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

188
        /** @scrutinizer ignore-call */ 
189
        $left = $object->GetIntNestedLeft();
Loading history...
189 88
        $right = $object->GetIntNestedRight();
0 ignored issues
show
Bug introduced by
The method GetIntNestedRight() does not exist on SignpostMarv\DaftObject\SuitableForRepositoryType. It seems like you code against a sub-type of SignpostMarv\DaftObject\SuitableForRepositoryType such as SignpostMarv\DaftObject\DaftNestedObject. ( Ignorable by Annotation )

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

189
        /** @scrutinizer ignore-call */ 
190
        $right = $object->GetIntNestedRight();
Loading history...
190 88
        $level = $object->GetIntNestedLevel();
0 ignored issues
show
Bug introduced by
The method GetIntNestedLevel() does not exist on SignpostMarv\DaftObject\SuitableForRepositoryType. It seems like you code against a sub-type of SignpostMarv\DaftObject\SuitableForRepositoryType such as SignpostMarv\DaftObject\DaftNestedObject. ( Ignorable by Annotation )

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

190
        /** @scrutinizer ignore-call */ 
191
        $level = $object->GetIntNestedLevel();
Loading history...
191
192 88
        if (0 === $left && 0 === $right && 0 === $level) {
193 86
            $fullTreeCount = $this->CountDaftNestedObjectFullTree();
194
195 86
            if ($fullTreeCount > AbstractArrayBackedDaftNestedObject::COUNT_EXPECT_NON_EMPTY) {
196 60
                $tree = $this->RecallDaftNestedObjectFullTree();
197
198
                /**
199
                * @var DaftNestedWriteableObject
200
                *
201
                * @psalm-var T
202
                */
203 60
                $end = end($tree);
204
205 60
                $left = $end->GetIntNestedRight() + 1;
206
            } else {
207 86
                $left = $fullTreeCount + $fullTreeCount;
208
            }
209
210 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

210
            $object->/** @scrutinizer ignore-call */ 
211
                     SetIntNestedLeft($left);
Loading history...
211 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

211
            $object->/** @scrutinizer ignore-call */ 
212
                     SetIntNestedRight($left + 1);
Loading history...
212
        }
213
214 88
        parent::RememberDaftObject($object);
215 88
    }
216
217
    /**
218
    * @psalm-param T $newLeaf
219
    * @psalm-param T $referenceLeaf
220
    */
221 52
    protected function ModifyDaftNestedObjectTreeInsertAdjacent(
222
        DaftNestedWriteableObject $newLeaf,
223
        DaftNestedWriteableObject $referenceLeaf,
224
        bool $before
225
    ) : void {
226 52
        $siblings = $this->SiblingsExceptLeaf($newLeaf, $referenceLeaf);
227
228 52
        $siblingIds = [];
229 52
        $siblingSort = [];
230 52
        $j = count($siblings);
231
232 52
        foreach ($siblings as $leaf) {
233
            /**
234
            * @var scalar|(scalar|array|object|null)[]
235
            */
236 28
            $siblingId = $leaf->GetId();
237 28
            $siblingIds[] = $siblingId;
238 28
            $siblingSort[] = $leaf->GetIntNestedSortOrder();
239
        }
240
241 52
        $pos = array_search($referenceLeaf->GetId(), $siblingIds, true);
242
243 52
        if (false === $pos) {
244 24
            throw new RuntimeException('Reference leaf not found in siblings tree!');
245
        }
246
247 28
        for ($i = 0; $i < $j; ++$i) {
248 28
            $siblings[$i]->SetIntNestedSortOrder(
249 28
                $siblingSort[$i] +
250 28
                (($before ? ($i < $pos) : ($i <= $pos)) ? self::DECREMENT : self::INCREMENT)
251
            );
252 28
            $this->StoreThenRetrieveFreshLeaf($siblings[$i]);
253
        }
254
255 28
        $newLeaf->SetIntNestedSortOrder($siblingSort[$pos]);
256 28
        $newLeaf->AlterDaftNestedObjectParentId($referenceLeaf->ObtainDaftNestedObjectParentId());
257
258 28
        $this->StoreThenRetrieveFreshLeaf($newLeaf);
259 28
    }
260
261 60
    protected function RebuildTreeInefficiently() : void
262
    {
263 60
        $rebuilder = new InefficientDaftNestedRebuild($this);
264 60
        $rebuilder->RebuildTree();
265 60
    }
266
267
    /**
268
    * @psalm-param T $newLeaf
269
    *
270
    * @psalm-return T
271
    */
272 60
    private function RebuildAfterInsert(
273
        DaftNestedWriteableObject $newLeaf
274
    ) : DaftNestedWriteableObject {
275 60
        $this->RebuildTreeInefficiently();
276
277
        /**
278
        * @psalm-var class-string<T>
279
        */
280 60
        $type = get_class($newLeaf);
281
282
        /**
283
        * @var DaftNestedWriteableObject
284
        *
285
        * @psalm-var T
286
        */
287 60
        $out = $this->RecallDaftObjectOrThrow($newLeaf->GetId(), $type);
288
289 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...
290
    }
291
292
    /**
293
    * @psalm-param T $root
294
    * @psalm-param T $replacementRoot
295
    */
296 4
    private function ModifyDaftNestedObjectTreeRemoveWithObjectPrepareRemovalAndRebuild(
297
        DaftNestedWriteableObject $root,
298
        DaftNestedWriteableObject $replacementRoot
299
    ) : void {
300 4
        $this->UpdateRoots($root, $this->StoreThenRetrieveFreshLeaf($replacementRoot)->GetId());
301 4
    }
302
303
    /**
304
    * @param scalar|(scalar|array|object|null)[] $replacementRootId
305
    *
306
    * @psalm-param T $root
307
    */
308 10
    private function UpdateRoots(DaftNestedWriteableObject $root, $replacementRootId) : void
309
    {
310
        /**
311
        * @var array<int, DaftNestedWriteableObject>
312
        *
313
        * @psalm-var array<int, T>
314
        */
315 10
        $alterThese = $this->RecallDaftNestedObjectTreeWithObject($root, false, self::LIMIT_ONE);
316
317 10
        foreach ($alterThese as $alter) {
318 4
            $alter->AlterDaftNestedObjectParentId($replacementRootId);
319 4
            $this->RememberDaftObject($alter);
320
        }
321 10
    }
322
323
    /**
324
    * @param DaftNestedWriteableObject|scalar|(scalar|array|object|null)[] $leaf
325
    *
326
    * @psalm-param T|scalar|(scalar|array|object|null)[] $leaf
327
    * @psalm-param class-string<T> $type
328
    *
329
    * @psalm-return T
330
    */
331 48
    private function MaybeGetLeafOrThrow(
332
        $leaf,
333
        string $type = DaftNestedWriteableObject::class
334
    ) : DaftNestedWriteableObject {
335 48
        if ($leaf === $this->GetNestedObjectTreeRootId()) {
336 24
            throw new InvalidArgumentException('Cannot pass root id as new leaf');
337 24
        } elseif ($leaf instanceof DaftNestedWriteableObject) {
338 24
            return $this->StoreThenRetrieveFreshLeaf($leaf);
339
        }
340
341
        /**
342
        * @psalm-var scalar|(scalar|array|object|null)[]
343
        */
344 16
        $leaf = $leaf;
345
346
        /**
347
        * @var DaftNestedWriteableObject
348
        *
349
        * @psalm-var T
350
        */
351 16
        $out = $this->RecallDaftObjectOrThrow($leaf, $type);
352
353 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...
354
    }
355
356
    /**
357
    * @param DaftNestedWriteableObject|scalar|(scalar|array|object|null)[] $leaf
358
    *
359
    * @psalm-param T|scalar|(scalar|array|object|null)[] $leaf
360
    */
361 24
    private function MaybeRecallLoose($leaf) : ? DaftNestedWriteableObject
362
    {
363 24
        if ($leaf instanceof DaftNestedWriteableObject) {
364 2
            return $leaf;
365
        }
366
367
        /**
368
        * @var scalar|(scalar|array|object|null)[]
369
        */
370 24
        $leaf = $leaf;
371
372
        /**
373
        * @var DaftNestedWriteableObject|null
374
        *
375
        * @psalm-var T|null
376
        */
377 24
        $out = $this->RecallDaftObject($leaf);
378
379 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...
380
    }
381
382
    /**
383
    * @psalm-param T $leaf
384
    *
385
    * @psalm-return T
386
    */
387 24
    private function ModifyDaftNestedObjectTreeInsertLooseIntoTree(
388
        DaftNestedWriteableObject $leaf,
389
        bool $before,
390
        ? bool $above
391
    ) : DaftNestedWriteableObject {
392
        /**
393
        * @var array<int, DaftNestedWriteableObject>
394
        *
395
        * @psalm-var array<int, T>
396
        */
397 24
        $leaves = array_filter(
398 24
            $this->RecallDaftNestedObjectFullTree(self::RELATIVE_DEPTH_SAME),
399
            /**
400
            * @psalm-param T $e
401
            */
402
            function (DaftNestedWriteableObject $e) use ($leaf) : bool {
403 24
                return $e->GetId() !== $leaf->GetId();
404 24
            }
405
        );
406
407 24
        if (count($leaves) < 1) {
408 24
            $leaf->SetIntNestedLeft(0);
409 24
            $leaf->SetIntNestedRight(1);
410 24
            $leaf->SetIntNestedLevel(0);
411 24
            $leaf->AlterDaftNestedObjectParentId($this->GetNestedObjectTreeRootId());
412
413 24
            return $this->StoreThenRetrieveFreshLeaf($leaf);
414
        }
415
416
        /**
417
        * @psalm-var T
418
        */
419 24
        $reference = $before ? current($leaves) : end($leaves);
420
421 24
        return $this->ModifyDaftNestedObjectTreeInsert($leaf, $reference, $before, $above);
422
    }
423
424
    /**
425
    * @psalm-param T $newLeaf
426
    * @psalm-param T $referenceLeaf
427
    */
428 48
    private function ModifyDaftNestedObjectTreeInsertAbove(
429
        DaftNestedWriteableObject $newLeaf,
430
        DaftNestedWriteableObject $referenceLeaf
431
    ) : void {
432 48
        $newLeaf->AlterDaftNestedObjectParentId($referenceLeaf->ObtainDaftNestedObjectParentId());
433 48
        $referenceLeaf->AlterDaftNestedObjectParentId($newLeaf->GetId());
434
435 48
        $this->StoreThenRetrieveFreshLeaf($newLeaf);
436 48
        $this->StoreThenRetrieveFreshLeaf($referenceLeaf);
437 48
    }
438
439
    /**
440
    * @psalm-param T $newLeaf
441
    * @psalm-param T $referenceLeaf
442
    */
443 6
    private function ModifyDaftNestedObjectTreeInsertBelow(
444
        DaftNestedWriteableObject $newLeaf,
445
        DaftNestedWriteableObject $referenceLeaf
446
    ) : void {
447 6
        $newLeaf->AlterDaftNestedObjectParentId($referenceLeaf->GetId());
448 6
        $this->StoreThenRetrieveFreshLeaf($newLeaf);
449 6
    }
450
451
    /**
452
    * @psalm-param T $newLeaf
453
    * @psalm-param T $referenceLeaf
454
    *
455
    * @return array<int, DaftNestedWriteableObject>
456
    *
457
    * @psalm-return array<int, T>
458
    */
459 52
    private function SiblingsExceptLeaf(
460
        DaftNestedWriteableObject $newLeaf,
461
        DaftNestedWriteableObject $referenceLeaf
462
    ) : array {
463
        /**
464
        * @var array<int, DaftNestedWriteableObject>
465
        *
466
        * @psalm-var array<int, T>
467
        */
468 52
        $out = array_values(array_filter(
469 52
            $this->RecallDaftNestedObjectTreeWithId(
470 52
                $referenceLeaf->ObtainDaftNestedObjectParentId(),
471 52
                self::EXCLUDE_ROOT,
472 52
                self::RELATIVE_DEPTH_SAME
473
            ),
474
            /**
475
            * @psalm-param T $leaf
476
            */
477
            function (DaftNestedWriteableObject $leaf) use ($newLeaf) : bool {
478 28
                return $leaf->GetId() !== $newLeaf->GetId();
479 52
            }
480
        ));
481
482 52
        return $out;
483
    }
484
485
    /**
486
    * @param scalar|(scalar|array|object|null)[]|null $replacementRoot
487
    *
488
    * @psalm-param T $rootObject
489
    */
490 12
    private function ModifyDaftNestedObjectTreeRemoveWithIdUsingRootObject(
491
        $replacementRoot,
492
        DaftNestedWriteableObject $rootObject
493
    ) : ? int {
494
        if (
495 12
            $this->CountDaftNestedObjectTreeWithObject(
496 12
                $rootObject,
497 12
                false,
498 12
                null
499 12
            ) > AbstractArrayBackedDaftNestedObject::COUNT_EXPECT_NON_EMPTY &&
500 12
            is_null($replacementRoot)
501
        ) {
502 2
            throw new BadMethodCallException('Cannot leave orphan objects in a tree');
503
        } elseif (
504 10
            ! is_null($replacementRoot) &&
505 10
            $replacementRoot !== $this->GetNestedObjectTreeRootId()
506
        ) {
507
            /**
508
            * @psalm-var class-string<T>
509
            */
510 4
            $type = get_class($rootObject);
511
512
            /**
513
            * @var DaftNestedWriteableObject
514
            *
515
            * @psalm-var T
516
            */
517 4
            $replacement = $this->RecallDaftObjectOrThrow($replacementRoot, $type);
518
519 2
            return $this->ModifyDaftNestedObjectTreeRemoveWithObject(
520 2
                $rootObject,
521 2
                $replacement
522
            );
523
        }
524
525
        /**
526
        * @var scalar|(scalar|array|object|null)[]
527
        */
528 6
        $replacementRoot = $replacementRoot;
529
530 6
        $this->UpdateRoots($rootObject, $replacementRoot);
531
532 6
        $this->RemoveDaftObject($rootObject);
533
534 6
        $this->RebuildTreeInefficiently();
535
536 6
        return null;
537
    }
538
}
539