1 <?php
2
3 namespace Kalnoy\Nestedset;
4
5 use Exception;
6 use Illuminate\Database\Eloquent\Collection as EloquentCollection;
7 use Illuminate\Database\Eloquent\Model;
8 use Illuminate\Database\Eloquent\Relations\BelongsTo;
9 use Illuminate\Database\Eloquent\Relations\HasMany;
10 use Illuminate\Support\Arr;
11 use LogicException;
12
13 trait NodeTrait
14 {
15 /**
16 * Pending operation.
17 *
18 * @var array
19 */
20 protected $pending;
21
22 /**
23 * Whether the node has moved since last save.
24 *
25 * @var bool
26 */
27 protected $moved = false;
28
29 /**
30 * @var \Carbon\Carbon
31 */
32 public static $deletedAt;
33
34 /**
35 * Keep track of the number of performed operations.
36 *
37 * @var int
38 */
39 public static $actionsPerformed = 0;
40
41 /**
42 * Sign on model events.
43 */
44 public static function bootNodeTrait()
45 {
46 static::saving(function ($model) {
47 return $model->callPendingAction();
48 });
49
50 static::deleting(function ($model) {
51 // We will need fresh data to delete node safely
52 $model->refreshNode();
53 });
54
55 static::deleted(function ($model) {
56 $model->deleteDescendants();
57 });
58
59 if (static::usesSoftDelete()) {
60 static::restoring(function ($model) {
61 static::$deletedAt = $model->{$model->getDeletedAtColumn()};
62 });
63
64 static::restored(function ($model) {
65 $model->restoreDescendants(static::$deletedAt);
66 });
67 }
68 }
69
70 /**
71 * Set an action.
72 *
73 * @param string $action
74 *
75 * @return $this
76 */
77 protected function setNodeAction($action)
78 {
79 $this->pending = func_get_args();
80
81 return $this;
82 }
83
84 /**
85 * Call pending action.
86 */
87 protected function callPendingAction()
88 {
89 $this->moved = false;
90
91 if ( ! $this->pending && ! $this->exists) {
92 $this->makeRoot();
93 }
94
95 if ( ! $this->pending) return;
96
97 $method = 'action'.ucfirst(array_shift($this->pending));
98 $parameters = $this->pending;
99
100 $this->pending = null;
101
102 $this->moved = call_user_func_array([ $this, $method ], $parameters);
103 }
104
105 /**
106 * @return bool
107 */
108 public static function usesSoftDelete()
109 {
110 static $softDelete;
111
112 if (is_null($softDelete)) {
113 $instance = new static;
114
115 return $softDelete = method_exists($instance, 'bootSoftDeletes');
116 }
117
118 return $softDelete;
119 }
120
121 /**
122 * @return bool
123 */
124 protected function actionRaw()
125 {
126 return true;
127 }
128
129 /**
130 * Make a root node.
131 */
132 protected function actionRoot()
133 {
134 // Simplest case that do not affect other nodes.
135 if ( ! $this->exists) {
136 $cut = $this->getLowerBound() + 1;
137
138 $this->setLft($cut);
139 $this->setRgt($cut + 1);
140
141 return true;
142 }
143
144 return $this->insertAt($this->getLowerBound() + 1);
145 }
146
147 /**
148 * Get the lower bound.
149 *
150 * @return int
151 */
152 protected function getLowerBound()
153 {
154 return (int)$this->newNestedSetQuery()->max($this->getRgtName());
155 }
156
157 /**
158 * Append or prepend a node to the parent.
159 *
160 * @param self $parent
161 * @param bool $prepend
162 *
163 * @return bool
164 */
165 protected function actionAppendOrPrepend(self $parent, $prepend = false)
166 {
167 $parent->refreshNode();
168
169 $cut = $prepend ? $parent->getLft() + 1 : $parent->getRgt();
170
171 if ( ! $this->insertAt($cut)) {
172 return false;
173 }
174
175 $parent->refreshNode();
176
177 return true;
178 }
179
180 /**
181 * Apply parent model.
182 *
183 * @param Model|null $value
184 *
185 * @return $this
186 */
187 protected function setParent($value)
188 {
189 $this->setParentId($value ? $value->getKey() : null)
190 ->setRelation('parent', $value);
191
192 return $this;
193 }
194
195 /**
196 * Insert node before or after another node.
197 *
198 * @param self $node
199 * @param bool $after
200 *
201 * @return bool
202 */
203 protected function actionBeforeOrAfter(self $node, $after = false)
204 {
205 $node->refreshNode();
206
207 return $this->insertAt($after ? $node->getRgt() + 1 : $node->getLft());
208 }
209
210 /**
211 * Refresh node's crucial attributes.
212 */
213 public function refreshNode()
214 {
215 if ( ! $this->exists || static::$actionsPerformed === 0) return;
216
217 $attributes = $this->newNestedSetQuery()->getNodeData($this->getKey());
218
219 $this->attributes = array_merge($this->attributes, $attributes);
220 // $this->original = array_merge($this->original, $attributes);
221 }
222
223 /**
224 * Relation to the parent.
225 *
226 * @return BelongsTo
227 */
228 public function parent()
229 {
230 return $this->belongsTo(get_class($this), $this->getParentIdName())
231 ->setModel($this);
232 }
233
234 /**
235 * Relation to children.
236 *
237 * @return HasMany
238 */
239 public function children()
240 {
241 return $this->hasMany(get_class($this), $this->getParentIdName())
242 ->setModel($this);
243 }
244
245 /**
246 * Get query for descendants of the node.
247 *
248 * @return DescendantsRelation
249 */
250 public function descendants()
251 {
252 return new DescendantsRelation($this->newQuery(), $this);
253 }
254
255 /**
256 * Get query for siblings of the node.
257 *
258 * @return QueryBuilder
259 */
260 public function siblings()
261 {
262 return $this->newScopedQuery()
263 ->where($this->getKeyName(), '<>', $this->getKey())
264 ->where($this->getParentIdName(), '=', $this->getParentId());
265 }
266
267 /**
268 * Get the node siblings and the node itself.
269 *
270 * @return \Kalnoy\Nestedset\QueryBuilder
271 */
272 public function siblingsAndSelf()
273 {
274 return $this->newScopedQuery()
275 ->where($this->getParentIdName(), '=', $this->getParentId());
276 }
277
278 /**
279 * Get query for the node siblings and the node itself.
280 *
281 * @param array $columns
282 *
283 * @return \Illuminate\Database\Eloquent\Collection
284 */
285 public function getSiblingsAndSelf(array $columns = [ '*' ])
286 {
287 return $this->siblingsAndSelf()->get($columns);
288 }
289
290 /**
291 * Get query for siblings after the node.
292 *
293 * @return QueryBuilder
294 */
295 public function nextSiblings()
296 {
297 return $this->nextNodes()
298 ->where($this->getParentIdName(), '=', $this->getParentId());
299 }
300
301 /**
302 * Get query for siblings before the node.
303 *
304 * @return QueryBuilder
305 */
306 public function prevSiblings()
307 {
308 return $this->prevNodes()
309 ->where($this->getParentIdName(), '=', $this->getParentId());
310 }
311
312 /**
313 * Get query for nodes after current node.
314 *
315 * @return QueryBuilder
316 */
317 public function nextNodes()
318 {
319 return $this->newScopedQuery()
320 ->where($this->getLftName(), '>', $this->getLft());
321 }
322
323 /**
324 * Get query for nodes before current node in reversed order.
325 *
326 * @return QueryBuilder
327 */
328 public function prevNodes()
329 {
330 return $this->newScopedQuery()
331 ->where($this->getLftName(), '<', $this->getLft());
332 }
333
334 /**
335 * Get query ancestors of the node.
336 *
337 * @return AncestorsRelation
338 */
339 public function ancestors()
340 {
341 return new AncestorsRelation($this->newQuery(), $this);
342 }
343
344 /**
345 * Make this node a root node.
346 *
347 * @return $this
348 */
349 public function makeRoot()
350 {
351 $this->setParent(null)->dirtyBounds();
352
353 return $this->setNodeAction('root');
354 }
355
356 /**
357 * Save node as root.
358 *
359 * @return bool
360 */
361 public function saveAsRoot()
362 {
363 if ($this->exists && $this->isRoot()) {
364 return $this->save();
365 }
366
367 return $this->makeRoot()->save();
368 }
369
370 /**
371 * Append and save a node.
372 *
373 * @param self $node
374 *
375 * @return bool
376 */
377 public function appendNode(self $node)
378 {
379 return $node->appendToNode($this)->save();
380 }
381
382 /**
383 * Prepend and save a node.
384 *
385 * @param self $node
386 *
387 * @return bool
388 */
389 public function prependNode(self $node)
390 {
391 return $node->prependToNode($this)->save();
392 }
393
394 /**
395 * Append a node to the new parent.
396 *
397 * @param self $parent
398 *
399 * @return $this
400 */
401 public function appendToNode(self $parent)
402 {
403 return $this->appendOrPrependTo($parent);
404 }
405
406 /**
407 * Prepend a node to the new parent.
408 *
409 * @param self $parent
410 *
411 * @return $this
412 */
413 public function prependToNode(self $parent)
414 {
415 return $this->appendOrPrependTo($parent, true);
416 }
417
418 /**
419 * @param self $parent
420 * @param bool $prepend
421 *
422 * @return self
423 */
424 public function appendOrPrependTo(self $parent, $prepend = false)
425 {
426 $this->assertNodeExists($parent)
427 ->assertNotDescendant($parent)
428 ->assertSameScope($parent);
429
430 $this->setParent($parent)->dirtyBounds();
431
432 return $this->setNodeAction('appendOrPrepend', $parent, $prepend);
433 }
434
435 /**
436 * Insert self after a node.
437 *
438 * @param self $node
439 *
440 * @return $this
441 */
442 public function afterNode(self $node)
443 {
444 return $this->beforeOrAfterNode($node, true);
445 }
446
447 /**
448 * Insert self before node.
449 *
450 * @param self $node
451 *
452 * @return $this
453 */
454 public function beforeNode(self $node)
455 {
456 return $this->beforeOrAfterNode($node);
457 }
458
459 /**
460 * @param self $node
461 * @param bool $after
462 *
463 * @return self
464 */
465 public function beforeOrAfterNode(self $node, $after = false)
466 {
467 $this->assertNodeExists($node)
468 ->assertNotDescendant($node)
469 ->assertSameScope($node);
470
471 if ( ! $this->isSiblingOf($node)) {
472 $this->setParent($node->getRelationValue('parent'));
473 }
474
475 $this->dirtyBounds();
476
477 return $this->setNodeAction('beforeOrAfter', $node, $after);
478 }
479
480 /**
481 * Insert self after a node and save.
482 *
483 * @param self $node
484 *
485 * @return bool
486 */
487 public function insertAfterNode(self $node)
488 {
489 return $this->afterNode($node)->save();
490 }
491
492 /**
493 * Insert self before a node and save.
494 *
495 * @param self $node
496 *
497 * @return bool
498 */
499 public function insertBeforeNode(self $node)
500 {
501 if ( ! $this->beforeNode($node)->save()) return false;
502
503 // We'll update the target node since it will be moved
504 $node->refreshNode();
505
506 return true;
507 }
508
509 /**
510 * @param $lft
511 * @param $rgt
512 * @param $parentId
513 *
514 * @return $this
515 */
516 public function rawNode($lft, $rgt, $parentId)
517 {
518 $this->setLft($lft)->setRgt($rgt)->setParentId($parentId);
519
520 return $this->setNodeAction('raw');
521 }
522
523 /**
524 * Move node up given amount of positions.
525 *
526 * @param int $amount
527 *
528 * @return bool
529 */
530 public function up($amount = 1)
531 {
532 $sibling = $this->prevSiblings()
533 ->defaultOrder('desc')
534 ->skip($amount - 1)
535 ->first();
536
537 if ( ! $sibling) return false;
538
539 return $this->insertBeforeNode($sibling);
540 }
541
542 /**
543 * Move node down given amount of positions.
544 *
545 * @param int $amount
546 *
547 * @return bool
548 */
549 public function down($amount = 1)
550 {
551 $sibling = $this->nextSiblings()
552 ->defaultOrder()
553 ->skip($amount - 1)
554 ->first();
555
556 if ( ! $sibling) return false;
557
558 return $this->insertAfterNode($sibling);
559 }
560
561 /**
562 * Insert node at specific position.
563 *
564 * @param int $position
565 *
566 * @return bool
567 */
568 protected function insertAt($position)
569 {
570 ++static::$actionsPerformed;
571
572 $result = $this->exists
573 ? $this->moveNode($position)
574 : $this->insertNode($position);
575
576 return $result;
577 }
578
579 /**
580 * Move a node to the new position.
581 *
582 * @since 2.0
583 *
584 * @param int $position
585 *
586 * @return int
587 */
588 protected function moveNode($position)
589 {
590 $updated = $this->newNestedSetQuery()
591 ->moveNode($this->getKey(), $position) > 0;
592
593 if ($updated) $this->refreshNode();
594
595 return $updated;
596 }
597
598 /**
599 * Insert new node at specified position.
600 *
601 * @since 2.0
602 *
603 * @param int $position
604 *
605 * @return bool
606 */
607 protected function insertNode($position)
608 {
609 $this->newNestedSetQuery()->makeGap($position, 2);
610
611 $height = $this->getNodeHeight();
612
613 $this->setLft($position);
614 $this->setRgt($position + $height - 1);
615
616 return true;
617 }
618
619 /**
620 * Update the tree when the node is removed physically.
621 */
622 protected function deleteDescendants()
623 {
624 $lft = $this->getLft();
625 $rgt = $this->getRgt();
626
627 $method = $this->usesSoftDelete() && $this->forceDeleting
628 ? 'forceDelete'
629 : 'delete';
630
631 $this->descendants()->{$method}();
632
633 if ($this->hardDeleting()) {
634 $height = $rgt - $lft + 1;
635
636 $this->newNestedSetQuery()->makeGap($rgt + 1, -$height);
637
638 // In case if user wants to re-create the node
639 $this->makeRoot();
640
641 static::$actionsPerformed++;
642 }
643 }
644
645 /**
646 * Restore the descendants.
647 *
648 * @param $deletedAt
649 */
650 protected function restoreDescendants($deletedAt)
651 {
652 $this->descendants()
653 ->where($this->getDeletedAtColumn(), '>=', $deletedAt)
654 ->restore();
655 }
656
657 /**
658 * {@inheritdoc}
659 *
660 * @since 2.0
661 */
662 public function newEloquentBuilder($query)
663 {
664 return new QueryBuilder($query);
665 }
666
667 /**
668 * Get a new base query that includes deleted nodes.
669 *
670 * @since 1.1
671 *
672 * @return QueryBuilder
673 */
674 public function newNestedSetQuery($table = null)
675 {
676 $builder = $this->usesSoftDelete()
677 ? $this->withTrashed()
678 : $this->newQuery();
679
680 return $this->applyNestedSetScope($builder, $table);
681 }
682
683 /**
684 * @param string $table
685 *
686 * @return QueryBuilder
687 */
688 public function newScopedQuery($table = null)
689 {
690 return $this->applyNestedSetScope($this->newQuery(), $table);
691 }
692
693 /**
694 * @param mixed $query
695 * @param string $table
696 *
697 * @return mixed
698 */
699 public function applyNestedSetScope($query, $table = null)
700 {
701 if ( ! $scoped = $this->getScopeAttributes()) {
702 return $query;
703 }
704
705 if ( ! $table) {
706 $table = $this->getTable();
707 }
708
709 foreach ($scoped as $attribute) {
710 $query->where($table.'.'.$attribute, '=',
711 $this->getAttributeValue($attribute));
712 }
713
714 return $query;
715 }
716
717 /**
718 * @return array
719 */
720 protected function getScopeAttributes()
721 {
722 return null;
723 }
724
725 /**
726 * @param array $attributes
727 *
728 * @return QueryBuilder
729 */
730 public static function scoped(array $attributes)
731 {
732 $instance = new static;
733
734 $instance->setRawAttributes($attributes);
735
736 return $instance->newScopedQuery();
737 }
738
739 /**
740 * {@inheritdoc}
741 */
742 public function newCollection(array $models = array())
743 {
744 return new Collection($models);
745 }
746
747 /**
748 * {@inheritdoc}
749 *
750 * Use `children` key on `$attributes` to create child nodes.
751 *
752 * @param self $parent
753 */
754 public static function create(array $attributes = [], self $parent = null)
755 {
756 $children = Arr::pull($attributes, 'children');
757
758 $instance = new static($attributes);
759
760 if ($parent) {
761 $instance->appendToNode($parent);
762 }
763
764 $instance->save();
765
766 // Now create children
767 $relation = new EloquentCollection;
768
769 foreach ((array)$children as $child) {
770 $relation->add($child = static::create($child, $instance));
771
772 $child->setRelation('parent', $instance);
773 }
774
775 $instance->refreshNode();
776
777 return $instance->setRelation('children', $relation);
778 }
779
780 /**
781 * Get node height (rgt - lft + 1).
782 *
783 * @return int
784 */
785 public function getNodeHeight()
786 {
787 if ( ! $this->exists) return 2;
788
789 return $this->getRgt() - $this->getLft() + 1;
790 }
791
792 /**
793 * Get number of descendant nodes.
794 *
795 * @return int
796 */
797 public function getDescendantCount()
798 {
799 return ceil($this->getNodeHeight() / 2) - 1;
800 }
801
802 /**
803 * Set the value of model's parent id key.
804 *
805 * Behind the scenes node is appended to found parent node.
806 *
807 * @param int $value
808 *
809 * @throws Exception If parent node doesn't exists
810 */
811 public function setParentIdAttribute($value)
812 {
813 if ($this->getParentId() == $value) return;
814
815 if ($value) {
816 $this->appendToNode($this->newScopedQuery()->findOrFail($value));
817 } else {
818 $this->makeRoot();
819 }
820 }
821
822 /**
823 * Get whether node is root.
824 *
825 * @return boolean
826 */
827 public function isRoot()
828 {
829 return is_null($this->getParentId());
830 }
831
832 /**
833 * @return bool
834 */
835 public function isLeaf()
836 {
837 return $this->getLft() + 1 == $this->getRgt();
838 }
839
840 /**
841 * Get the lft key name.
842 *
843 * @return string
844 */
845 public function getLftName()
846 {
847 return NestedSet::LFT;
848 }
849
850 /**
851 * Get the rgt key name.
852 *
853 * @return string
854 */
855 public function getRgtName()
856 {
857 return NestedSet::RGT;
858 }
859
860 /**
861 * Get the parent id key name.
862 *
863 * @return string
864 */
865 public function getParentIdName()
866 {
867 return NestedSet::PARENT_ID;
868 }
869
870 /**
871 * Get the value of the model's lft key.
872 *
873 * @return integer
874 */
875 public function getLft()
876 {
877 return $this->getAttributeValue($this->getLftName());
878 }
879
880 /**
881 * Get the value of the model's rgt key.
882 *
883 * @return integer
884 */
885 public function getRgt()
886 {
887 return $this->getAttributeValue($this->getRgtName());
888 }
889
890 /**
891 * Get the value of the model's parent id key.
892 *
893 * @return integer
894 */
895 public function getParentId()
896 {
897 return $this->getAttributeValue($this->getParentIdName());
898 }
899
900 /**
901 * Returns node that is next to current node without constraining to siblings.
902 *
903 * This can be either a next sibling or a next sibling of the parent node.
904 *
905 * @param array $columns
906 *
907 * @return self
908 */
909 public function getNextNode(array $columns = [ '*' ])
910 {
911 return $this->nextNodes()->defaultOrder()->first($columns);
912 }
913
914 /**
915 * Returns node that is before current node without constraining to siblings.
916 *
917 * This can be either a prev sibling or parent node.
918 *
919 * @param array $columns
920 *
921 * @return self
922 */
923 public function getPrevNode(array $columns = [ '*' ])
924 {
925 return $this->prevNodes()->defaultOrder('desc')->first($columns);
926 }
927
928 /**
929 * @param array $columns
930 *
931 * @return Collection
932 */
933 public function getAncestors(array $columns = [ '*' ])
934 {
935 return $this->ancestors()->get($columns);
936 }
937
938 /**
939 * @param array $columns
940 *
941 * @return Collection|self[]
942 */
943 public function getDescendants(array $columns = [ '*' ])
944 {
945 return $this->descendants()->get($columns);
946 }
947
948 /**
949 * @param array $columns
950 *
951 * @return Collection|self[]
952 */
953 public function getSiblings(array $columns = [ '*' ])
954 {
955 return $this->siblings()->get($columns);
956 }
957
958 /**
959 * @param array $columns
960 *
961 * @return Collection|self[]
962 */
963 public function getNextSiblings(array $columns = [ '*' ])
964 {
965 return $this->nextSiblings()->get($columns);
966 }
967
968 /**
969 * @param array $columns
970 *
971 * @return Collection|self[]
972 */
973 public function getPrevSiblings(array $columns = [ '*' ])
974 {
975 return $this->prevSiblings()->get($columns);
976 }
977
978 /**
979 * @param array $columns
980 *
981 * @return self
982 */
983 public function getNextSibling(array $columns = [ '*' ])
984 {
985 return $this->nextSiblings()->defaultOrder()->first($columns);
986 }
987
988 /**
989 * @param array $columns
990 *
991 * @return self
992 */
993 public function getPrevSibling(array $columns = [ '*' ])
994 {
995 return $this->prevSiblings()->defaultOrder('desc')->first($columns);
996 }
997
998 /**
999 * Get whether a node is a descendant of other node.
1000 *
1001 * @param self $other
1002 *
1003 * @return bool
1004 */
1005 public function isDescendantOf(self $other)
1006 {
1007 return $this->getLft() > $other->getLft() &&
1008 $this->getLft() < $other->getRgt() &&
1009 $this->isSameScope($other);
1010 }
1011
1012 /**
1013 * Get whether a node is itself or a descendant of other node.
1014 *
1015 * @param self $other
1016 *
1017 * @return bool
1018 */
1019 public function isSelfOrDescendantOf(self $other)
1020 {
1021 return $this->getLft() >= $other->getLft() &&
1022 $this->getLft() < $other->getRgt();
1023 }
1024
1025 /**
1026 * Get whether the node is immediate children of other node.
1027 *
1028 * @param self $other
1029 *
1030 * @return bool
1031 */
1032 public function isChildOf(self $other)
1033 {
1034 return $this->getParentId() == $other->getKey();
1035 }
1036
1037 /**
1038 * Get whether the node is a sibling of another node.
1039 *
1040 * @param self $other
1041 *
1042 * @return bool
1043 */
1044 public function isSiblingOf(self $other)
1045 {
1046 return $this->getParentId() == $other->getParentId();
1047 }
1048
1049 /**
1050 * Get whether the node is an ancestor of other node, including immediate parent.
1051 *
1052 * @param self $other
1053 *
1054 * @return bool
1055 */
1056 public function isAncestorOf(self $other)
1057 {
1058 return $other->isDescendantOf($this);
1059 }
1060
1061 /**
1062 * Get whether the node is itself or an ancestor of other node, including immediate parent.
1063 *
1064 * @param self $other
1065 *
1066 * @return bool
1067 */
1068 public function isSelfOrAncestorOf(self $other)
1069 {
1070 return $other->isSelfOrDescendantOf($this);
1071 }
1072
1073 /**
1074 * Get whether the node has moved since last save.
1075 *
1076 * @return bool
1077 */
1078 public function hasMoved()
1079 {
1080 return $this->moved;
1081 }
1082
1083 /**
1084 * @return array
1085 */
1086 protected function getArrayableRelations()
1087 {
1088 $result = parent::getArrayableRelations();
1089
1090 // To fix #17 when converting tree to json falling to infinite recursion.
1091 unset($result['parent']);
1092
1093 return $result;
1094 }
1095
1096 /**
1097 * Get whether user is intended to delete the model from database entirely.
1098 *
1099 * @return bool
1100 */
1101 protected function hardDeleting()
1102 {
1103 return ! $this->usesSoftDelete() || $this->forceDeleting;
1104 }
1105
1106 /**
1107 * @return array
1108 */
1109 public function getBounds()
1110 {
1111 return [ $this->getLft(), $this->getRgt() ];
1112 }
1113
1114 /**
1115 * @param $value
1116 *
1117 * @return $this
1118 */
1119 public function setLft($value)
1120 {
1121 $this->attributes[$this->getLftName()] = $value;
1122
1123 return $this;
1124 }
1125
1126 /**
1127 * @param $value
1128 *
1129 * @return $this
1130 */
1131 public function setRgt($value)
1132 {
1133 $this->attributes[$this->getRgtName()] = $value;
1134
1135 return $this;
1136 }
1137
1138 /**
1139 * @param $value
1140 *
1141 * @return $this
1142 */
1143 public function setParentId($value)
1144 {
1145 $this->attributes[$this->getParentIdName()] = $value;
1146
1147 return $this;
1148 }
1149
1150 /**
1151 * @return $this
1152 */
1153 protected function dirtyBounds()
1154 {
1155 $this->original[$this->getLftName()] = null;
1156 $this->original[$this->getRgtName()] = null;
1157
1158 return $this;
1159 }
1160
1161 /**
1162 * @param self $node
1163 *
1164 * @return $this
1165 */
1166 protected function assertNotDescendant(self $node)
1167 {
1168 if ($node == $this || $node->isDescendantOf($this)) {
1169 throw new LogicException('Node must not be a descendant.');
1170 }
1171
1172 return $this;
1173 }
1174
1175 /**
1176 * @param self $node
1177 *
1178 * @return $this
1179 */
1180 protected function assertNodeExists(self $node)
1181 {
1182 if ( ! $node->getLft() || ! $node->getRgt()) {
1183 throw new LogicException('Node must exists.');
1184 }
1185
1186 return $this;
1187 }
1188
1189 /**
1190 * @param self $node
1191 */
1192 protected function assertSameScope(self $node)
1193 {
1194 if ( ! $scoped = $this->getScopeAttributes()) {
1195 return;
1196 }
1197
1198 foreach ($scoped as $attr) {
1199 if ($this->getAttribute($attr) != $node->getAttribute($attr)) {
1200 throw new LogicException('Nodes must be in the same scope');
1201 }
1202 }
1203 }
1204
1205 /**
1206 * @param self $node
1207 */
1208 protected function isSameScope(self $node): bool
1209 {
1210 if ( ! $scoped = $this->getScopeAttributes()) {
1211 return true;
1212 }
1213
1214 foreach ($scoped as $attr) {
1215 if ($this->getAttribute($attr) != $node->getAttribute($attr)) {
1216 return false;
1217 }
1218 }
1219
1220 return true;
1221 }
1222
1223 /**
1224 * @param array|null $except
1225 *
1226 * @return \Illuminate\Database\Eloquent\Model
1227 */
1228 public function replicate(array $except = null)
1229 {
1230 $defaults = [
1231 $this->getParentIdName(),
1232 $this->getLftName(),
1233 $this->getRgtName(),
1234 ];
1235
1236 $except = $except ? array_unique(array_merge($except, $defaults)) : $defaults;
1237
1238 return parent::replicate($except);
1239 }
1240 }