Completed
Push — master ( 049e48...c56598 )
by ARCANEDEV
05:26
created

src/NodeTrait.php (1 issue)

Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php namespace Arcanedev\LaravelNestedSet;
2
3
use Arcanedev\LaravelNestedSet\Contracts\Nodeable;
4
use Arcanedev\LaravelNestedSet\Eloquent\DescendantsRelation;
5
use Arcanedev\LaravelNestedSet\Traits\EloquentTrait;
6
use Arcanedev\LaravelNestedSet\Traits\SoftDeleteTrait;
7
use Arcanedev\LaravelNestedSet\Utilities\NestedSet;
8
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
9
use LogicException;
10
11
/**
12
 * Class     NodeTrait
13
 *
14
 * @package  Arcanedev\Taxonomies\Traits
15
 * @author   ARCANEDEV <[email protected]>
16
 *
17
 * @property  int                                             $id
18
 * @property  int                                             $_lft
19
 * @property  int                                             $_rgt
20
 * @property  int                                             $parent_id
21
 * @property  array                                           $attributes
22
 * @property  array                                           $original
23
 * @property  bool                                            $exists
24
 * @property  \Arcanedev\LaravelNestedSet\Contracts\Nodeable  $parent
25
 * @property  \Illuminate\Database\Eloquent\Collection        $children
26
 *
27
 * @method  static  bool                                               isBroken()
28
 * @method  static  array                                              getNodeData($id, $required = false)
29
 * @method  static  array                                              getPlainNodeData($id, $required = false)
30
 * @method  static  int                                                rebuildTree(array $data, bool $delete = false)
31
 * @method  static  int                                                fixTree()
32
 * @method  static  \Arcanedev\LaravelNestedSet\Contracts\Nodeable     root(array $columns = ['*'])
33
 *
34
 * @method  static  \Arcanedev\LaravelNestedSet\Eloquent\QueryBuilder  ancestorsOf(mixed $id, array $columns = ['*'])
35
 * @method  static  \Arcanedev\LaravelNestedSet\Eloquent\QueryBuilder  withDepth(string $as = 'depth')
36
 * @method  static  \Arcanedev\LaravelNestedSet\Eloquent\QueryBuilder  withoutRoot()
37
 * @method  static  \Arcanedev\LaravelNestedSet\Eloquent\QueryBuilder  whereDescendantOf(mixed $id, string $boolean = 'and', bool $not = false)
38
 * @method  static  \Arcanedev\LaravelNestedSet\Eloquent\QueryBuilder  whereAncestorOf(mixed $id)
39
 * @method  static  \Arcanedev\LaravelNestedSet\Eloquent\QueryBuilder  whereIsRoot()
40
 * @method  static  array                                              countErrors()
41
 *
42
 * @method  \Illuminate\Database\Eloquent\Relations\BelongsTo          belongsTo(string $related, string $foreignKey = null, string $otherKey = null, string $relation = null)
43
 * @method  \Illuminate\Database\Eloquent\Relations\HasMany            hasMany(string $related, string $foreignKey = null, string $localKey = null)
44
 * @method  static  \Illuminate\Database\Eloquent\Builder              where(string $column, string $operator = null, mixed $value = null, string $boolean = 'and')
45
 */
46
trait NodeTrait
47
{
48
    /* ------------------------------------------------------------------------------------------------
49
     |  Traits
50
     | ------------------------------------------------------------------------------------------------
51
     */
52
    use EloquentTrait, SoftDeleteTrait;
53
54
    /* ------------------------------------------------------------------------------------------------
55
     |  Properties
56
     | ------------------------------------------------------------------------------------------------
57
     */
58
    /**
59
     * Pending operation.
60
     *
61
     * @var array|null
62
     */
63
    protected $pending;
64
65
    /**
66
     * Whether the node has moved since last save.
67
     *
68
     * @var bool
69
     */
70
    protected $moved = false;
71
72
    /**
73
     * Keep track of the number of performed operations.
74
     *
75
     * @var int
76
     */
77
    public static $actionsPerformed = 0;
78
79
    /* ------------------------------------------------------------------------------------------------
80
     |  Boot Function
81
     | ------------------------------------------------------------------------------------------------
82
     */
83
    /**
84
     * Sign on model events.
85
     */
86 284
    public static function bootNodeTrait()
87
    {
88
        static::saving(function ($model) {
89
            /** @var self $model */
90 100
            $model->getConnection()->beginTransaction();
91
92 100
            return $model->callPendingAction();
93 284
        });
94
95
        static::saved(function ($model) {
96
            /** @var self $model */
97 96
            $model->getConnection()->commit();
98 284
        });
99
100
        static::deleting(function ($model) {
101
            /** @var self $model */
102 20
            $model->getConnection()->beginTransaction();
103
104
            // We will need fresh data to delete node safely
105 20
            $model->refreshNode();
106 284
        });
107
108
        static::deleted(function ($model) {
109
            /** @var self $model */
110 20
            $model->deleteDescendants();
111
112 20
            $model->getConnection()->commit();
113 284
        });
114
115 284
        if (static::usesSoftDelete()) {
116
            static::restoring(function ($model) {
117
                /** @var self $model */
118 4
                $model->getConnection()->beginTransaction();
119
120 4
                static::$deletedAt = $model->{$model->getDeletedAtColumn()};
121 240
            });
122
123 240
            static::restored(function ($model) {
124
                /** @var self $model */
125 4
                $model->restoreDescendants(static::$deletedAt);
126
127 4
                $model->getConnection()->commit();
128 240
            });
129 180
        }
130 284
    }
131
132
    /* ------------------------------------------------------------------------------------------------
133
     |  Relationships
134
     | ------------------------------------------------------------------------------------------------
135
     */
136
    /**
137
     * Relation to the parent.
138
     *
139
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
140
     */
141 16
    public function parent()
142
    {
143 16
        return $this->belongsTo(get_class($this), $this->getParentIdName())
144 16
            ->setModel($this);
145
    }
146
147
    /**
148
     * Relation to children.
149
     *
150
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
151
     */
152 16
    public function children()
153
    {
154 16
        return $this->hasMany(get_class($this), $this->getParentIdName())
155 16
            ->setModel($this);
156
    }
157
158
    /**
159
     * Get query for descendants of the node.
160
     *
161
     * @return \Arcanedev\LaravelNestedSet\Eloquent\DescendantsRelation
162
     */
163 44
    public function descendants()
164
    {
165 44
        return new DescendantsRelation($this->newScopedQuery(), $this);
166
    }
167
168
    /**
169
     * Get query for siblings of the node.
170
     *
171
     * @return \Arcanedev\LaravelNestedSet\Eloquent\QueryBuilder
172
     */
173 8
    public function siblings()
174
    {
175 8
        return $this->newScopedQuery()
176 8
            ->where($this->getKeyName(), '<>', $this->getKey())
177 8
            ->where($this->getParentIdName(), '=', $this->getParentId());
178
    }
179
180
    /* ------------------------------------------------------------------------------------------------
181
     |  Getters & Setters
182
     | ------------------------------------------------------------------------------------------------
183
     */
184
    /**
185
     * Get the lft key name.
186
     *
187
     * @return string
188
     */
189 260
    public function getLftName()
190
    {
191 260
        return NestedSet::LFT;
192
    }
193
194
    /**
195
     * Get the rgt key name.
196
     *
197
     * @return string
198
     */
199 224
    public function getRgtName()
200
    {
201 224
        return NestedSet::RGT;
202
    }
203
204
    /**
205
     * Get the parent id key name.
206
     *
207
     * @return string
208
     */
209 168
    public function getParentIdName()
210
    {
211 168
        return NestedSet::PARENT_ID;
212
    }
213
214
    /**
215
     * Get the value of the model's lft key.
216
     *
217
     * @return int
218
     */
219 188
    public function getLft()
220
    {
221 188
        return $this->getAttributeValue($this->getLftName());
222
    }
223
224
    /**
225
     * Set the value of the model's lft key.
226
     *
227
     * @param  int  $value
228
     *
229
     * @return self
230
     */
231 96
    public function setLft($value)
232
    {
233 96
        $this->attributes[$this->getLftName()] = $value;
234
235 96
        return $this;
236
    }
237
238
    /**
239
     * Get the value of the model's rgt key.
240
     *
241
     * @return int
242
     */
243 124
    public function getRgt()
244
    {
245 124
        return $this->getAttributeValue($this->getRgtName());
246
    }
247
248
    /**
249
     * Set the value of the model's rgt key.
250
     *
251
     * @param  int  $value
252
     *
253
     * @return self
254
     */
255 96
    public function setRgt($value)
256
    {
257 96
        $this->attributes[$this->getRgtName()] = $value;
258
259 96
        return $this;
260
    }
261
262
    /**
263
     * Get the value of the model's parent id key.
264
     *
265
     * @return int
266
     */
267 100
    public function getParentId()
268
    {
269 100
        return $this->getAttributeValue($this->getParentIdName());
270
    }
271
272
    /**
273
     * Set the value of the model's parent id key.
274
     *
275
     * @param  int  $value
276
     *
277
     * @return self
278
     */
279 68
    public function setParentId($value)
280
    {
281 68
        $this->attributes[$this->getParentIdName()] = $value;
282
283 68
        return $this;
284
    }
285
286
    /**
287
     * Apply parent model.
288
     *
289
     * @param  \Arcanedev\LaravelNestedSet\Contracts\Nodeable  $value
290
     *
291
     * @return self
292
     */
293 56
    protected function setParent($value)
294
    {
295 56
        $this->setParentId($value ? $value->getKey() : null)
296 56
            ->setRelation('parent', $value);
297
298 56
        return $this;
299
    }
300
301
    /**
302
     * Set the value of model's parent id key.
303
     *
304
     * Behind the scenes node is appended to found parent node.
305
     *
306
     * @param  int  $value
307
     *
308
     * @throws \Exception If parent node doesn't exists
309
     */
310 12
    public function setParentIdAttribute($value)
311
    {
312 12
        if ($this->getParentId() == $value) return;
313
314 12
        if ($value) {
315
            /** @var self $node */
316 12
            $node = $this->newScopedQuery()->findOrFail($value);
317
318 12
            $this->appendToNode($node);
319 9
        } else {
320 4
            $this->makeRoot();
321
        }
322 12
    }
323
324
    /**
325
     * Get the boundaries.
326
     *
327
     * @return array
328
     */
329 44
    public function getBounds()
330
    {
331 44
        return [$this->getLft(), $this->getRgt()];
332
    }
333
334
    /**
335
     * Get the scope attributes.
336
     *
337
     * @return array
338
     */
339 160
    protected function getScopeAttributes()
340
    {
341 160
        return null;
342
    }
343
344
    /**
345
     * Set the lft and rgt boundaries to null.
346
     *
347
     * @return self
348
     */
349 68
    protected function dirtyBounds()
350
    {
351 68
        return $this->setLft(null)->setRgt(null);
352
    }
353
354
    /**
355
     * Returns node that is next to current node without constraining to siblings.
356
     * This can be either a next sibling or a next sibling of the parent node.
357
     *
358
     * @param  array  $columns
359
     *
360
     * @return self
361
     */
362
    public function getNextNode(array $columns = ['*'])
363
    {
364
        return $this->nextNodes()->defaultOrder()->first($columns);
365
    }
366
367
    /**
368
     * Returns node that is before current node without constraining to siblings.
369
     * This can be either a prev sibling or parent node.
370
     *
371
     * @param  array  $columns
372
     *
373
     * @return self
374
     */
375 4
    public function getPrevNode(array $columns = ['*'])
376
    {
377 4
        return $this->prevNodes()->defaultOrder('desc')->first($columns);
378
    }
379
380
    /**
381
     * Get the ancestors nodes.
382
     *
383
     * @param  array  $columns
384
     *
385
     * @return \Arcanedev\LaravelNestedSet\Eloquent\Collection
386
     */
387 8
    public function getAncestors(array $columns = ['*'])
388
    {
389 8
        return $this->newScopedQuery()
390 8
            ->defaultOrder()
391 8
            ->ancestorsOf($this, $columns);
392
    }
393
394
    /**
395
     * Get the descendants nodes.
396
     *
397
     * @param  array  $columns
398
     *
399
     * @return \Arcanedev\LaravelNestedSet\Eloquent\Collection
400
     */
401 12
    public function getDescendants(array $columns = ['*'])
402
    {
403 12
        return $this->descendants()->get($columns);
404
    }
405
406
    /**
407
     * Get the siblings nodes.
408
     *
409
     * @param  array  $columns
410
     *
411
     * @return \Arcanedev\LaravelNestedSet\Eloquent\Collection
412
     */
413 8
    public function getSiblings(array $columns = ['*'])
414
    {
415 8
        return $this->siblings()->get($columns);
416
    }
417
418
    /**
419
     * Get the next siblings nodes.
420
     *
421
     * @param  array  $columns
422
     *
423
     * @return \Arcanedev\LaravelNestedSet\Eloquent\Collection
424
     */
425 8
    public function getNextSiblings(array $columns = ['*'])
426
    {
427 8
        return $this->nextSiblings()->get($columns);
428
    }
429
430
    /**
431
     * Get the previous siblings nodes.
432
     *
433
     * @param  array  $columns
434
     *
435
     * @return \Arcanedev\LaravelNestedSet\Eloquent\Collection
436
     */
437 8
    public function getPrevSiblings(array $columns = ['*'])
438
    {
439 8
        return $this->prevSiblings()->get($columns);
440
    }
441
442
    /**
443
     * Get the next sibling node.
444
     *
445
     * @param  array  $columns
446
     *
447
     * @return self
448
     */
449 4
    public function getNextSibling(array $columns = ['*'])
450
    {
451 4
        return $this->nextSiblings()->defaultOrder()->first($columns);
452
    }
453
454
    /**
455
     * Get the previous sibling node.
456
     *
457
     * @param  array  $columns
458
     *
459
     * @return self
460
     */
461 4
    public function getPrevSibling(array $columns = ['*'])
462
    {
463 4
        return $this->prevSiblings()->defaultOrder('desc')->first($columns);
464
    }
465
466
    /**
467
     * Get node height (rgt - lft + 1).
468
     *
469
     * @return int
470
     */
471 36
    public function getNodeHeight()
472
    {
473 36
        if ( ! $this->exists) return 2;
474
475 4
        return $this->getRgt() - $this->getLft() + 1;
476
    }
477
478
    /**
479
     * Get number of descendant nodes.
480
     *
481
     * @return int
482
     */
483 4
    public function getDescendantCount()
484
    {
485 4
        return (int) ceil($this->getNodeHeight() / 2) - 1;
486
    }
487
488
    /**
489
     * Set raw node.
490
     *
491
     * @param  int  $lft
492
     * @param  int  $rgt
493
     * @param  int  $parentId
494
     *
495
     * @return self
496
     */
497 12
    public function rawNode($lft, $rgt, $parentId)
498
    {
499 12
        $this->setLft($lft)->setRgt($rgt)->setParentId($parentId);
500
501 12
        return $this->setNodeAction('raw');
502
    }
503
504
    /**
505
     * Set an action.
506
     *
507
     * @param  string  $action
508
     *
509
     * @return self
510
     */
511 120
    protected function setNodeAction($action)
512
    {
513 120
        $this->pending = func_get_args();
514 120
        unset($action);
515
516 120
        return $this;
517
    }
518
519
    /* ------------------------------------------------------------------------------------------------
520
     |  Other Functions
521
     | ------------------------------------------------------------------------------------------------
522
     */
523
    /**
524
     * Get the lower bound.
525
     *
526
     * @return int
527
     */
528 32
    protected function getLowerBound()
529
    {
530 32
        return (int) $this->newNestedSetQuery()->max($this->getRgtName());
531
    }
532
533
    /**
534
     * Call pending action.
535
     *
536
     * @return null|false
537
     */
538 100
    protected function callPendingAction()
539
    {
540 100
        $this->moved = false;
541
542 100
        if ( ! $this->pending && ! $this->exists) {
543 20
            $this->makeRoot();
544 15
        }
545
546 100
        if ( ! $this->pending) return;
547
548 96
        $method        = 'action'.ucfirst(array_shift($this->pending));
549 96
        $parameters    = $this->pending;
550
551 96
        $this->pending = null;
552 96
        $this->moved   = call_user_func_array([$this, $method], $parameters);
553 96
    }
554
555
    /**
556
     * @return bool
557
     */
558 12
    protected function actionRaw()
559
    {
560 12
        return true;
561
    }
562
563
    /**
564
     * Make a root node.
565
     */
566 32
    protected function actionRoot()
567
    {
568
        // Simplest case that do not affect other nodes.
569 32
        if ( ! $this->exists) {
570 20
            $cut = $this->getLowerBound() + 1;
571
572 20
            $this->setLft($cut);
573 20
            $this->setRgt($cut + 1);
574
575 20
            return true;
576
        }
577
578 12
        if ($this->isRoot()) return false;
579
580
581
        // Reset parent object
582 12
        $this->setParent(null);
0 ignored issues
show
null is of type null, but the function expects a object<Arcanedev\Laravel...Set\Contracts\Nodeable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
583
584 12
        return $this->insertAt($this->getLowerBound() + 1);
585
    }
586
587
    /**
588
     * Append or prepend a node to the parent.
589
     *
590
     * @param  \Arcanedev\LaravelNestedSet\Contracts\Nodeable  $parent
591
     * @param  bool                                            $prepend
592
     *
593
     * @return bool
594
     */
595 32
    protected function actionAppendOrPrepend(Nodeable $parent, $prepend = false)
596
    {
597 32
        $parent->refreshNode();
598
599 32
        $cut = $prepend ? $parent->getLft() + 1 : $parent->getRgt();
600
601 32
        if ( ! $this->insertAt($cut)) {
602
            return false;
603
        }
604
605 32
        $parent->refreshNode();
606
607 32
        return true;
608
    }
609
610
    /**
611
     * Insert node before or after another node.
612
     *
613
     * @param  self  $node
614
     * @param  bool  $after
615
     *
616
     * @return bool
617
     */
618 28
    protected function actionBeforeOrAfter(self $node, $after = false)
619
    {
620 28
        $node->refreshNode();
621
622 28
        return $this->insertAt($after ? $node->getRgt() + 1 : $node->getLft());
623
    }
624
625
    /**
626
     * Refresh node's crucial attributes.
627
     */
628 88
    public function refreshNode()
629
    {
630 88
        if ( ! $this->exists || static::$actionsPerformed === 0) return;
631
632 64
        $attributes = $this->newNestedSetQuery()->getNodeData($this->getKey());
633
634 64
        $this->attributes = array_merge($this->attributes, $attributes);
635 64
        $this->original   = array_merge($this->original,   $attributes);
636 64
    }
637
638
    /**
639
     * Get query for siblings after the node.
640
     *
641
     * @return \Arcanedev\LaravelNestedSet\Eloquent\QueryBuilder
642
     */
643 24
    public function nextSiblings()
644
    {
645 24
        return $this->nextNodes()
646 24
            ->where($this->getParentIdName(), '=', $this->getParentId());
647
    }
648
649
    /**
650
     * Get query for siblings before the node.
651
     *
652
     * @return \Arcanedev\LaravelNestedSet\Eloquent\QueryBuilder
653
     */
654 16
    public function prevSiblings()
655
    {
656 16
        return $this->prevNodes()
657 16
            ->where($this->getParentIdName(), '=', $this->getParentId());
658
    }
659
660
    /**
661
     * Get query for nodes after current node.
662
     *
663
     * @return \Arcanedev\LaravelNestedSet\Eloquent\QueryBuilder
664
     */
665 28
    public function nextNodes()
666
    {
667 28
        return $this->newScopedQuery()
668 28
            ->where($this->getLftName(), '>', $this->getLft());
669
    }
670
671
    /**
672
     * Get query for nodes before current node in reversed order.
673
     *
674
     * @return \Arcanedev\LaravelNestedSet\Eloquent\QueryBuilder
675
     */
676 20
    public function prevNodes()
677
    {
678 20
        return $this->newScopedQuery()
679 20
            ->where($this->getLftName(), '<', $this->getLft());
680
    }
681
682
    /**
683
     * Get query for ancestors to the node not including the node itself.
684
     *
685
     * @return \Arcanedev\LaravelNestedSet\Eloquent\QueryBuilder
686
     */
687 4
    public function ancestors()
688
    {
689 4
        return $this->newScopedQuery()
690 4
            ->whereAncestorOf($this)->defaultOrder();
691
    }
692
693
    /**
694
     * Make this node a root node.
695
     *
696
     * @return self
697
     */
698 48
    public function makeRoot()
699
    {
700 48
        return $this->setNodeAction('root');
701
    }
702
703
    /**
704
     * Save node as root.
705
     *
706
     * @return bool
707
     */
708 8
    public function saveAsRoot()
709
    {
710 8
        if ($this->exists && $this->isRoot()) {
711
            return true;
712
        }
713
714 8
        return $this->makeRoot()->save();
715
    }
716
717
    /**
718
     * Append and save a node.
719
     *
720
     * @param  \Arcanedev\LaravelNestedSet\Contracts\Nodeable  $node
721
     *
722
     * @return bool
723
     */
724 16
    public function appendNode(Nodeable $node)
725
    {
726 16
        return $node->appendToNode($this)->save();
727
    }
728
729
    /**
730
     * Prepend and save a node.
731
     *
732
     * @param  \Arcanedev\LaravelNestedSet\Contracts\Nodeable  $node
733
     *
734
     * @return bool
735
     */
736 4
    public function prependNode(Nodeable $node)
737
    {
738 4
        return $node->prependToNode($this)->save();
739
    }
740
741
    /**
742
     * Append a node to the new parent.
743
     *
744
     * @param  \Arcanedev\LaravelNestedSet\Contracts\Nodeable  $parent
745
     *
746
     * @return self
747
     */
748 44
    public function appendToNode(Nodeable $parent)
749
    {
750 44
        return $this->appendOrPrependTo($parent);
751
    }
752
753
    /**
754
     * Prepend a node to the new parent.
755
     *
756
     * @param  \Arcanedev\LaravelNestedSet\Contracts\Nodeable  $parent
757
     *
758
     * @return \Arcanedev\LaravelNestedSet\Contracts\Nodeable
759
     */
760 8
    public function prependToNode(Nodeable $parent)
761
    {
762 8
        return $this->appendOrPrependTo($parent, true);
763
    }
764
765
    /**
766
     * Append or prepend a node to parent.
767
     *
768
     * @param  \Arcanedev\LaravelNestedSet\Contracts\Nodeable  $parent
769
     * @param  bool                                            $prepend
770
     *
771
     * @return \Arcanedev\LaravelNestedSet\Contracts\Nodeable
772
     */
773 52
    public function appendOrPrependTo(Nodeable $parent, $prepend = false)
774
    {
775 52
        $this->assertNodeExists($parent)
776 48
             ->assertNotDescendant($parent);
777
778 40
        $this->setParent($parent)->dirtyBounds();
779
780 40
        return $this->setNodeAction('appendOrPrepend', $parent, $prepend);
781
    }
782
783
    /**
784
     * Insert self after a node.
785
     *
786
     * @param  \Arcanedev\LaravelNestedSet\Contracts\Nodeable  $node
787
     *
788
     * @return \Arcanedev\LaravelNestedSet\Contracts\Nodeable
789
     */
790 28
    public function afterNode(Nodeable $node)
791
    {
792 28
        return $this->beforeOrAfterNode($node, true);
793
    }
794
795
    /**
796
     * Insert self before node.
797
     *
798
     * @param  \Arcanedev\LaravelNestedSet\Contracts\Nodeable  $node
799
     *
800
     * @return \Arcanedev\LaravelNestedSet\Contracts\Nodeable
801
     */
802 8
    public function beforeNode(Nodeable $node)
803
    {
804 8
        return $this->beforeOrAfterNode($node);
805
    }
806
807
    /**
808
     * Set before or after a node.
809
     *
810
     * @param  \Arcanedev\LaravelNestedSet\Contracts\Nodeable  $node
811
     * @param  bool                                            $after
812
     *
813
     * @return \Arcanedev\LaravelNestedSet\Contracts\Nodeable
814
     */
815 36
    public function beforeOrAfterNode(Nodeable $node, $after = false)
816
    {
817 36
        $this->assertNodeExists($node)->assertNotDescendant($node);
818
819 32
        if ( ! $this->isSiblingOf($node)) {
820 12
            $this->setParent($node->getRelationValue('parent'));
821 9
        }
822
823 32
        $this->dirtyBounds();
824
825 32
        return $this->setNodeAction('beforeOrAfter', $node, $after);
826
    }
827
828
    /**
829
     * Insert after a node and save.
830
     *
831
     * @param  \Arcanedev\LaravelNestedSet\Contracts\Nodeable  $node
832
     *
833
     * @return bool
834
     */
835 16
    public function insertAfterNode(Nodeable $node)
836
    {
837 16
        return $this->afterNode($node)->save();
838
    }
839
840
    /**
841
     * Insert self before a node and save.
842
     *
843
     * @param  \Arcanedev\LaravelNestedSet\Contracts\Nodeable  $node
844
     *
845
     * @return bool
846
     */
847 4
    public function insertBeforeNode(Nodeable $node)
848
    {
849 4
        if ( ! $this->beforeNode($node)->save()) return false;
850
851
        // We'll update the target node since it will be moved
852 4
        $node->refreshNode();
853
854 4
        return true;
855
    }
856
857
    /**
858
     * Move node up given amount of positions.
859
     *
860
     * @param  int  $amount
861
     *
862
     * @return bool
863
     */
864 7
    public function up($amount = 1)
865
    {
866 4
        $sibling = $this->prevSiblings()
867 4
                        ->defaultOrder('desc')
868 4
                        ->skip($amount - 1)
869 4
                        ->first();
870
871 4
        if ( ! $sibling) return false;
872
873 7
        return $this->insertBeforeNode($sibling);
874
    }
875
876
    /**
877
     * Move node down given amount of positions.
878
     *
879
     * @param  int  $amount
880
     *
881
     * @return bool
882
     */
883 16
    public function down($amount = 1)
884
    {
885 16
        $sibling = $this->nextSiblings()
886 16
                        ->defaultOrder()
887 16
                        ->skip($amount - 1)
888 16
                        ->first();
889
890 16
        if ( ! $sibling) return false;
891
892 16
        return $this->insertAfterNode($sibling);
893
    }
894
895
    /**
896
     * Insert node at specific position.
897
     *
898
     * @param  int  $position
899
     *
900
     * @return bool
901
     */
902 68
    protected function insertAt($position)
903
    {
904 68
        ++static::$actionsPerformed;
905
906 68
        $result = $this->exists
907 61
            ? $this->moveNode($position)
908 68
            : $this->insertNode($position);
909
910 68
        return $result;
911
    }
912
913
    /**
914
     * Move a node to the new position.
915
     *
916
     * @param  int  $position
917
     *
918
     * @return int
919
     */
920 40
    protected function moveNode($position)
921
    {
922 40
        $updated = $this->newNestedSetQuery()
923 40
                        ->moveNode($this->getKey(), $position) > 0;
924
925 40
        if ($updated) $this->refreshNode();
926
927 40
        return $updated;
928
    }
929
930
    /**
931
     * Insert new node at specified position.
932
     *
933
     * @param  int  $position
934
     *
935
     * @return bool
936
     */
937 32
    protected function insertNode($position)
938
    {
939 32
        $this->newNestedSetQuery()->makeGap($position, 2);
940
941 32
        $height = $this->getNodeHeight();
942
943 32
        $this->setLft($position);
944 32
        $this->setRgt($position + $height - 1);
945
946 32
        return true;
947
    }
948
949
    /**
950
     * Update the tree when the node is removed physically.
951
     */
952 20
    protected function deleteDescendants()
953
    {
954 20
        $lft = $this->getLft();
955 20
        $rgt = $this->getRgt();
956
957 20
        $method = ($this->usesSoftDelete() && $this->forceDeleting)
958 18
            ? 'forceDelete'
959 20
            : 'delete';
960
961 20
        $this->descendants()->{$method}();
962
963 20
        if ($this->hardDeleting()) {
964 16
            $height = $rgt - $lft + 1;
965
966 16
            $this->newNestedSetQuery()->makeGap($rgt + 1, -$height);
967
968
            // In case if user wants to re-create the node
969 16
            $this->makeRoot();
970
971 16
            static::$actionsPerformed++;
972 12
        }
973 20
    }
974
975
    /**
976
     * Restore the descendants.
977
     *
978
     * @param  \Carbon\Carbon  $deletedAt
979
     */
980 4
    protected function restoreDescendants($deletedAt)
981
    {
982 4
        $this->descendants()
983 4
            ->where($this->getDeletedAtColumn(), '>=', $deletedAt)
984 4
            ->applyScopes()
985 4
            ->restore();
986 4
    }
987
988
    /**
989
     * Get a new base query that includes deleted nodes.
990
     *
991
     * @param  string|null $table
992
     *
993
     * @return \Arcanedev\LaravelNestedSet\Eloquent\QueryBuilder
994
     */
995 116
    public function newNestedSetQuery($table = null)
996
    {
997 116
        $builder = $this->usesSoftDelete()
998 110
            ? $this->withTrashed()
999 116
            : $this->newQuery();
1000
1001 116
        return $this->applyNestedSetScope($builder, $table);
1002
    }
1003
1004
    /**
1005
     * @param  string|null  $table
1006
     *
1007
     * @return \Arcanedev\LaravelNestedSet\Eloquent\QueryBuilder
1008
     */
1009 136
    public function newScopedQuery($table = null)
1010
    {
1011 136
        return $this->applyNestedSetScope($this->newQuery(), $table);
1012
    }
1013
1014
    /**
1015
     * @param  \Illuminate\Database\Eloquent\Builder  $query
1016
     * @param  string                                 $table
1017
     *
1018
     * @return \Arcanedev\LaravelNestedSet\Eloquent\QueryBuilder|\Illuminate\Database\Query\Builder
1019
     */
1020 204
    public function applyNestedSetScope($query, $table = null)
1021
    {
1022 204
        if ( ! $scoped = $this->getScopeAttributes()) {
1023 160
            return $query;
1024
        }
1025
1026 44
        if ($table === null) {
1027 44
            $table = $this->getTable();
1028 33
        }
1029
1030 44
        foreach ($scoped as $attribute) {
1031 44
            $query->where("$table.$attribute", '=', $this->getAttributeValue($attribute));
1032 33
        }
1033
1034 44
        return $query;
1035
    }
1036
1037
    /**
1038
     * @param  array  $attributes
1039
     *
1040
     * @return self
1041
     */
1042 12
    public static function scoped(array $attributes)
1043
    {
1044 12
        $instance = new static;
1045
1046 12
        $instance->setRawAttributes($attributes);
1047
1048 12
        return $instance->newScopedQuery();
1049
    }
1050
1051
    /**
1052
     * Save a new model and return the instance.
1053
     *
1054
     * Use `children` key on `$attributes` to create child nodes.
1055
     *
1056
     * @param  array  $attributes
1057
     * @param  self   $parent
1058
     *
1059
     * @return self
1060
     */
1061 12
    public static function create(array $attributes = [], self $parent = null)
1062
    {
1063 12
        $children = array_pull($attributes, 'children');
1064 12
        $instance = new static($attributes);
1065
1066 12
        if ($parent) {
1067 4
            $instance->appendToNode($parent);
1068 3
        }
1069
1070 12
        $instance->save();
1071
1072
        // Now create children
1073 12
        $relation = new EloquentCollection;
1074
1075 12
        foreach ((array) $children as $child) {
1076 4
            $relation->add($child = static::create($child, $instance));
1077
1078 4
            $child->setRelation('parent', $instance);
1079 9
        }
1080
1081 12
        return $instance->setRelation('children', $relation);
1082
    }
1083
1084
    /* ------------------------------------------------------------------------------------------------
1085
     |  Check Functions
1086
     | ------------------------------------------------------------------------------------------------
1087
     */
1088
    /**
1089
     * Get whether node is root.
1090
     *
1091
     * @return bool
1092
     */
1093 16
    public function isRoot()
1094
    {
1095 16
        return is_null($this->getParentId());
1096
    }
1097
1098
    /**
1099
     * Get whether a node is a descendant of other node.
1100
     *
1101
     * @param  \Arcanedev\LaravelNestedSet\Contracts\Nodeable  $node
1102
     *
1103
     * @return bool
1104
     */
1105 76
    public function isDescendantOf(Nodeable $node)
1106
    {
1107
        return (
1108 76
            $this->getLft() > $node->getLft() &&
1109 70
            $this->getLft() < $node->getRgt()
1110 57
        );
1111
    }
1112
1113
    /**
1114
     * Get whether the node is immediate children of other node.
1115
     *
1116
     * @param  \Arcanedev\LaravelNestedSet\Contracts\Nodeable  $node
1117
     *
1118
     * @return bool
1119
     */
1120 4
    public function isChildOf(Nodeable $node)
1121
    {
1122 4
        return $this->getParentId() == $node->getKey();
1123
    }
1124
1125
    /**
1126
     * Get whether the node is a sibling of another node.
1127
     *
1128
     * @param  \Arcanedev\LaravelNestedSet\Contracts\Nodeable  $node
1129
     *
1130
     * @return bool
1131
     */
1132 32
    public function isSiblingOf(Nodeable $node)
1133
    {
1134 32
        return $this->getParentId() == $node->getParentId();
1135
    }
1136
1137
    /**
1138
     * Get whether the node is an ancestor of other node, including immediate parent.
1139
     *
1140
     * @param  \Arcanedev\LaravelNestedSet\Contracts\Nodeable  $node
1141
     *
1142
     * @return bool
1143
     */
1144 4
    public function isAncestorOf(Nodeable $node)
1145
    {
1146
        /** @var \Arcanedev\LaravelNestedSet\Contracts\Nodeable $this */
1147 4
        return $node->isDescendantOf($this);
1148
    }
1149
1150
    /**
1151
     * Get whether the node has moved since last save.
1152
     *
1153
     * @return bool
1154
     */
1155 24
    public function hasMoved()
1156
    {
1157 24
        return $this->moved;
1158
    }
1159
1160
    /* ------------------------------------------------------------------------------------------------
1161
     |  Assertion Functions
1162
     | ------------------------------------------------------------------------------------------------
1163
     */
1164
    /**
1165
     * Assert that the node is not a descendant.
1166
     *
1167
     * @param  \Arcanedev\LaravelNestedSet\Contracts\Nodeable  $node
1168
     *
1169
     * @return self
1170
     *
1171
     * @throws \LogicException
1172
     */
1173 80
    protected function assertNotDescendant(Nodeable $node)
1174
    {
1175
        /** @var \Arcanedev\LaravelNestedSet\Contracts\Nodeable $this */
1176 80
        if ($node == $this || $node->isDescendantOf($this)) {
1177 12
            throw new LogicException('Node must not be a descendant.');
1178
        }
1179
1180 68
        return $this;
1181
    }
1182
1183
    /**
1184
     * Assert node exists.
1185
     *
1186
     * @param  \Arcanedev\LaravelNestedSet\Contracts\Nodeable  $node
1187
     *
1188
     * @return self
1189
     *
1190
     * @throws \LogicException
1191
     */
1192 84
    protected function assertNodeExists(Nodeable $node)
1193
    {
1194 84
        if ( ! $node->getLft() || ! $node->getRgt()) {
1195 4
            throw new LogicException('Node must exists.');
1196
        }
1197
1198 80
        return $this;
1199
    }
1200
}
1201