NestedSet::deletePullUpChildren()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 3
c 1
b 0
f 0
dl 0
loc 8
ccs 4
cts 4
cp 1
rs 10
cc 1
nc 1
nop 2
crap 1
1
<?php
2
3
namespace kalanis\nested_tree;
4
5
use kalanis\nested_tree\Sources\SourceInterface;
6
7
/**
8
 * Nested Set class for build left, right, level data.
9
 *
10
 * @see http://mikehillyer.com/articles/managing-hierarchical-data-in-mysql/ Query references.
11
 */
12
class NestedSet
13
{
14 79
    public function __construct(
15
        protected readonly SourceInterface $source,
16
        protected readonly Support\Node $nodeBase = new Support\Node(),
17
        protected readonly Support\TableSettings $tableSettings = new Support\TableSettings(),
18
    ) {
19 79
    }
20
21
    /**
22
     * Add new record to structure
23
     * The position is set to end
24
     *
25
     * Note: Properties of tree will be skipped and filled later. For their change use different methods.
26
     *
27
     * @param Support\Node $node
28
     * @param Support\Options $options
29
     * @return Support\Node
30
     */
31 2
    public function add(Support\Node $node, Support\Options $options = new Support\Options()) : Support\Node
32
    {
33 2
        $node->position = $this->getNewPosition($node->parentId);
34
35 2
        return $this->source->add($node, $options->where);
36
    }
37
38
    /**
39
     * Update current record in structure
40
     *
41
     * Note: Properties with Null value will be skipped and stay same in the storage.
42
     * Note: Properties of tree will be skipped too. For their change use different methods.
43
     *
44
     * @param Support\Node $node
45
     * @param Support\Options $options
46
     * @return bool
47
     */
48 2
    public function update(Support\Node $node, Support\Options $options = new Support\Options()) : bool
49
    {
50 2
        return $this->source->updateData($node, $options->where);
51
    }
52
53
    /**
54
     * Change parent node to different one; put the content on the last position
55
     * Parent id can be either some number for existing one or 0/null for root
56
     *
57
     * Return true for pass, false otherwise
58
     *
59
     * @param int<1, max> $nodeId
60
     * @param int<0, max>|null $newParentId
61
     * @param Support\Options $options
62
     * @return bool
63
     */
64 2
    public function changeParent(int $nodeId, ?int $newParentId, Support\Options $options = new Support\Options()) : bool
65
    {
66 2
        if (!$this->isNewParentOutsideCurrentNodeTree($nodeId, $newParentId, $options)) {
67 1
            return false;
68
        }
69 2
        $newPosition = $this->getNewPosition($newParentId, $options->where);
70
71 2
        return $this->source->updateNodeParent($nodeId, $newParentId, $newPosition, $options->where);
72
    }
73
74
    /**
75
     * Move node to new position
76
     *
77
     * @param int<1, max> $nodeId
78
     * @param int<0, max> $newPosition
79
     * @param Support\Options $options
80
     * @return bool
81
     */
82 6
    public function move(int $nodeId, int $newPosition, Support\Options $options = new Support\Options()) : bool
83
    {
84 6
        $parentId = $this->source->selectParent($nodeId, $options);
85
        // get single node
86 6
        $customOptions = new Support\Options();
87 6
        $customOptions->currentId = $nodeId;
88 6
        $nodes = $this->source->selectSimple($customOptions);
89 6
        $node = reset($nodes);
90 6
        if (empty($node)) {
91 1
            return false;
92
        }
93
        // move it
94 5
        if ($this->source->makeHole($parentId, $newPosition, $newPosition > $node->position, $options->where)) {
95 4
            return $this->source->updateNodeParent($nodeId, $parentId, $newPosition, $options->where);
96
        }
97
98 1
        return false;
99
    }
100
101
    /**
102
     * Delete the selected taxonomy ID and pull children's parent ID to the same as selected one.<br>
103
     * Example: selected taxonomy ID is 4, its parent ID is 2. This method will be pull all children that has parent ID = 4 to 2 and delete the taxonomy ID 4.<br>
104
     * Always run <code>$NestedSet->rebuild()</code> after insert, update, delete to rebuild the correctly level, left, right data.
105
     *
106
     * @param int<1, max> $nodeId The selected taxonomy ID.
107
     * @param Support\Options $options Where array structure will be like this.
108
     * @return bool Return true on success, false for otherwise.
109
     */
110 6
    public function deletePullUpChildren(int $nodeId, Support\Options $options = new Support\Options()) : bool
111
    {
112
        // get this taxonomy parent id
113 6
        $parentNodeId = $this->source->selectParent($nodeId, $options);
114
        // update this children first level.
115 6
        $this->source->updateChildrenParent($nodeId, $parentNodeId, $options->where);
116
117 6
        return $this->source->deleteSolo($nodeId, $options->where);
118
    }
119
120
    /**
121
     * Delete the selected taxonomy ID with its ALL children.<br>
122
     * Always run <code>$NestedSet->rebuild()</code> after insert, update, delete to rebuild the correctly level, left, right data.
123
     *
124
     * The columns `left`, `right` must have been built before using this method, otherwise the result will be incorrect.
125
     *
126
     * @param int<1, max> $nodeId The taxonomy ID to delete.
127
     * @param Support\Options $options Where array structure will be like this.
128
     * @return int|null Return number on success, return null for otherwise.
129
     */
130 5
    public function deleteWithChildren(int $nodeId, Support\Options $options = new Support\Options()) : ?int
131
    {
132 5
        $options->currentId = $nodeId;
133 5
        $options->unlimited = true;
134 5
        $result = $this->getNodesWithChildren($options);
135 5
        $i_count = 0;
136
137 5
        if (!empty($result->items)) {
138 4
            foreach ($result->items as $row) {
139 4
                if ($this->source->deleteWithChildren($row, $options->where)) {
140 3
                    $i_count++;
141
                }
142
            }
143
        }
144
145 5
        if (0 >= $i_count) {
146 2
            return null;
147
        }
148
149 3
        return $i_count;
150
    }
151
152
    /**
153
     * Get new position for taxonomy in the selected parent.
154
     *
155
     * @param int<0, max>|null $parentId The parent ID. If root, set this to 0 or null.
156
     * @param Support\Conditions|null $where Where array structure will be like this.
157
     * @return int<1, max> Return the new position in the same parent.<br>
158
     *              WARNING! If there are no results or the results according to the conditions cannot be found. It always returns 1.
159
     */
160 7
    public function getNewPosition(?int $parentId, ?Support\Conditions $where = null) : int
161
    {
162 7
        $lastPosition = $this->source->selectLastPosition($parentId, $where);
163
164 7
        return null === $lastPosition ? 1 : $lastPosition + 1;
165
    }
166
167
    /**
168
     * Get taxonomy from selected item and fetch its ALL children.<br>
169
     * Example: There are taxonomy tree like this. Root 1 > 1.1 > 1.1.1, Root 2, Root 3 > 3.1, Root 3 > 3.2 > 3.2.1, Root 3 > 3.2 > 3.2.2, Root 3 > 3.3<br>
170
     * Assume that selected item is Root 3. So, the result will be Root 3 > 3.1, 3.2 > 3.2.1, 3.2.2, 3.3<br>
171
     *
172
     * Warning! Even this method has options for search, custom where conditions,
173
     * but it is recommended that you should set the option to select only specific item.<br>
174
     * This method is intended to show results from a single target.
175
     *
176
     * The columns `left`, `right` must have been built before using this method, otherwise the result will be incorrect.
177
     *
178
     * @param Support\Options $options Available options
179
     *
180
     * @return Support\Result Return object of taxonomy data
181
     */
182 10
    public function getNodesWithChildren(Support\Options $options = new Support\Options()) : Support\Result
183
    {
184
        // set unwanted options that is available in `listTaxonomy()` method to defaults
185 10
        $options->parentId = null;
186 10
        $options->filterIdBy = [];
187 10
        $options->noSortOrder = false;
188
        // set required option.
189 10
        $options->listFlattened = true;
190
191 10
        return $this->listNodes($options);
192
    }
193
194
    /**
195
     * Get simple taxonomy from all known; set things to make the query more limited
196
     *
197
     * @param Support\Options $options
198
     * @return Support\Node|null
199
     */
200 2
    public function getNode(Support\Options $options = new Support\Options()) : ?Support\Node
201
    {
202 2
        $options = clone $options;
203 2
        $options->unlimited = false;
204 2
        $options->offset = 0;
205 2
        $options->limit = 1;
206 2
        $options->noSortOrder = false;
207 2
        $nodes = $this->source->selectLimited($options);
208 2
        if (empty($nodes)) {
209 1
            return null;
210
        }
211
212 1
        return reset($nodes);
213
    }
214
215
    /**
216
     * Get taxonomy from selected item and fetch its parent in a line until root item.<br>
217
     * Example: There are taxonomy tree like this. Root1 > 1.1 > 1.1.1 > 1.1.1.1<br>
218
     * Assume that you selected at 1.1.1. So, the result will be Root1 > 1.1 > 1.1.1<br>
219
     * But if you set 'skipCurrent' to true the result will be Root1 > 1.1
220
     *
221
     * Warning! Even this method has options for search, custom where conditions,
222
     * but it is recommended that you should set the option to select only specific item.<br>
223
     * This method is intended to show results from a single target.
224
     *
225
     * The columns `left`, `right` must have been built before using this method, otherwise the result will be incorrect.
226
     *
227
     * @see http://mikehillyer.com/articles/managing-hierarchical-data-in-mysql/ Original source.
228
     * @param Support\Options $options Available options
229
     * @return Support\Result Return array object of taxonomy data
230
     */
231 12
    public function getNodesWithParents(Support\Options $options = new Support\Options()) : Support\Result
232
    {
233 12
        $result = new Support\Result();
234 12
        $result->items = $this->source->selectWithParents($options);
235 12
        $result->count = count($result->items);
236
237 12
        return $result;
238
    }
239
240
    /**
241
     * Rebuild children into array.
242
     *
243
     * @internal This method was called from `getTreeWithChildren()`.
244
     * @param array<int<0, max>, Support\Node> $array The array data that was get while running `getTreeWithChildren()`. This data contains 'children' object property but empty, it will be added here.
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<int<0, max>, Support\Node> at position 2 could not be parsed: Expected '>' at position 2, but found 'int'.
Loading history...
245
     * @return array<int<0, max>, Support\Node> Return added correct id of the children to data.
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<int<0, max>, Support\Node> at position 2 could not be parsed: Expected '>' at position 2, but found 'int'.
Loading history...
246
     */
247 72
    protected function getTreeRebuildChildren(array $array) : array
248
    {
249 72
        foreach ($array as $id => $row) {
250 72
            if (!is_null($row->parentId) && !empty($array[$row->parentId]) && ($row->parentId !== $row->id)) {
251 72
                $array[$row->parentId]->childrenIds[$id] = $id;
252 72
                $array[$row->parentId]->childrenNodes[$id] = $row;
253 72
            } elseif (is_null($row->parentId) && $this->tableSettings->rootIsNull && !empty($row->id)) {
254 20
                $array[0]->childrenIds[$id] = $id;
255 20
                $array[0]->childrenNodes[$id] = $row;
256
            }
257
        }
258
259 72
        return $array;
260
    }
261
262
    /**
263
     * Get the data nest tree with children.<br>
264
     * Its result will be look like this...<pre>
265
     * Array(
266
     *     [0] => Support\Node Object
267
     *         (
268
     *             [id] => 0
269
     *             [children] => Array
270
     *                 (
271
     *                     [1] => 1
272
     *                     [2] => 2
273
     *                     [3] => 3
274
     *                 )
275
     *         )
276
     *     [1] => Support\Node Object
277
     *         (
278
     *             [id] => 1
279
     *             [parent_id] => 0
280
     *             [level] => 1
281
     *             [children] => Array
282
     *                 (
283
     *                 )
284
     *         )
285
     *     [2] => Support\Node Object
286
     *         (
287
     *             [id] => 2
288
     *             [parent_id] => 0
289
     *             [level] => 1
290
     *             [children] => Array
291
     *                 (
292
     *                     [4] => 4
293
     *                     [5] => 5
294
     *                 )
295
     *         )
296
     *     [3] => Support\Node Object
297
     *         (
298
     *             [id] => 3
299
     *             [parent_id] => 0
300
     *             [level] => 1
301
     *             [children] => Array
302
     *                 (
303
     *                 )
304
     *         )
305
     *     [4] => Support\Node Object
306
     *         (
307
     *             [id] => 4
308
     *             [parent_id] => 2
309
     *             [level] => 2
310
     *             [children] => Array
311
     *                 (
312
     *                 )
313
     *         )
314
     *     [5] => Support\Node Object
315
     *         (
316
     *             [id] => 5
317
     *             [parent_id] => 2
318
     *             [level] => 2
319
     *             [children] => Array
320
     *                 (
321
     *                 )
322
     *         )
323
     * )</pre>
324
     *
325
     * Usually, this method is for get taxonomy tree data in the array format that suit for loop/nest loop verify level.
326
     *
327
     * @since 1.0
328
     * @internal This method was called from `rebuild()`.
329
     * @param Support\Options $options Where array structure will be like this.
330
     * @return array<int<0, max>, Support\Node> Return formatted array structure as seen in example of docblock.
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<int<0, max>, Support\Node> at position 2 could not be parsed: Expected '>' at position 2, but found 'int'.
Loading history...
331
     */
332 69
    protected function getTreeWithChildren(Support\Options $options = new Support\Options()) : array
333
    {
334
        // create a root node to hold child data about first level items
335 69
        $result = $this->source->selectSimple($options);
336 69
        $result[0] = clone $this->nodeBase; // hack for root node
337
338
        // now process the array and build the child data
339 69
        return $this->getTreeRebuildChildren($result);
340
    }
341
342
    /**
343
     * Detect that is this taxonomy's parent setting to be under this taxonomy's children or not.<br>
344
     * For example: Root 1 > 1.1 > 1.1.1 > 1.1.1.1 > 1.1.1.1.1<br>
345
     * Assume that you are editing 1.1.1 and its parent is 1.1. Now you change its parent to 1.1.1.1.1 which is under its children.<br>
346
     * The parent of 1.1.1 must be root, Root 1, 1.1 and never go under that.
347
     *
348
     * @param int<1, max> $currentNodeId The taxonomy ID that is changing the parent.
349
     * @param int<0, max>|null $newParentId The selected parent ID to check.
350
     * @param Support\Options $options Where array structure will be like this.
351
     * @return bool Return `false` if its parent is under its children (INCORRECT changes).<br>
352
     *              Return `false` if search result was not found (INCORRECT changes).<br>
353
     *              Return `true` if its parent is not under its children (CORRECT changes).
354
     */
355 5
    public function isNewParentOutsideCurrentNodeTree(int $currentNodeId, ?int $newParentId, Support\Options $options = new Support\Options()) : bool
356
    {
357 5
        if (empty($newParentId)) {
358
            // if parent is root, always return false because that is correct!
359 2
            return true;
360
        }
361
362
        // check for selected parent that must not under this taxonomy.
363 4
        $options->currentId = $newParentId;
364 4
        $nodesWithParents = $this->getNodesWithParents($options);
365
366 4
        if (!empty($nodesWithParents->items)) {
367 4
            foreach ($nodesWithParents->items as $row) {
368 4
                if ($row->parentId === $currentNodeId) {
369 4
                    return false;
370
                }
371
            }
372
373 4
            return true;
374
        }
375
376 1
        return false;
377
    }
378
379
    /**
380
     * List taxonomy.
381
     *
382
     * The columns `left`, `right` must have been built before using this method, otherwise the result will be incorrect.
383
     *
384
     * @param Support\Options $options Available options
385
     */
386 42
    public function listNodes(Support\Options $options = new Support\Options()) : Support\Result
387
    {
388 42
        $output = new Support\Result();
389 42
        $output->count = $this->source->selectCount($options);
390 42
        $output->items = $this->source->selectLimited($options);
391
392 42
        if (!$options->listFlattened) {
393 20
            $output = $this->listNodesBuildTreeWithChildren($output, $options);
394
        }
395
396 42
        return $output;
397
    }
398
399
    /**
400
     * Build tree data with children.
401
     *
402
     * @internal This method was called from `listTaxonomy()`.
403
     * @param Support\Result $result The array item get from fetchAll() method using the PDO.
404
     * @param Support\Options $options Available options
405
     * @return Support\Result Return array data of formatted values.
406
     */
407 20
    protected function listNodesBuildTreeWithChildren(Support\Result $result, Support\Options $options) : Support\Result
408
    {
409 20
        $items = [];
410 20
        foreach ($result->items as &$item) {
411 20
            $items[$item->parentId][] = $item;
412
        }
413
414 20
        if (empty($options->filterIdBy)) {
415
            // without taxonomy_id_in option exists, this result can format to be hierarchical.
416 16
            foreach ($result->items as $row) {
417 16
                if (isset($items[$row->id])) {
418 14
                    $row->childrenNodes = $items[$row->id];
419 14
                    $row->childrenIds = array_map(fn (Support\Node $node) => $node->id, $row->childrenNodes);
420
                }
421
            }
422
423 16
            $partItems = ($items[0] ?? array_shift($items)); // this is important ([0]) for prevent duplicate items
424 16
            if (empty($partItems)) {
425
                return new Support\Result();
426
            } else {
427 16
                $result->items = $partItems;
428
            }
429
        }
430
431 20
        return $result;
432
    }
433
434
    /**
435
     * List taxonomy as flatten not tree.<br>
436
     * All parameters or arguments are same as `listTaxonomy()` method.
437
     *
438
     * @param Support\Options $options Available options
439
     */
440 16
    public function listNodesFlatten(Support\Options $options = new Support\Options()) : Support\Result
441
    {
442 16
        $options->listFlattened = true;
443
444 16
        return $this->listNodes($options);
445
    }
446
447
    /**
448
     * Rebuilds the tree data and save it to the database.<br>
449
     * This will rebuild the level, left, right values.
450
     *
451
     * The columns `left`, `right` must have been built before using this method, otherwise the result will be incorrect.
452
     *
453
     * @param Support\Options $options Where array structure will be like this.
454
     */
455 68
    public function rebuild(Support\Options $options = new Support\Options()) : void
456
    {
457
        // get taxonomy tree data in the array format that suit for loop/nest loop verify level.
458 68
        $data = $this->getTreeWithChildren($options);
459
460 68
        $n = 0; // need a variable to hold the running n tally
461 68
        $p = 0; // need a variable to hold the running position tally
462
463
        // rebuild positions
464 68
        $this->rebuildGeneratePositionData($data, 0, $p);
465
466
        // verify the level data. this method will be alter the $data value because it will be called as reference.
467
        // so, it doesn't need to use `$data = $this->rebuildGenerateTreeData()`;
468 68
        $this->rebuildGenerateTreeData($data, 0, 0, $n);
469
470 68
        foreach ($data as $id => $row) {
471 68
            if (0 === $id) {
472 68
                continue;
473
            }
474
475 68
            $this->source->updateLeftRightPos($row, $options->where);
476
        }
477
    }
478
479
    /**
480
     * Rebuild taxonomy level, left, right for tree data.<br>
481
     * This method will alter the $array value. It will be set level, left, right value.
482
     *
483
     * This method modify variables via argument reference without return anything.
484
     *
485
     * @internal This method was called from `rebuild()`.
486
     * @param array<int<0, max>, Support\Node> $array The data array, will be call as reference and modify its value.
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<int<0, max>, Support\Node> at position 2 could not be parsed: Expected '>' at position 2, but found 'int'.
Loading history...
487
     * @param int<0, max> $id The ID of taxonomy.
488
     * @param int<0, max> $level The level of taxonomy.
489
     * @param int<0, max> $n The tally or count number, will be call as reference and modify its value.
490
     */
491 69
    protected function rebuildGenerateTreeData(array &$array, int $id, int $level, int &$n) : void
492
    {
493 69
        $array[$id]->level = $level;
494 69
        $array[$id]->left = $n++;
495
496
        // loop over the node's children and process their data
497
        // before assigning the right value
498 69
        foreach ($array[$id]->childrenIds as $childNodeId) {
499 69
            $this->rebuildGenerateTreeData($array, $childNodeId, $level + 1, $n);
500
        }
501
502 69
        $array[$id]->right = $n++;
503
    }
504
505
    /**
506
     * Rebuild taxonomy positions for tree data.<br>
507
     * This method will alter the $array value. It will set position value.
508
     *
509
     * This method modify variables via argument reference without return anything.
510
     *
511
     * @internal This method was called from `rebuild()`.
512
     * @param array<int<0, max>, Support\Node> $array The data array, will be call as reference and modify its value.
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<int<0, max>, Support\Node> at position 2 could not be parsed: Expected '>' at position 2, but found 'int'.
Loading history...
513
     * @param int<0, max> $id The ID of taxonomy.
514
     * @param int<0, max> $n The position number, will be call as reference and modify its value.
515
     */
516 69
    protected function rebuildGeneratePositionData(array &$array, int $id, int &$n) : void
517
    {
518 69
        $array[$id]->position = ++$n;
519
520
        // loop over the node's children and process their data
521
        // before assigning the right value
522 69
        $p = 0;
523 69
        foreach ($array[$id]->childrenIds as $childNodeId) {
524 69
            $this->rebuildGeneratePositionData($array, $childNodeId, $p);
525
        }
526
527 69
        usort($array[$id]->childrenIds, function (int $a, int $b) use ($array) {
528 69
            return $array[$a]->position <=> $array[$b]->position;
529 69
        });
530
    }
531
}
532