Passed
Push — master ( 27da09...b75ff6 )
by Einar-Johan
02:43
created

HasNestedSets   F

Complexity

Total Complexity 156

Size/Duplication

Total Lines 1517
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 156
eloc 308
c 1
b 0
f 0
dl 0
loc 1517
rs 2

How to fix   Complexity   

Complex Class

Complex classes like HasNestedSets often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use HasNestedSets, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Encima\Albero;
4
5
use Illuminate\Database\Eloquent\Model;
6
use Illuminate\Database\Eloquent\Builder;
7
use Encima\Albero\Extensions\Eloquent\Collection;
8
use Illuminate\Database\Eloquent\Relations\HasMany;
9
use Illuminate\Database\Eloquent\SoftDeletingScope;
10
use Illuminate\Database\Eloquent\Relations\BelongsTo;
11
use Illuminate\Database\Eloquent\ModelNotFoundException;
12
use Encima\Albero\Extensions\Query\Builder as QueryBuilder;
13
14
/**
15
 * Node
16
 *
17
 * This abstract class implements Nested Set functionality. A Nested Set is a
18
 * smart way to implement an ordered tree with the added benefit that you can
19
 * select all of their descendants with a single query. Drawbacks are that
20
 * insertion or move operations need more complex sql queries.
21
 *
22
 * Nested sets are appropiate when you want either an ordered tree (menus,
23
 * commercial categories, etc.) or an efficient way of querying big trees.
24
 */
25
trait HasNestedSets
26
{
27
    protected string $parentColumn = 'parent_id';
28
29
    protected string $leftColumn = 'left';
30
31
    protected string $rightColumn = 'right';
32
33
    protected string $depthColumn = 'depth';
34
35
    /** @var string|null */
36
    protected $orderColumn = null;
37
38
    /** @var int|string|bool|null */
39
    protected static $moveToNewParentId = null;
40
41
    protected array $scoped = [];
42
43
    /**
44
     * The "booting" method of the model.
45
     *
46
     * We'll use this method to register event listeners on a Node instance as
47
     * suggested in the beta documentation...
48
     *
49
     * TODO:
50
     *
51
     *    - Find a way to avoid needing to declare the called methods "public"
52
     *    as registering the event listeners *inside* this methods does not give
53
     *    us an object context.
54
     *
55
     * Events:
56
     *
57
     *    1. "creating": Before creating a new Node we'll assign a default value
58
     *    for the left and right indexes.
59
     *
60
     *    2. "saving": Before saving, we'll perform a check to see if we have to
61
     *    move to another parent.
62
     *
63
     *    3. "saved": Move to the new parent after saving if needed and re-set
64
     *    depth.
65
     *
66
     *    4. "deleting": Before delete we should prune all children and update
67
     *    the left and right indexes for the remaining nodes.
68
     *
69
     *    5. (optional) "restoring": Before a soft-delete node restore operation,
70
     *    shift its siblings.
71
     *
72
     *    6. (optional) "restore": After having restored a soft-deleted node,
73
     *    restore all of its descendants.
74
     *
75
     * @return void
76
     */
77
    protected static function bootHasNestedSets(): void
78
    {
79
        static::creating(function ($node) {
80
            $node->setDefaultLeftAndRight();
81
        });
82
83
        static::saving(function ($node) {
84
            $node->storeNewParent();
85
        });
86
87
        static::saved(function ($node) {
88
            $node->moveToNewParent();
89
            $node->setDepth();
90
        });
91
92
        static::deleting(function ($node) {
93
            $node->destroyDescendants();
94
        });
95
96
        if (static::softDeletesEnabled()) {
97
            static::restoring(function ($node) {
98
                $node->shiftSiblingsForRestore();
99
            });
100
101
            static::restored(function ($node) {
102
                $node->restoreDescendants();
103
            });
104
        }
105
    }
106
107
    /**
108
     * Get the table associated with the model.
109
     *
110
     * @return string
111
     */
112
    abstract public function getTable();
113
114
    /**
115
     * Get an attribute from the model.
116
     *
117
     * @param  string  $key
118
     * @return mixed
119
     */
120
    abstract public function getAttribute($key);
121
122
    /**
123
     * Set a given attribute on the model.
124
     *
125
     * @param  string  $key
126
     * @param  mixed  $value
127
     * @return mixed
128
     */
129
    abstract public function setAttribute($key, $value);
130
131
    /**
132
     * Determine if the model or any of the given attribute(s) have been modified.
133
     *
134
     * @param  array|string|null  $attributes
135
     * @return bool
136
     */
137
    abstract public function isDirty($attributes = null);
138
139
    /**
140
    * Define an inverse one-to-one or many relationship.
141
    *
142
    * @param  string  $related
143
    * @param  string|null  $foreignKey
144
    * @param  string|null  $ownerKey
145
    * @param  string|null  $relation
146
    * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
147
    */
148
    abstract public function belongsTo($related, $foreignKey = null, $ownerKey = null, $relation = null);
149
150
    /**
151
     * Define a one-to-many relationship.
152
     *
153
     * @param  string  $related
154
     * @param  string|null  $foreignKey
155
     * @param  string|null  $localKey
156
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
157
     */
158
    abstract public function hasMany($related, $foreignKey = null, $localKey = null);
159
160
    /**
161
     * Get a new query builder for the model's table.
162
     *
163
     * @return \Illuminate\Database\Eloquent\Builder
164
     */
165
    abstract public function newQuery();
166
167
    /**
168
     * Get the database connection for the model.
169
     *
170
     * @return \Illuminate\Database\Connection
171
     */
172
    abstract public function getConnection();
173
174
175
    /**
176
     * Get the value of the model's primary key.
177
     *
178
     * @return mixed
179
     */
180
    abstract public function getKey()
181
182
183
    /**
184
     * Get the primary key for the model.
185
     *
186
     * @return string
187
     */
188
    abstract public function getKeyName();
0 ignored issues
show
Bug introduced by
A parse error occurred: Syntax error, unexpected T_ABSTRACT, expecting '{' or ';' on line 188 at column 4
Loading history...
189
190
191
    /**
192
    * Get the parent column name.
193
    *
194
    * @return string
195
    */
196
    public function getParentColumnName(): string
197
    {
198
        return $this->parentColumn;
199
    }
200
201
    /**
202
    * Get the table qualified parent column name.
203
    *
204
    * @return string
205
    */
206
    public function getQualifiedParentColumnName(): string
207
    {
208
        return $this->getTable().'.'.$this->getParentColumnName();
209
    }
210
211
    /**
212
    * Get the value of the models "parent_id" field.
213
    *
214
    * @return int|string
215
    */
216
    public function getParentId()
217
    {
218
        return $this->getAttribute($this->getparentColumnName());
219
    }
220
221
    /**
222
     * Get the "left" field column name.
223
     *
224
     * @return string
225
     */
226
    public function getLeftColumnName(): string
227
    {
228
        return $this->leftColumn;
229
    }
230
231
    /**
232
     * Get the table qualified "left" field column name.
233
     *
234
     * @return string
235
     */
236
    public function getQualifiedLeftColumnName(): string
237
    {
238
        return $this->getTable().'.'.$this->getLeftColumnName();
239
    }
240
241
    /**
242
     * Get the value of the model's "left" field.
243
     *
244
     * @return int
245
     */
246
    public function getLeft(): int
247
    {
248
        return $this->getAttribute($this->getLeftColumnName());
249
    }
250
251
    /**
252
     * Get the "right" field column name.
253
     *
254
     * @return string
255
     */
256
    public function getRightColumnName(): string
257
    {
258
        return $this->rightColumn;
259
    }
260
261
    /**
262
     * Get the table qualified "right" field column name.
263
     *
264
     * @return string
265
     */
266
    public function getQualifiedRightColumnName(): string
267
    {
268
        return $this->getTable().'.'.$this->getRightColumnName();
269
    }
270
271
    /**
272
     * Get the value of the model's "right" field.
273
     *
274
     * @return int
275
     */
276
    public function getRight(): int
277
    {
278
        return $this->getAttribute($this->getRightColumnName());
279
    }
280
281
    /**
282
     * Get the "depth" field column name.
283
     *
284
     * @return string
285
     */
286
    public function getDepthColumnName(): string
287
    {
288
        return $this->depthColumn;
289
    }
290
291
    /**
292
     * Get the table qualified "depth" field column name.
293
     *
294
     * @return string
295
     */
296
    public function getQualifiedDepthColumnName(): string
297
    {
298
        return $this->getTable().'.'.$this->getDepthColumnName();
299
    }
300
301
    /**
302
     * Get the model's "depth" value.
303
     *
304
     * @return int
305
     */
306
    public function getDepth(): ?int
307
    {
308
        return $this->getAttribute($this->getDepthColumnName());
309
    }
310
311
    /**
312
     * Get the "order" field column name.
313
     *
314
     * @return string
315
     */
316
    public function getOrderColumnName(): string
317
    {
318
        return is_null($this->orderColumn) ? $this->getLeftColumnName() : $this->orderColumn;
319
    }
320
321
    /**
322
     * Get the table qualified "order" field column name.
323
     *
324
     * @return string
325
     */
326
    public function getQualifiedOrderColumnName(): string
327
    {
328
        return $this->getTable().'.'.$this->getOrderColumnName();
329
    }
330
331
    /**
332
     * Get the model's "order" value.
333
     *
334
     * @return mixed
335
     */
336
    public function getOrder()
337
    {
338
        return $this->getAttribute($this->getOrderColumnName());
339
    }
340
341
    /**
342
     * Get the column names which define our scope
343
     *
344
     * @return array
345
     */
346
    public function getScopedColumns(): array
347
    {
348
        return (array) $this->scoped;
349
    }
350
351
    /**
352
     * Get the qualified column names which define our scope
353
     *
354
     * @return array
355
     */
356
    public function getQualifiedScopedColumns(): array
357
    {
358
        if (!$this->isScoped()) {
359
            return $this->getScopedColumns();
360
        }
361
362
        $prefix = $this->getTable().'.';
363
364
        return array_map(function ($c) use ($prefix) {
365
            return $prefix.$c;
366
        }, $this->getScopedColumns());
367
    }
368
369
    /**
370
     * Returns wether this particular node instance is scoped by certain fields
371
     * or not.
372
     *
373
     * @return boolean
374
     */
375
    public function isScoped(): bool
376
    {
377
        return !!(count($this->getScopedColumns()) > 0);
378
    }
379
380
    /**
381
    * Parent relation (self-referential) 1-1.
382
    *
383
    * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
384
    */
385
    public function parent(): BelongsTo
386
    {
387
        return $this->belongsTo(get_class($this), $this->getParentColumnName());
388
    }
389
390
    /**
391
    * Children relation (self-referential) 1-N.
392
    *
393
    * @return \Illuminate\Database\Eloquent\Relations\HasMany
394
    */
395
    public function children(): HasMany
396
    {
397
        return $this->hasMany(get_class($this), $this->getParentColumnName())
398
                ->orderBy($this->getOrderColumnName());
399
    }
400
401
    /**
402
     * Get a new "scoped" query builder for the Node's model.
403
     *
404
     * @param  bool  $excludeDeleted
405
     * @return \Illuminate\Database\Eloquent\Builder|static
406
     */
407
    public function newNestedSetQuery(): Builder
408
    {
409
        $builder = $this->newQuery()->orderBy($this->getQualifiedOrderColumnName());
410
411
        if ($this->isScoped()) {
412
            foreach ($this->scoped as $scopeFld) {
413
                $builder->where($scopeFld, '=', $this->{$scopeFld});
414
            }
415
        }
416
417
        return $builder;
418
    }
419
420
    /**
421
     * Overload new Collection
422
     *
423
     * @param array $models
424
     * @return \Encima\Albero\Extensions\Eloquent\Collection
425
     */
426
    public function newCollection(array $models = []): Collection
427
    {
428
        return new Collection($models);
429
    }
430
431
    /**
432
     * Get all of the nodes from the database.
433
     *
434
     * @param  array  $columns
435
     * @return \Encima\Albero\Extensions\Eloquent\Collection|static[]
436
     */
437
    public static function all($columns = ['*']): Collection
438
    {
439
        $instance = new static();
440
441
        return $instance->newQuery()
442
                    ->orderBy($instance->getQualifiedOrderColumnName())
443
                    ->get($columns);
444
    }
445
446
    /**
447
     * Returns the first root node.
448
     *
449
     * @return \Illuminate\Database\Eloquent\Model
450
     */
451
    public static function root(): Model
452
    {
453
        return static::roots()->first();
454
    }
455
456
    /**
457
     * Static query scope. Returns a query scope with all root nodes.
458
     *
459
     * @return \Illuminate\Database\Query\Builder
460
     */
461
    public static function roots(): Builder
462
    {
463
        $instance = new static();
464
465
        return $instance->newQuery()
466
                    ->whereNull($instance->getParentColumnName())
467
                    ->orderBy($instance->getQualifiedOrderColumnName());
468
    }
469
470
    /**
471
     * Static query scope. Returns a query scope with all nodes which are at
472
     * the end of a branch.
473
     *
474
     * @return \Illuminate\Database\Query\Builder
475
     */
476
    public static function allLeaves(): Builder
477
    {
478
        $instance = new static();
479
480
        $grammar = $instance->getConnection()->getQueryGrammar();
481
482
        $rgtCol = $grammar->wrap($instance->getQualifiedRightColumnName());
483
        $lftCol = $grammar->wrap($instance->getQualifiedLeftColumnName());
484
485
        return $instance->newQuery()
486
            ->whereRaw($rgtCol.' - '.$lftCol.' = 1')
487
            ->orderBy($instance->getQualifiedOrderColumnName());
488
    }
489
490
    /**
491
     * Static query scope. Returns a query scope with all nodes which are at
492
     * the middle of a branch (not root and not leaves).
493
     *
494
     * @return \Illuminate\Database\Query\Builder
495
     */
496
    public static function allTrunks(): Builder
497
    {
498
        $instance = new static();
499
500
        $grammar = $instance->getConnection()->getQueryGrammar();
501
502
        $rgtCol = $grammar->wrap($instance->getQualifiedRightColumnName());
503
        $lftCol = $grammar->wrap($instance->getQualifiedLeftColumnName());
504
505
        return $instance->newQuery()
506
            ->whereNotNull($instance->getParentColumnName())
507
            ->whereRaw($rgtCol.' - '.$lftCol.' != 1')
508
            ->orderBy($instance->getQualifiedOrderColumnName());
509
    }
510
511
    /**
512
     * Checks wether the underlying Nested Set structure is valid.
513
     *
514
     * @return boolean
515
     */
516
    public static function isValidNestedSet(): bool
517
    {
518
        $validator = new SetValidator(new static());
519
520
        return $validator->passes();
521
    }
522
523
    /**
524
     * Rebuilds the structure of the current Nested Set.
525
     *
526
     * @param  bool $force
527
     * @return void
528
     */
529
    public static function rebuild($force = false): void
530
    {
531
        $builder = new SetBuilder(new static());
532
533
        $builder->rebuild($force);
534
    }
535
536
    /**
537
     * Maps the provided tree structure into the database.
538
     *
539
     * @param   array|\Illuminate\Support\Contracts\ArrayableInterface
540
     * @return  boolean
541
     */
542
    public static function buildTree($nodeList): bool
543
    {
544
        return (new static())->makeTree($nodeList);
545
    }
546
547
    /**
548
     * Query scope which extracts a certain node object from the current query
549
     * expression.
550
     *
551
     * @return \Illuminate\Database\Query\Builder
552
     */
553
    public function scopeWithoutNode($query, $node): Builder
554
    {
555
        return $query->where($node->getKeyName(), '!=', $node->getKey());
556
    }
557
558
    /**
559
     * Extracts current node (self) from current query expression.
560
     *
561
     * @return \Illuminate\Database\Query\Builder
562
     */
563
    public function scopeWithoutSelf($query): Builder
564
    {
565
        return $this->scopeWithoutNode($query, $this);
566
    }
567
568
    /**
569
     * Extracts first root (from the current node p-o-v) from current query
570
     * expression.
571
     *
572
     * @return \Illuminate\Database\Query\Builder
573
     */
574
    public function scopeWithoutRoot($query): Builder
575
    {
576
        return $this->scopeWithoutNode($query, $this->getRoot());
577
    }
578
579
    /**
580
     * Provides a depth level limit for the query.
581
     *
582
     * @param   query   \Illuminate\Database\Query\Builder
583
     * @param   limit   integer
584
     * @return  \Illuminate\Database\Query\Builder
585
     */
586
    public function scopeLimitDepth($query, $limit): Builder
587
    {
588
        $depth = $this->exists ? $this->getDepth() : $this->getLevel();
589
        $max = $depth + $limit;
590
        $scopes = [$depth, $max];
591
592
        return $query->whereBetween($this->getDepthColumnName(), [min($scopes), max($scopes)]);
593
    }
594
595
    /**
596
     * Returns true if this is a root node.
597
     *
598
     * @return boolean
599
     */
600
    public function isRoot(): bool
601
    {
602
        return is_null($this->getParentId());
603
    }
604
605
    /**
606
     * Returns true if this is a leaf node (end of a branch).
607
     *
608
     * @return boolean
609
     */
610
    public function isLeaf(): bool
611
    {
612
        return $this->exists && ($this->getRight() - $this->getLeft() == 1);
613
    }
614
615
    /**
616
     * Returns true if this is a trunk node (not root or leaf).
617
     *
618
     * @return boolean
619
     */
620
    public function isTrunk(): bool
621
    {
622
        return !$this->isRoot() && !$this->isLeaf();
623
    }
624
625
    /**
626
     * Returns true if this is a child node.
627
     *
628
     * @return boolean
629
     */
630
    public function isChild(): bool
631
    {
632
        return !$this->isRoot();
633
    }
634
635
    /**
636
     * Returns the root node starting at the current node.
637
     *
638
     * @return \Illuminate\Database\Eloquent\Model
639
     */
640
    public function getRoot(): Model
641
    {
642
        if ($this->exists) {
643
            return $this->ancestorsAndSelf()->whereNull($this->getParentColumnName())->first();
644
        }
645
        $parentId = $this->getParentId();
646
647
        if (!is_null($parentId) && $currentParent = static::find($parentId)) {
648
            return $currentParent->getRoot();
649
        }
650
651
        return $this;
652
    }
653
654
    /**
655
     * Instance scope which targes all the ancestor chain nodes including
656
     * the current one.
657
     *
658
     * @return \Illuminate\Database\Eloquent\Builder
659
     */
660
    public function ancestorsAndSelf(): Builder
661
    {
662
        return $this->newNestedSetQuery()
663
                ->where($this->getLeftColumnName(), '<=', $this->getLeft())
664
                ->where($this->getRightColumnName(), '>=', $this->getRight());
665
    }
666
667
    /**
668
     * Get all the ancestor chain from the database including the current node.
669
     *
670
     * @param  array  $columns
671
     * @return \Illuminate\Database\Eloquent\Collection
672
     */
673
    public function getAncestorsAndSelf($columns = ['*']): Collection
674
    {
675
        return $this->ancestorsAndSelf()->get($columns);
676
    }
677
678
    /**
679
     * Get all the ancestor chain from the database including the current node
680
     * but without the root node.
681
     *
682
     * @param  array  $columns
683
     * @return \Illuminate\Database\Eloquent\Collection
684
     */
685
    public function getAncestorsAndSelfWithoutRoot($columns = ['*']): Collection
686
    {
687
        return $this->ancestorsAndSelf()->withoutRoot()->get($columns);
688
    }
689
690
    /**
691
     * Instance scope which targets all the ancestor chain nodes excluding
692
     * the current one.
693
     *
694
     * @return \Illuminate\Database\Eloquent\Builder
695
     */
696
    public function ancestors(): Builder
697
    {
698
        return $this->ancestorsAndSelf()->withoutSelf();
699
    }
700
701
    /**
702
     * Get all the ancestor chain from the database excluding the current node.
703
     *
704
     * @param  array  $columns
705
     * @return \Illuminate\Database\Eloquent\Collection
706
     */
707
    public function getAncestors($columns = ['*']): Collection
708
    {
709
        return $this->ancestors()->get($columns);
710
    }
711
712
    /**
713
     * Get all the ancestor chain from the database excluding the current node
714
     * and the root node (from the current node's perspective).
715
     *
716
     * @param  array  $columns
717
     * @return \Illuminate\Database\Eloquent\Collection
718
     */
719
    public function getAncestorsWithoutRoot($columns = ['*']): Collection
720
    {
721
        return $this->ancestors()->withoutRoot()->get($columns);
722
    }
723
724
    /**
725
     * Instance scope which targets all children of the parent, including self.
726
     *
727
     * @return \Illuminate\Database\Eloquent\Builder
728
     */
729
    public function siblingsAndSelf(): Builder
730
    {
731
        return $this->newNestedSetQuery()
732
                ->where($this->getParentColumnName(), $this->getParentId());
733
    }
734
735
    /**
736
     * Get all children of the parent, including self.
737
     *
738
     * @param  array  $columns
739
     * @return \Illuminate\Database\Eloquent\Collection
740
     */
741
    public function getSiblingsAndSelf($columns = ['*']): Collection
742
    {
743
        return $this->siblingsAndSelf()->get($columns);
744
    }
745
746
    /**
747
     * Instance scope targeting all children of the parent, except self.
748
     *
749
     * @return \Illuminate\Database\Eloquent\Builder
750
     */
751
    public function siblings(): Builder
752
    {
753
        return $this->siblingsAndSelf()->withoutSelf();
754
    }
755
756
    /**
757
     * Return all children of the parent, except self.
758
     *
759
     * @param  array  $columns
760
     * @return \Illuminate\Database\Eloquent\Collection
761
     */
762
    public function getSiblings($columns = ['*']): Collection
763
    {
764
        return $this->siblings()->get($columns);
765
    }
766
767
    /**
768
     * Instance scope targeting all of its nested children which do not have
769
     * children.
770
     *
771
     * @return \Illuminate\Database\Query\Builder
772
     */
773
    public function leaves(): Builder
774
    {
775
        $grammar = $this->getConnection()->getQueryGrammar();
776
777
        $rgtCol = $grammar->wrap($this->getQualifiedRightColumnName());
778
        $lftCol = $grammar->wrap($this->getQualifiedLeftColumnName());
779
780
        return $this->descendants()
781
                ->whereRaw($rgtCol.' - '.$lftCol.' = 1');
782
    }
783
784
    /**
785
     * Return all of its nested children which do not have children.
786
     *
787
     * @param  array  $columns
788
     * @return \Illuminate\Database\Eloquent\Collection
789
     */
790
    public function getLeaves($columns = ['*']): Collection
791
    {
792
        return $this->leaves()->get($columns);
793
    }
794
795
    /**
796
     * Instance scope targeting all of its nested children which are between the
797
     * root and the leaf nodes (middle branch).
798
     *
799
     * @return \Illuminate\Database\Query\Builder
800
     */
801
    public function trunks(): Builder
802
    {
803
        $grammar = $this->getConnection()->getQueryGrammar();
804
805
        $rgtCol = $grammar->wrap($this->getQualifiedRightColumnName());
806
        $lftCol = $grammar->wrap($this->getQualifiedLeftColumnName());
807
808
        return $this->descendants()
809
                ->whereNotNull($this->getQualifiedParentColumnName())
810
                ->whereRaw($rgtCol.' - '.$lftCol.' != 1');
811
    }
812
813
    /**
814
     * Return all of its nested children which are trunks.
815
     *
816
     * @param  array  $columns
817
     * @return \Illuminate\Database\Eloquent\Collection
818
     */
819
    public function getTrunks($columns = ['*']): Collection
820
    {
821
        return $this->trunks()->get($columns);
822
    }
823
824
    /**
825
     * Scope targeting itself and all of its nested children.
826
     *
827
     * @return \Illuminate\Database\Query\Builder
828
     */
829
    public function descendantsAndSelf(): Builder
830
    {
831
        return $this->newNestedSetQuery()
832
                ->where($this->getLeftColumnName(), '>=', $this->getLeft())
833
                ->where($this->getLeftColumnName(), '<', $this->getRight());
834
    }
835
836
    /**
837
     * Retrieve all nested children an self.
838
     *
839
     * @param  array  $columns
840
     * @return \Illuminate\Database\Eloquent\Collection
841
     */
842
    public function getDescendantsAndSelf($columns = ['*']): Collection
843
    {
844
        if (is_array($columns)) {
845
            return $this->descendantsAndSelf()->get($columns);
846
        }
847
848
        $arguments = func_get_args();
849
850
        $limit = intval(array_shift($arguments));
851
        $columns = array_shift($arguments) ?: ['*'];
852
853
        return $this->descendantsAndSelf()->limitDepth($limit)->get($columns);
854
    }
855
856
    /**
857
     * Set of all children & nested children.
858
     *
859
     * @return \Illuminate\Database\Query\Builder
860
     */
861
    public function descendants(): Builder
862
    {
863
        return $this->descendantsAndSelf()->withoutSelf();
864
    }
865
866
    /**
867
     * Retrieve all of its children & nested children.
868
     *
869
     * @param  array  $columns
870
     * @return \Illuminate\Database\Eloquent\Collection
871
     */
872
    public function getDescendants($columns = ['*']): Collection
873
    {
874
        if (is_array($columns)) {
875
            return $this->descendants()->get($columns);
876
        }
877
878
        $arguments = func_get_args();
879
880
        $limit = intval(array_shift($arguments));
881
        $columns = array_shift($arguments) ?: ['*'];
882
883
        return $this->descendants()->limitDepth($limit)->get($columns);
884
    }
885
886
    /**
887
     * Set of "immediate" descendants (aka children), alias for the children relation.
888
     *
889
     * @return \Illuminate\Database\Query\Builder
890
     */
891
    public function immediateDescendants(): Builder
892
    {
893
        return $this->children();
894
    }
895
896
    /**
897
     * Retrive all of its "immediate" descendants.
898
     *
899
     * @param array   $columns
900
     * @return \Illuminate\Database\Eloquent\Collection
901
     */
902
    public function getImmediateDescendants($columns = ['*']): Collection
903
    {
904
        return $this->children()->get($columns);
905
    }
906
907
    /**
908
    * Returns the level of this node in the tree.
909
    * Root level is 0.
910
    *
911
    * @return int
912
    */
913
    public function getLevel(): int
914
    {
915
        if (is_null($this->getParentId())) {
916
            return 0;
917
        }
918
919
        return $this->computeLevel();
920
    }
921
922
    /**
923
     * Returns true if node is a descendant.
924
     *
925
     * @param \Illuminate\Database\Eloquent\Model
926
     * @return boolean
927
     */
928
    public function isDescendantOf($other): bool
929
    {
930
        return (
931
      $this->getLeft() > $other->getLeft() &&
932
      $this->getLeft() < $other->getRight() &&
933
      $this->inSameScope($other)
934
    );
935
    }
936
937
    /**
938
     * Returns true if node is self or a descendant.
939
     *
940
     * @param \Illuminate\Database\Eloquent\Model
941
     * @return boolean
942
     */
943
    public function isSelfOrDescendantOf($other): bool
944
    {
945
        return (
946
      $this->getLeft() >= $other->getLeft() &&
947
      $this->getLeft() < $other->getRight() &&
948
      $this->inSameScope($other)
949
    );
950
    }
951
952
    /**
953
     * Returns true if node is an ancestor.
954
     *
955
     * @param \Illuminate\Database\Eloquent\Model
956
     * @return boolean
957
     */
958
    public function isAncestorOf($other): bool
959
    {
960
        return (
961
      $this->getLeft() < $other->getLeft() &&
962
      $this->getRight() > $other->getLeft() &&
963
      $this->inSameScope($other)
964
    );
965
    }
966
967
    /**
968
     * Returns true if node is self or an ancestor.
969
     *
970
     * @param \Illuminate\Database\Eloquent\Model
971
     * @return boolean
972
     */
973
    public function isSelfOrAncestorOf($other): bool
974
    {
975
        return (
976
      $this->getLeft() <= $other->getLeft() &&
977
      $this->getRight() > $other->getLeft() &&
978
      $this->inSameScope($other)
979
    );
980
    }
981
982
    /**
983
     * Returns the first sibling to the left.
984
     *
985
     * @return \Illuminate\Database\Eloquent\Model
986
     */
987
    public function getLeftSibling(): ?Model
988
    {
989
        return $this->siblings()
990
                ->where($this->getLeftColumnName(), '<', $this->getLeft())
991
                ->orderBy($this->getOrderColumnName(), 'desc')
992
                ->get()
993
                ->last();
994
    }
995
996
    /**
997
     * Returns the first sibling to the right.
998
     *
999
     * @return \Illuminate\Database\Eloquent\Model
1000
     */
1001
    public function getRightSibling(): ?Model
1002
    {
1003
        return $this->siblings()
1004
                ->where($this->getLeftColumnName(), '>', $this->getLeft())
1005
                ->first();
1006
    }
1007
1008
    /**
1009
     * Find the left sibling and move to left of it.
1010
     *
1011
     * @return \Illuminate\Database\Eloquent\Model
1012
     */
1013
    public function moveLeft(): Model
1014
    {
1015
        return $this->moveToLeftOf($this->getLeftSibling());
1016
    }
1017
1018
    /**
1019
     * Find the right sibling and move to the right of it.
1020
     *
1021
     * @return \Illuminate\Database\Eloquent\Model
1022
     */
1023
    public function moveRight(): Model
1024
    {
1025
        return $this->moveToRightOf($this->getRightSibling());
1026
    }
1027
1028
    /**
1029
     * Move to the node to the left of ...
1030
     *
1031
     * @return \Illuminate\Database\Eloquent\Model
1032
     */
1033
    public function moveToLeftOf($node): Model
1034
    {
1035
        return $this->moveTo($node, 'left');
1036
    }
1037
1038
    /**
1039
     * Move to the node to the right of ...
1040
     *
1041
     * @return \Illuminate\Database\Eloquent\Model
1042
     */
1043
    public function moveToRightOf($node): Model
1044
    {
1045
        return $this->moveTo($node, 'right');
1046
    }
1047
1048
    /**
1049
     * Alias for moveToRightOf
1050
     *
1051
     * @return \Illuminate\Database\Eloquent\Model
1052
     */
1053
    public function makeNextSiblingOf($node): Model
1054
    {
1055
        return $this->moveToRightOf($node);
1056
    }
1057
1058
    /**
1059
     * Alias for moveToRightOf
1060
     *
1061
     * @return \Illuminate\Database\Eloquent\Model
1062
     */
1063
    public function makeSiblingOf($node): Model
1064
    {
1065
        return $this->moveToRightOf($node);
1066
    }
1067
1068
    /**
1069
     * Alias for moveToLeftOf
1070
     *
1071
     * @return \Illuminate\Database\Eloquent\Model
1072
     */
1073
    public function makePreviousSiblingOf($node): Model
1074
    {
1075
        return $this->moveToLeftOf($node);
1076
    }
1077
1078
    /**
1079
     * Make the node a child of ...
1080
     *
1081
     * @return \Illuminate\Database\Eloquent\Model
1082
     */
1083
    public function makeChildOf($node): self
1084
    {
1085
        return $this->moveTo($node, 'child');
1086
    }
1087
1088
    /**
1089
     * Make the node the first child of ...
1090
     *
1091
     * @return \Illuminate\Database\Eloquent\Model
1092
     */
1093
    public function makeFirstChildOf($node): Model
1094
    {
1095
        if ($node->children()->count() == 0) {
1096
            return $this->makeChildOf($node);
1097
        }
1098
1099
        return $this->moveToLeftOf($node->children()->first());
1100
    }
1101
1102
    /**
1103
     * Make the node the last child of ...
1104
     *
1105
     * @return \Illuminate\Database\Eloquent\Model
1106
     */
1107
    public function makeLastChildOf($node): Model
1108
    {
1109
        return $this->makeChildOf($node);
1110
    }
1111
1112
    /**
1113
     * Make current node a root node.
1114
     *
1115
     * @return \Illuminate\Database\Eloquent\Model
1116
     */
1117
    public function makeRoot(): Model
1118
    {
1119
        return $this->moveTo($this, 'root');
1120
    }
1121
1122
    /**
1123
     * Equals?
1124
     *
1125
     * @param \Illuminate\Database\Eloquent\Model
1126
     * @return boolean
1127
     */
1128
    public function equals($node): bool
1129
    {
1130
        return ($this == $node);
1131
    }
1132
1133
    /**
1134
     * Checkes if the given node is in the same scope as the current one.
1135
     *
1136
     * @param \Illuminate\Database\Eloquent\Model
1137
     * @return boolean
1138
     */
1139
    public function inSameScope($other): bool
1140
    {
1141
        foreach ($this->getScopedColumns() as $fld) {
1142
            if ($this->{$fld} != $other->{$fld}) {
1143
                return false;
1144
            }
1145
        }
1146
1147
        return true;
1148
    }
1149
1150
    /**
1151
     * Checks wether the given node is a descendant of itself. Basically, whether
1152
     * its in the subtree defined by the left and right indices.
1153
     *
1154
     * @param \Illuminate\Database\Eloquent\Model
1155
     * @return boolean
1156
     */
1157
    public function insideSubtree($node): bool
1158
    {
1159
        return (
1160
            $this->getLeft() >= $node->getLeft() &&
1161
            $this->getLeft() <= $node->getRight() &&
1162
            $this->getRight() >= $node->getLeft() &&
1163
            $this->getRight() <= $node->getRight()
1164
        );
1165
    }
1166
1167
    /**
1168
     * Sets default values for left and right fields.
1169
     *
1170
     * @return void
1171
     */
1172
    public function setDefaultLeftAndRight(): void
1173
    {
1174
        $withHighestRight = $this->newNestedSetQuery()->reOrderBy($this->getRightColumnName(), 'desc')->take(1)->sharedLock()->first();
1175
1176
        $maxRgt = 0;
1177
        if (!is_null($withHighestRight)) {
1178
            $maxRgt = $withHighestRight->getRight();
1179
        }
1180
1181
        $this->setAttribute($this->getLeftColumnName(), $maxRgt + 1);
1182
        $this->setAttribute($this->getRightColumnName(), $maxRgt + 2);
1183
    }
1184
1185
    /**
1186
     * Store the parent_id if the attribute is modified so as we are able to move
1187
     * the node to this new parent after saving.
1188
     *
1189
     * @return void
1190
     */
1191
    public function storeNewParent(): void
1192
    {
1193
        if ($this->isDirty($this->getParentColumnName()) && ($this->exists || !$this->isRoot())) {
1194
            static::$moveToNewParentId = $this->getParentId();
1195
        } else {
1196
            static::$moveToNewParentId = false;
1197
        }
1198
    }
1199
1200
    /**
1201
     * Move to the new parent if appropiate.
1202
     *
1203
     * @return void
1204
     */
1205
    public function moveToNewParent(): void
1206
    {
1207
        $pid = static::$moveToNewParentId;
1208
1209
        if (is_null($pid)) {
1210
            $this->makeRoot();
1211
        } elseif ($pid !== false) {
1212
            $this->makeChildOf($pid);
1213
        }
1214
    }
1215
1216
    /**
1217
     * Sets the depth attribute
1218
     *
1219
     * @return \Illuminate\Database\Eloquent\Model
1220
     */
1221
    public function setDepth(): Model
1222
    {
1223
        $self = $this;
1224
1225
        $this->getConnection()->transaction(function () use ($self) {
1226
            $self->reload();
1227
1228
            $level = $self->getLevel();
1229
1230
            $self->newNestedSetQuery()->where($self->getKeyName(), '=', $self->getKey())->update([$self->getDepthColumnName() => $level]);
1231
            $self->setAttribute($self->getDepthColumnName(), $level);
1232
        });
1233
1234
        return $this;
1235
    }
1236
1237
    /**
1238
     * Sets the depth attribute for the current node and all of its descendants.
1239
     *
1240
     * @return \Illuminate\Database\Eloquent\Model
1241
     */
1242
    public function setDepthWithSubtree(): Model
1243
    {
1244
        $self = $this;
1245
1246
        $this->getConnection()->transaction(function () use ($self) {
1247
            $self->reload();
1248
1249
            $self->descendantsAndSelf()->select($self->getKeyName())->lockForUpdate()->get();
1250
1251
            $oldDepth = !is_null($self->getDepth()) ? $self->getDepth() : 0;
1252
1253
            $newDepth = $self->getLevel();
1254
1255
            $self->newNestedSetQuery()->where($self->getKeyName(), '=', $self->getKey())->update([$self->getDepthColumnName() => $newDepth]);
1256
            $self->setAttribute($self->getDepthColumnName(), $newDepth);
1257
1258
            $diff = $newDepth - $oldDepth;
1259
            if (!$self->isLeaf() && $diff != 0) {
1260
                $self->descendants()->increment($self->getDepthColumnName(), $diff);
1261
            }
1262
        });
1263
1264
        return $this;
1265
    }
1266
1267
    /**
1268
     * Prunes a branch off the tree, shifting all the elements on the right
1269
     * back to the left so the counts work.
1270
     *
1271
     * @return void;
1272
     */
1273
    public function destroyDescendants(): void
1274
    {
1275
        if (is_null($this->getRight()) || is_null($this->getLeft())) {
1276
            return;
1277
        }
1278
1279
        $self = $this;
1280
1281
        $this->getConnection()->transaction(function () use ($self) {
1282
            $self->reload();
1283
1284
            $lftCol = $self->getLeftColumnName();
1285
            $rgtCol = $self->getRightColumnName();
1286
            $lft = $self->getLeft();
1287
            $rgt = $self->getRight();
1288
1289
            // Apply a lock to the rows which fall past the deletion point
1290
            $self->newNestedSetQuery()->where($lftCol, '>=', $lft)->select($self->getKeyName())->lockForUpdate()->get();
1291
1292
            // Prune children
1293
            $self->newNestedSetQuery()->where($lftCol, '>', $lft)->where($rgtCol, '<', $rgt)->delete();
1294
1295
            // Update left and right indexes for the remaining nodes
1296
            $diff = $rgt - $lft + 1;
1297
1298
            $self->newNestedSetQuery()->where($lftCol, '>', $rgt)->decrement($lftCol, $diff);
1299
            $self->newNestedSetQuery()->where($rgtCol, '>', $rgt)->decrement($rgtCol, $diff);
1300
        });
1301
    }
1302
1303
    /**
1304
     * "Makes room" for the the current node between its siblings.
1305
     *
1306
     * @return void
1307
     */
1308
    public function shiftSiblingsForRestore(): void
1309
    {
1310
        if (is_null($this->getRight()) || is_null($this->getLeft())) {
1311
            return;
1312
        }
1313
1314
        $self = $this;
1315
1316
        $this->getConnection()->transaction(function () use ($self) {
1317
            $lftCol = $self->getLeftColumnName();
1318
            $rgtCol = $self->getRightColumnName();
1319
            $lft = $self->getLeft();
1320
            $rgt = $self->getRight();
1321
1322
            $diff = $rgt - $lft + 1;
1323
1324
            $self->newNestedSetQuery()->where($lftCol, '>=', $lft)->increment($lftCol, $diff);
1325
            $self->newNestedSetQuery()->where($rgtCol, '>=', $lft)->increment($rgtCol, $diff);
1326
        });
1327
    }
1328
1329
    /**
1330
     * Restores all of the current node's descendants.
1331
     *
1332
     * @return void
1333
     */
1334
    public function restoreDescendants(): void
1335
    {
1336
        if (is_null($this->getRight()) || is_null($this->getLeft())) {
1337
            return;
1338
        }
1339
1340
        $self = $this;
1341
1342
        $this->getConnection()->transaction(function () use ($self) {
1343
            $self->newNestedSetQuery()
1344
        ->withTrashed()
1345
        ->where($self->getLeftColumnName(), '>', $self->getLeft())
1346
        ->where($self->getRightColumnName(), '<', $self->getRight())
1347
        ->update([
1348
          $self->getDeletedAtColumn() => null,
1349
          $self->getUpdatedAtColumn() => $self->{$self->getUpdatedAtColumn()},
1350
        ]);
1351
        });
1352
    }
1353
1354
    /**
1355
     * Return an key-value array indicating the node's depth with $seperator
1356
     *
1357
     * @return Array
1358
     */
1359
    public static function getNestedList($column, $key = null, $seperator = ' '): array
1360
    {
1361
        $instance = new static();
1362
1363
        $key = $key ?: $instance->getKeyName();
1364
        $depthColumn = $instance->getDepthColumnName();
1365
1366
        $nodes = $instance->newNestedSetQuery()->get()->toArray();
1367
1368
        return array_combine(array_map(function ($node) use ($key) {
1369
            return $node[$key];
1370
        }, $nodes), array_map(function ($node) use ($seperator, $depthColumn, $column) {
1371
            return str_repeat($seperator, $node[$depthColumn]).$node[$column];
1372
        }, $nodes));
1373
    }
1374
1375
    /**
1376
     * Maps the provided tree structure into the database using the current node
1377
     * as the parent. The provided tree structure will be inserted/updated as the
1378
     * descendancy subtree of the current node instance.
1379
     *
1380
     * @param   array|\Illuminate\Support\Contracts\ArrayableInterface
1381
     * @return  boolean
1382
     */
1383
    public function makeTree($nodeList): bool
1384
    {
1385
        $mapper = new SetMapper($this);
1386
1387
        return $mapper->map($nodeList);
1388
    }
1389
1390
    /**
1391
     * Main move method. Here we handle all node movements with the corresponding
1392
     * lft/rgt index updates.
1393
     *
1394
     * @param \Illuminate\Database\Eloquent\Model|int $target
1395
     * @param string        $position
1396
     * @return \Illuminate\Database\Eloquent\Model
1397
     */
1398
    protected function moveTo($target, $position): self
1399
    {
1400
        return Move::to($this, $target, $position);
1401
    }
1402
1403
    /**
1404
     * Compute current node level. If could not move past ourseleves return
1405
     * our ancestor count, otherwhise get the first parent level + the computed
1406
     * nesting.
1407
     *
1408
     * @return integer
1409
     */
1410
    protected function computeLevel(): int
1411
    {
1412
        list($node, $nesting) = $this->determineDepth($this);
1413
1414
        if ($node->equals($this)) {
1415
            return $this->ancestors()->count();
1416
        }
1417
1418
        return $node->getLevel() + $nesting;
1419
    }
1420
1421
    /**
1422
     * Return an array with the last node we could reach and its nesting level
1423
     *
1424
     * @param   Baum\Node $node
1425
     * @param   integer   $nesting
1426
     * @return  array
1427
     */
1428
    protected function determineDepth($node, $nesting = 0): array
1429
    {
1430
        // Traverse back up the ancestry chain and add to the nesting level count
1431
        while ($parent = $node->parent()->first()) {
1432
            $nesting = $nesting + 1;
1433
1434
            $node = $parent;
1435
        }
1436
1437
        return [$node, $nesting];
1438
    }
1439
1440
    /**
1441
     * Reloads the model from the database.
1442
     *
1443
     * @return \Illuminate\Database\Eloquent\Model
1444
     *
1445
     * @throws ModelNotFoundException
1446
     */
1447
    public function reload(): Model
1448
    {
1449
        if ($this->exists || ($this->areSoftDeletesEnabled() && $this->trashed())) {
1450
            $fresh = $this->getFreshInstance();
1451
1452
            if (is_null($fresh)) {
1453
                throw (new ModelNotFoundException())->setModel(get_called_class());
1454
            }
1455
1456
            $this->setRawAttributes($fresh->getAttributes(), true);
1457
1458
            $this->setRelations($fresh->getRelations());
1459
1460
            $this->exists = $fresh->exists;
1461
        } else {
1462
            // Revert changes if model is not persisted
1463
            $this->attributes = $this->original;
1464
        }
1465
1466
1467
        return $this;
1468
    }
1469
1470
    /**
1471
     * Get the observable event names.
1472
     *
1473
     * @return array
1474
     */
1475
    public function getObservableEvents(): array
1476
    {
1477
        return array_merge(['moving', 'moved'], parent::getObservableEvents());
1478
    }
1479
1480
    /**
1481
     * Register a moving model event with the dispatcher.
1482
     *
1483
     * @param  Closure|string  $callback
1484
     * @return void
1485
     */
1486
    public static function moving($callback, $priority = 0): void
1487
    {
1488
        static::registerModelEvent('moving', $callback, $priority);
1489
    }
1490
1491
    /**
1492
     * Register a moved model event with the dispatcher.
1493
     *
1494
     * @param  Closure|string  $callback
1495
     * @return void
1496
     */
1497
    public static function moved($callback, $priority = 0): void
1498
    {
1499
        static::registerModelEvent('moved', $callback, $priority);
1500
    }
1501
1502
    /**
1503
     * Get a new query builder instance for the connection.
1504
     *
1505
     * @return \Encima\Albero\Extensions\Query\Builder
1506
     */
1507
    protected function newBaseQueryBuilder(): QueryBuilder
1508
    {
1509
        $conn = $this->getConnection();
1510
1511
        $grammar = $conn->getQueryGrammar();
1512
1513
        return new QueryBuilder($conn, $grammar, $conn->getPostProcessor());
1514
    }
1515
1516
    /**
1517
     * Returns a fresh instance from the database.
1518
     *
1519
     * @return \Illuminate\Database\Eloquent\Model
1520
     */
1521
    protected function getFreshInstance(): ?Model
1522
    {
1523
        if ($this->areSoftDeletesEnabled()) {
1524
            return static::withTrashed()->find($this->getKey());
1525
        }
1526
1527
        return static::find($this->getKey());
1528
    }
1529
1530
    /**
1531
     * Returns wether soft delete functionality is enabled on the model or not.
1532
     *
1533
     * @return boolean
1534
     */
1535
    public function areSoftDeletesEnabled(): bool
1536
    {
1537
        // To determine if there's a global soft delete scope defined we must
1538
        // first determine if there are any, to workaround a non-existent key error.
1539
        $globalScopes = $this->getGlobalScopes();
1540
1541
        if (count($globalScopes) === 0) {
1542
            return false;
1543
        }
1544
1545
        // Now that we're sure that the calling class has some kind of global scope
1546
        // we check for the SoftDeletingScope existance
1547
        return static::hasGlobalScope(new SoftDeletingScope());
1548
    }
1549
1550
    /**
1551
     * Static method which returns wether soft delete functionality is enabled
1552
     * on the model.
1553
     *
1554
     * @return boolean
1555
     */
1556
    public static function softDeletesEnabled(): bool
1557
    {
1558
        return (new static())->areSoftDeletesEnabled();
1559
    }
1560
}
1561