Test Failed
Push — master ( 7826a3...f27fcf )
by SignpostMarv
06:31
created

DaftWriteableObjectMemoryTree::UpdateRoots()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 3
nop 2
dl 0
loc 11
ccs 0
cts 0
cp 0
crap 12
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 112
* @template T as DaftNestedWriteableObject
17
*
18
* @template-extends DaftObjectMemoryTree<T>
19
*
20 112
* @template-implements DaftNestedWriteableObjectTree<T>
21 112
*/
22 112
abstract class DaftWriteableObjectMemoryTree extends DaftObjectMemoryTree implements DaftNestedWriteableObjectTree
23 112
{
24 112
    /**
25
    * @psalm-param T $newLeaf
26
    * @psalm-param T $referenceLeaf
27 112
    *
28 112
    * @psalm-return T
29
    */
30
    public function ModifyDaftNestedObjectTreeInsert(
31
        DaftNestedWriteableObject $newLeaf,
32
        DaftNestedWriteableObject $referenceLeaf,
33 52
        bool $before = false,
34
        bool $above = null
35
    ) : DaftNestedWriteableObject {
36
        if ($newLeaf->GetId() === $referenceLeaf->GetId()) {
37 52
            throw new InvalidArgumentException('Cannot modify leaf relative to itself!');
38
        }
39 50
40
        if (true === $above) {
41
            $this->ModifyDaftNestedObjectTreeInsertAbove($newLeaf, $referenceLeaf);
42
        } elseif (false === $above) {
43
            $this->ModifyDaftNestedObjectTreeInsertBelow($newLeaf, $referenceLeaf);
44
        } else {
45
            $this->ModifyDaftNestedObjectTreeInsertAdjacent($newLeaf, $referenceLeaf, $before);
46
        }
47
48
        return $this->RebuildAfterInsert($newLeaf);
49
    }
50
51
    /**
52
    * @param DaftNestedWriteableObject|scalar|(scalar|array|object|null)[] $leaf
53
    * @param DaftNestedWriteableObject|scalar|(scalar|array|object|null)[] $referenceId
54
    *
55
    * @psalm-param T|scalar|(scalar|array|object|null)[] $leaf
56
    * @psalm-param T|scalar|(scalar|array|object|null)[] $referenceId
57
    *
58
    * @psalm-return T
59
    */
60
    public function ModifyDaftNestedObjectTreeInsertLoose(
61
        $leaf,
62
        $referenceId,
63
        bool $before = false,
64
        bool $above = null
65
    ) : DaftNestedWriteableObject {
66
        $leaf = $this->MaybeGetLeaf($leaf);
67
68
        $reference = $referenceId;
69
70
        if ( ! ($referenceId instanceof DaftNestedWriteableObject)) {
71
            /**
72
            * @var scalar|(scalar|array|object|null)[]
73
            */
74
            $referenceId = $referenceId;
75
76
            $reference = $this->RecallDaftObject($referenceId);
77
        }
78
79
        /**
80
        * @var DaftNestedWriteableObject|null
81
        *
82
        * @psalm-var T|null
83
        */
84
        $reference = $reference;
85
86
        $resp = $this->ModifyDaftNestedObjectTreeInsertMaybeLooseIntoTree(
87
            $this,
88
            $leaf,
89
            $reference,
90
            $referenceId === $this->GetNestedObjectTreeRootId(),
91
            $before,
92
            $above
93
        );
94
95
        if ($resp instanceof DaftNestedWriteableObject) {
96
            return $resp;
97
        }
98
99
        throw new InvalidArgumentException(sprintf(
100
            'Argument %u passed to %s() did not resolve to a leaf node!',
101
            is_null($leaf) ? 1 : 2,
102
            __METHOD__
103
        ));
104
    }
105
106
    /**
107
    * @psalm-param T $root
108
    * @psalm-param T|null $replacementRoot
109
    */
110
    public function ModifyDaftNestedObjectTreeRemoveWithObject(
111
        DaftNestedWriteableObject $root,
112
        ? DaftNestedWriteableObject $replacementRoot
113
    ) : int {
114
        if (
115
            $this->CountDaftNestedObjectTreeWithObject(
116
                $root,
117
                false,
118
                null
119
            ) > AbstractArrayBackedDaftNestedObject::COUNT_EXPECT_NON_EMPTY &&
120
            is_null($replacementRoot)
121
        ) {
122
            throw new BadMethodCallException('Cannot leave orphan objects in a tree');
123
        }
124
125
        $root = $this->StoreThenRetrieveFreshLeaf($root);
126
127
        if ( ! is_null($replacementRoot)) {
128
            $this->ModifyDaftNestedObjectTreeRemoveWithObjectPrepareRemovalAndRebuild(
129
                $root,
130
                $replacementRoot
131
            );
132
        }
133
134
        $this->RemoveDaftObject($root);
135
136
        $this->RebuildTreeInefficiently();
137
138
        return $this->CountDaftNestedObjectFullTree();
139
    }
140
141
    /**
142
    * @param scalar|(scalar|array|object|null)[] $root
143
    * @param scalar|(scalar|array|object|null)[]|null $replacementRoot
144
    */
145
    public function ModifyDaftNestedObjectTreeRemoveWithId($root, $replacementRoot) : int
146
    {
147
        $rootObject = $this->RecallDaftObject($root);
148
149
        $resp = null;
150
151
        if ($rootObject instanceof DaftNestedWriteableObject) {
152
            $resp = $this->ModifyDaftNestedObjectTreeRemoveWithIdUsingRootObject(
153
                $replacementRoot,
154
                $rootObject
155
            );
156
        }
157
158
        return is_int($resp) ? $resp : $this->CountDaftNestedObjectFullTree();
159
    }
160
161
    /**
162
    * @psalm-param T $leaf
163
    *
164
    * @psalm-return T
165
    */
166
    public function StoreThenRetrieveFreshLeaf(
167
        DaftNestedWriteableObject $leaf
168
    ) : DaftNestedWriteableObject {
169
        $this->RememberDaftObject($leaf);
170
        $this->ForgetDaftObject($leaf);
171
        $this->ForgetDaftObjectById($leaf->GetId());
172
173
        $fresh = $this->RecallDaftObject($leaf->GetId());
174
175
        if ( ! ($fresh instanceof DaftNestedWriteableObject)) {
176
            throw new RuntimeException('Was not able to obtain a fresh copy of the object!');
177
        }
178
179
        return $fresh;
180
    }
181
182
    /**
183
    * @psalm-param T $object
184
    */
185
    public function RememberDaftObject(SuitableForRepositoryType $object) : void
186
    {
187
        /**
188
        * @var DaftNestedWriteableObject
189
        *
190
        * @psalm-var T
191
        */
192
        $object = $object;
193
194
        $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

194
        /** @scrutinizer ignore-call */ 
195
        $left = $object->GetIntNestedLeft();
Loading history...
195
        $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

195
        /** @scrutinizer ignore-call */ 
196
        $right = $object->GetIntNestedRight();
Loading history...
196
        $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

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

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

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