Completed
Pull Request — master (#2)
by ARCANEDEV
09:08
created

NodeTrait::getPrevSibling()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
c 0
b 0
f 0
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 1
crap 1
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\LaravelNestedSet
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  \Arcanedev\LaravelNestedSet\Eloquent\QueryBuilder  descendantsAndSelf(mixed $id, array $columns = ['*'])
41
 * @method  static  array                                              countErrors()
42
 *
43
 * @method  \Illuminate\Database\Eloquent\Relations\BelongsTo          belongsTo(string $related, string $foreignKey = null, string $otherKey = null, string $relation = null)
44
 * @method  \Illuminate\Database\Eloquent\Relations\HasMany            hasMany(string $related, string $foreignKey = null, string $localKey = null)
45
 * @method  static  \Illuminate\Database\Eloquent\Builder              where(string $column, string $operator = null, mixed $value = null, string $boolean = 'and')
46
 */
47
trait NodeTrait
48
{
49
    /* ------------------------------------------------------------------------------------------------
50
     |  Traits
51
     | ------------------------------------------------------------------------------------------------
52
     */
53
    use EloquentTrait, SoftDeleteTrait;
54
55
    /* ------------------------------------------------------------------------------------------------
56
     |  Properties
57
     | ------------------------------------------------------------------------------------------------
58
     */
59
    /**
60
     * Pending operation.
61
     *
62
     * @var array|null
63
     */
64
    protected $pending;
65
66
    /**
67
     * Whether the node has moved since last save.
68
     *
69
     * @var bool
70
     */
71
    protected $moved = false;
72
73
    /**
74
     * Keep track of the number of performed operations.
75
     *
76
     * @var int
77
     */
78
    public static $actionsPerformed = 0;
79
80
    /* ------------------------------------------------------------------------------------------------
81
     |  Boot Function
82
     | ------------------------------------------------------------------------------------------------
83
     */
84
    /**
85
     * Sign on model events.
86
     */
87 216
    public static function bootNodeTrait()
88
    {
89
        static::saving(function ($model) {
90 75
            return $model->callPendingAction();
91 216
        });
92
93
        static::deleting(function ($model) {
0 ignored issues
show
Bug introduced by
The method deleting() does not exist on Arcanedev\LaravelNestedSet\NodeTrait. Did you maybe mean hardDeleting()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
94
            // We will need fresh data to delete node safely
95 15
            $model->refreshNode();
96 216
        });
97
98
        static::deleted(function ($model) {
0 ignored issues
show
Bug introduced by
The method deleted() does not exist on Arcanedev\LaravelNestedSet\NodeTrait. Did you maybe mean deleteDescendants()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
99 15
            $model->deleteDescendants();
100 216
        });
101
102 216
        if (static::usesSoftDelete()) {
103
            static::restoring(function ($model) {
104 3
                static::$deletedAt = $model->{$model->getDeletedAtColumn()};
105 183
            });
106 183
            static::restored(function ($model) {
0 ignored issues
show
Bug introduced by
The method restored() does not exist on Arcanedev\LaravelNestedSet\NodeTrait. Did you maybe mean restoreDescendants()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
107 3
                $model->restoreDescendants(static::$deletedAt);
108 183
            });
109 61
        }
110 216
    }
111
112
    /* ------------------------------------------------------------------------------------------------
113
     |  Relationships
114
     | ------------------------------------------------------------------------------------------------
115
     */
116
    /**
117
     * Relation to the parent.
118
     *
119
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
120
     */
121 12
    public function parent()
122
    {
123 12
        return $this->belongsTo(get_class($this), $this->getParentIdName())
124 12
            ->setModel($this);
125
    }
126
127
    /**
128
     * Relation to children.
129
     *
130
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
131
     */
132 12
    public function children()
133
    {
134 12
        return $this->hasMany(get_class($this), $this->getParentIdName())
135 12
            ->setModel($this);
136
    }
137
138
    /**
139
     * Get query for descendants of the node.
140
     *
141
     * @return \Arcanedev\LaravelNestedSet\Eloquent\DescendantsRelation
142
     */
143 36
    public function descendants()
144
    {
145 36
        return new DescendantsRelation($this->newScopedQuery(), $this);
146
    }
147
148
    /**
149
     * Get query for siblings of the node.
150
     *
151
     * @return \Arcanedev\LaravelNestedSet\Eloquent\QueryBuilder
152
     */
153 6
    public function siblings()
154
    {
155 6
        return $this->newScopedQuery()
156 6
            ->where($this->getKeyName(), '<>', $this->getKey())
157 6
            ->where($this->getParentIdName(), '=', $this->getParentId());
158
    }
159
160
    /**
161
     * Get query for the node siblings and the node itself.
162
     *
163
     * @return \Arcanedev\LaravelNestedSet\Eloquent\QueryBuilder
164
     */
165
    public function siblingsAndSelf()
166
    {
167
        return $this->newScopedQuery()
168
            ->where($this->getParentIdName(), '=', $this->getParentId());
169
    }
170
171
    /**
172
     * Get the node siblings and the node itself.
173
     *
174
     * @param  array  $columns
175
     *
176
     * @return \Illuminate\Database\Eloquent\Collection
177
     */
178
    public function getSiblingsAndSelf(array $columns = ['*'])
179
    {
180
        return $this->siblingsAndSelf()->get($columns);
181
    }
182
183
    /* ------------------------------------------------------------------------------------------------
184
     |  Getters & Setters
185
     | ------------------------------------------------------------------------------------------------
186
     */
187
    /**
188
     * Get the lft key name.
189
     *
190
     * @return string
191
     */
192 195
    public function getLftName()
193
    {
194 195
        return NestedSet::LFT;
195
    }
196
197
    /**
198
     * Get the rgt key name.
199
     *
200
     * @return string
201
     */
202 168
    public function getRgtName()
203
    {
204 168
        return NestedSet::RGT;
205
    }
206
207
    /**
208
     * Get the parent id key name.
209
     *
210
     * @return string
211
     */
212 129
    public function getParentIdName()
213
    {
214 129
        return NestedSet::PARENT_ID;
215
    }
216
217
    /**
218
     * Get the value of the model's lft key.
219
     *
220
     * @return int
221
     */
222 144
    public function getLft()
223
    {
224 144
        return $this->getAttributeValue($this->getLftName());
225
    }
226
227
    /**
228
     * Set the value of the model's lft key.
229
     *
230
     * @param  int  $value
231
     *
232
     * @return self
233
     */
234 45
    public function setLft($value)
235
    {
236 45
        $this->attributes[$this->getLftName()] = $value;
237
238 45
        return $this;
239
    }
240
241
    /**
242
     * Get the value of the model's rgt key.
243
     *
244
     * @return int
245
     */
246 96
    public function getRgt()
247
    {
248 96
        return $this->getAttributeValue($this->getRgtName());
249
    }
250
251
    /**
252
     * Set the value of the model's rgt key.
253
     *
254
     * @param  int  $value
255
     *
256
     * @return self
257
     */
258 45
    public function setRgt($value)
259
    {
260 45
        $this->attributes[$this->getRgtName()] = $value;
261
262 45
        return $this;
263
    }
264
265
    /**
266
     * Get the value of the model's parent id key.
267
     *
268
     * @return int
269
     */
270 78
    public function getParentId()
271
    {
272 78
        return $this->getAttributeValue($this->getParentIdName());
273
    }
274
275
    /**
276
     * Set the value of the model's parent id key.
277
     *
278
     * @param  int  $value
279
     *
280
     * @return self
281
     */
282 51
    public function setParentId($value)
283
    {
284 51
        $this->attributes[$this->getParentIdName()] = $value;
285
286 51
        return $this;
287
    }
288
289
    /**
290
     * Apply parent model.
291
     *
292
     * @param  \Arcanedev\LaravelNestedSet\Contracts\Nodeable|null  $value
293
     *
294
     * @return self
295
     */
296 42
    protected function setParent($value)
297
    {
298 42
        $this->setParentId($value ? $value->getKey() : null)
299 42
            ->setRelation('parent', $value);
300
301 42
        return $this;
302
    }
303
304
    /**
305
     * Set the value of model's parent id key.
306
     *
307
     * Behind the scenes node is appended to found parent node.
308
     *
309
     * @param  int  $value
310
     *
311
     * @throws \Exception If parent node doesn't exists
312
     */
313 9
    public function setParentIdAttribute($value)
314
    {
315 9
        if ($this->getParentId() == $value) return;
316
317 9
        if ($value) {
318
            /** @var \Arcanedev\LaravelNestedSet\Contracts\Nodeable $node */
319 9
            $node = $this->newScopedQuery()->findOrFail($value);
320
321 9
            $this->appendToNode($node);
322 3
        } else {
323 3
            $this->makeRoot();
324
        }
325 9
    }
326
327
    /**
328
     * Get the boundaries.
329
     *
330
     * @return array
331
     */
332 36
    public function getBounds()
333
    {
334 36
        return [$this->getLft(), $this->getRgt()];
335
    }
336
337
    /**
338
     * Get the scope attributes.
339
     *
340
     * @return array
341
     */
342 123
    protected function getScopeAttributes()
343
    {
344 123
        return null;
345
    }
346
347
    /**
348
     * Set the lft and rgt boundaries to null.
349
     *
350
     * @return self
351
     */
352 51
    protected function dirtyBounds()
353
    {
354 51
        $this->original[$this->getLftName()] = null;
355 51
        $this->original[$this->getRgtName()] = null;
356
357 51
        return $this;
358
    }
359
360
    /**
361
     * Returns node that is next to current node without constraining to siblings.
362
     * This can be either a next sibling or a next sibling of the parent node.
363
     *
364
     * @param  array  $columns
365
     *
366
     * @return self
367
     */
368
    public function getNextNode(array $columns = ['*'])
369
    {
370
        return $this->nextNodes()->defaultOrder()->first($columns);
371
    }
372
373
    /**
374
     * Returns node that is before current node without constraining to siblings.
375
     * This can be either a prev sibling or parent node.
376
     *
377
     * @param  array  $columns
378
     *
379
     * @return self
380
     */
381 3
    public function getPrevNode(array $columns = ['*'])
382
    {
383 3
        return $this->prevNodes()->defaultOrder('desc')->first($columns);
384
    }
385
386
    /**
387
     * Get the ancestors nodes.
388
     *
389
     * @param  array  $columns
390
     *
391
     * @return \Arcanedev\LaravelNestedSet\Eloquent\Collection
392
     */
393 6
    public function getAncestors(array $columns = ['*'])
394
    {
395 6
        return $this->newScopedQuery()
396 6
            ->defaultOrder()
397 6
            ->ancestorsOf($this, $columns);
398
    }
399
400
    /**
401
     * Get the descendants nodes.
402
     *
403
     * @param  array  $columns
404
     *
405
     * @return \Arcanedev\LaravelNestedSet\Eloquent\Collection
406
     */
407 9
    public function getDescendants(array $columns = ['*'])
408
    {
409 9
        return $this->descendants()->get($columns);
410
    }
411
412
    /**
413
     * Get the siblings nodes.
414
     *
415
     * @param  array  $columns
416
     *
417
     * @return \Arcanedev\LaravelNestedSet\Eloquent\Collection
418
     */
419 6
    public function getSiblings(array $columns = ['*'])
420
    {
421 6
        return $this->siblings()->get($columns);
422
    }
423
424
    /**
425
     * Get the next siblings nodes.
426
     *
427
     * @param  array  $columns
428
     *
429
     * @return \Arcanedev\LaravelNestedSet\Eloquent\Collection
430
     */
431 6
    public function getNextSiblings(array $columns = ['*'])
432
    {
433 6
        return $this->nextSiblings()->get($columns);
434
    }
435
436
    /**
437
     * Get the previous siblings nodes.
438
     *
439
     * @param  array  $columns
440
     *
441
     * @return \Arcanedev\LaravelNestedSet\Eloquent\Collection
442
     */
443 6
    public function getPrevSiblings(array $columns = ['*'])
444
    {
445 6
        return $this->prevSiblings()->get($columns);
446
    }
447
448
    /**
449
     * Get the next sibling node.
450
     *
451
     * @param  array  $columns
452
     *
453
     * @return self
454
     */
455 3
    public function getNextSibling(array $columns = ['*'])
456
    {
457 3
        return $this->nextSiblings()->defaultOrder()->first($columns);
458
    }
459
460
    /**
461
     * Get the previous sibling node.
462
     *
463
     * @param  array  $columns
464
     *
465
     * @return self
466
     */
467 3
    public function getPrevSibling(array $columns = ['*'])
468
    {
469 3
        return $this->prevSiblings()->defaultOrder('desc')->first($columns);
470
    }
471
472
    /**
473
     * Get node height (rgt - lft + 1).
474
     *
475
     * @return int
476
     */
477 27
    public function getNodeHeight()
478
    {
479 27
        if ( ! $this->exists) return 2;
480
481 3
        return $this->getRgt() - $this->getLft() + 1;
482
    }
483
484
    /**
485
     * Get number of descendant nodes.
486
     *
487
     * @return int
488
     */
489 3
    public function getDescendantCount()
490
    {
491 3
        return (int) ceil($this->getNodeHeight() / 2) - 1;
492
    }
493
494
    /**
495
     * Set raw node.
496
     *
497
     * @param  int  $lft
498
     * @param  int  $rgt
499
     * @param  int  $parentId
500
     *
501
     * @return self
502
     */
503 9
    public function rawNode($lft, $rgt, $parentId)
504
    {
505 9
        $this->setLft($lft)->setRgt($rgt)->setParentId($parentId);
506
507 9
        return $this->setNodeAction('raw');
508
    }
509
510
    /**
511
     * Set an action.
512
     *
513
     * @param  string  $action
514
     *
515
     * @return self
516
     */
517 90
    protected function setNodeAction($action)
518
    {
519 90
        $this->pending = func_get_args();
520 90
        unset($action);
521
522 90
        return $this;
523
    }
524
525
    /* ------------------------------------------------------------------------------------------------
526
     |  Other Functions
527
     | ------------------------------------------------------------------------------------------------
528
     */
529
    /**
530
     * Get the lower bound.
531
     *
532
     * @return int
533
     */
534 24
    protected function getLowerBound()
535
    {
536 24
        return (int) $this->newNestedSetQuery()->max($this->getRgtName());
0 ignored issues
show
Documentation Bug introduced by
The method max does not exist on object<Arcanedev\Laravel...\Eloquent\QueryBuilder>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
537
    }
538
539
    /**
540
     * Call pending action.
541
     */
542 75
    protected function callPendingAction()
543
    {
544 75
        $this->moved = false;
545
546 75
        if ( ! $this->pending && ! $this->exists) {
547 15
            $this->makeRoot();
548 5
        }
549
550 75
        if ( ! $this->pending) return;
551
552 72
        $method        = 'action'.ucfirst(array_shift($this->pending));
553 72
        $parameters    = $this->pending;
554
555 72
        $this->pending = null;
556 72
        $this->moved   = call_user_func_array([$this, $method], $parameters);
557 72
    }
558
559
    /**
560
     * @return bool
561
     */
562 9
    protected function actionRaw()
563
    {
564 9
        return true;
565
    }
566
567
    /**
568
     * Make a root node.
569
     */
570 24
    protected function actionRoot()
571
    {
572
        // Simplest case that do not affect other nodes.
573 24
        if ( ! $this->exists) {
574 15
            $cut = $this->getLowerBound() + 1;
575
576 15
            $this->setLft($cut);
577 15
            $this->setRgt($cut + 1);
578
579 15
            return true;
580
        }
581
582 9
        if ($this->isRoot()) return false;
583
584
585
        // Reset parent object
586 9
        $this->setParent(null);
587
588 9
        return $this->insertAt($this->getLowerBound() + 1);
589
    }
590
591
    /**
592
     * Append or prepend a node to the parent.
593
     *
594
     * @param  \Arcanedev\LaravelNestedSet\Contracts\Nodeable  $parent
595
     * @param  bool                                            $prepend
596
     *
597
     * @return bool
598
     */
599 24
    protected function actionAppendOrPrepend(Nodeable $parent, $prepend = false)
600
    {
601 24
        $parent->refreshNode();
602
603 24
        $cut = $prepend ? $parent->getLft() + 1 : $parent->getRgt();
604
605 24
        if ( ! $this->insertAt($cut)) {
606
            return false;
607
        }
608
609 24
        $parent->refreshNode();
610
611 24
        return true;
612
    }
613
614
    /**
615
     * Insert node before or after another node.
616
     *
617
     * @param  self  $node
0 ignored issues
show
introduced by
The type NodeTrait for parameter $node is a trait, and thus cannot be used for type-hinting in PHP. Maybe consider adding an interface and use that for type-hinting?
Loading history...
618
     * @param  bool  $after
619
     *
620
     * @return bool
621
     */
622 21
    protected function actionBeforeOrAfter(self $node, $after = false)
623
    {
624 21
        $node->refreshNode();
625
626 21
        return $this->insertAt($after ? $node->getRgt() + 1 : $node->getLft());
627
    }
628
629
    /**
630
     * Refresh node's crucial attributes.
631
     */
632 66
    public function refreshNode()
633
    {
634 66
        if ( ! $this->exists || static::$actionsPerformed === 0) return;
635
636 48
        $attributes = $this->newNestedSetQuery()->getNodeData($this->getKey());
637
638 48
        $this->attributes = array_merge($this->attributes, $attributes);
639 48
    }
640
641
    /**
642
     * Get query for siblings after the node.
643
     *
644
     * @return \Arcanedev\LaravelNestedSet\Eloquent\QueryBuilder
645
     */
646 18
    public function nextSiblings()
647
    {
648 18
        return $this->nextNodes()
649 18
            ->where($this->getParentIdName(), '=', $this->getParentId());
650
    }
651
652
    /**
653
     * Get query for siblings before the node.
654
     *
655
     * @return \Arcanedev\LaravelNestedSet\Eloquent\QueryBuilder
656
     */
657 12
    public function prevSiblings()
658
    {
659 12
        return $this->prevNodes()
660 12
            ->where($this->getParentIdName(), '=', $this->getParentId());
661
    }
662
663
    /**
664
     * Get query for nodes after current node.
665
     *
666
     * @return \Arcanedev\LaravelNestedSet\Eloquent\QueryBuilder
667
     */
668 21
    public function nextNodes()
669
    {
670 21
        return $this->newScopedQuery()
671 21
            ->where($this->getLftName(), '>', $this->getLft());
672
    }
673
674
    /**
675
     * Get query for nodes before current node in reversed order.
676
     *
677
     * @return \Arcanedev\LaravelNestedSet\Eloquent\QueryBuilder
678
     */
679 15
    public function prevNodes()
680
    {
681 15
        return $this->newScopedQuery()
682 15
            ->where($this->getLftName(), '<', $this->getLft());
683
    }
684
685
    /**
686
     * Get query for ancestors to the node not including the node itself.
687
     *
688
     * @return \Arcanedev\LaravelNestedSet\Eloquent\QueryBuilder
689
     */
690 3
    public function ancestors()
691
    {
692 3
        return $this->newScopedQuery()
693 3
            ->whereAncestorOf($this)->defaultOrder();
694
    }
695
696
    /**
697
     * Make this node a root node.
698
     *
699
     * @return self
700
     */
701 36
    public function makeRoot()
702
    {
703 36
        return $this->setNodeAction('root');
704
    }
705
706
    /**
707
     * Save node as root.
708
     *
709
     * @return bool
710
     */
711 6
    public function saveAsRoot()
712
    {
713 6
        if ($this->exists && $this->isRoot()) {
714
            return true;
715
        }
716
717 6
        return $this->makeRoot()->save();
718
    }
719
720
    /**
721
     * Append and save a node.
722
     *
723
     * @param  \Arcanedev\LaravelNestedSet\Contracts\Nodeable  $node
724
     *
725
     * @return bool
726
     */
727 12
    public function appendNode(Nodeable $node)
728
    {
729 12
        return $node->appendToNode($this)->save();
730
    }
731
732
    /**
733
     * Prepend and save a node.
734
     *
735
     * @param  \Arcanedev\LaravelNestedSet\Contracts\Nodeable  $node
736
     *
737
     * @return bool
738
     */
739 3
    public function prependNode(Nodeable $node)
740
    {
741 3
        return $node->prependToNode($this)->save();
742
    }
743
744
    /**
745
     * Append a node to the new parent.
746
     *
747
     * @param  \Arcanedev\LaravelNestedSet\Contracts\Nodeable  $parent
748
     *
749
     * @return self
750
     */
751 33
    public function appendToNode(Nodeable $parent)
752
    {
753 33
        return $this->appendOrPrependTo($parent);
754
    }
755
756
    /**
757
     * Prepend a node to the new parent.
758
     *
759
     * @param  \Arcanedev\LaravelNestedSet\Contracts\Nodeable  $parent
760
     *
761
     * @return \Arcanedev\LaravelNestedSet\Contracts\Nodeable
762
     */
763 6
    public function prependToNode(Nodeable $parent)
764
    {
765 6
        return $this->appendOrPrependTo($parent, true);
766
    }
767
768
    /**
769
     * Append or prepend a node to parent.
770
     *
771
     * @param  \Arcanedev\LaravelNestedSet\Contracts\Nodeable  $parent
772
     * @param  bool                                            $prepend
773
     *
774
     * @return \Arcanedev\LaravelNestedSet\Contracts\Nodeable
775
     */
776 39
    public function appendOrPrependTo(Nodeable $parent, $prepend = false)
777
    {
778 39
        $this->assertNodeExists($parent)
779 36
             ->assertNotDescendant($parent);
780
781 30
        $this->setParent($parent)->dirtyBounds();
782
783 30
        return $this->setNodeAction('appendOrPrepend', $parent, $prepend);
784
    }
785
786
    /**
787
     * Insert self after a node.
788
     *
789
     * @param  \Arcanedev\LaravelNestedSet\Contracts\Nodeable  $node
790
     *
791
     * @return \Arcanedev\LaravelNestedSet\Contracts\Nodeable
792
     */
793 21
    public function afterNode(Nodeable $node)
794
    {
795 21
        return $this->beforeOrAfterNode($node, true);
796
    }
797
798
    /**
799
     * Insert self before node.
800
     *
801
     * @param  \Arcanedev\LaravelNestedSet\Contracts\Nodeable  $node
802
     *
803
     * @return \Arcanedev\LaravelNestedSet\Contracts\Nodeable
804
     */
805 6
    public function beforeNode(Nodeable $node)
806
    {
807 6
        return $this->beforeOrAfterNode($node);
808
    }
809
810
    /**
811
     * Set before or after a node.
812
     *
813
     * @param  \Arcanedev\LaravelNestedSet\Contracts\Nodeable  $node
814
     * @param  bool                                            $after
815
     *
816
     * @return \Arcanedev\LaravelNestedSet\Contracts\Nodeable
817
     */
818 27
    public function beforeOrAfterNode(Nodeable $node, $after = false)
819
    {
820 27
        $this->assertNodeExists($node)->assertNotDescendant($node);
821
822 24
        if ( ! $this->isSiblingOf($node)) {
823 9
            $this->setParent($node->getRelationValue('parent'));
824 3
        }
825
826 24
        $this->dirtyBounds();
827
828 24
        return $this->setNodeAction('beforeOrAfter', $node, $after);
829
    }
830
831
    /**
832
     * Insert after a node and save.
833
     *
834
     * @param  \Arcanedev\LaravelNestedSet\Contracts\Nodeable  $node
835
     *
836
     * @return bool
837
     */
838 12
    public function insertAfterNode(Nodeable $node)
839
    {
840 12
        return $this->afterNode($node)->save();
841
    }
842
843
    /**
844
     * Insert self before a node and save.
845
     *
846
     * @param  \Arcanedev\LaravelNestedSet\Contracts\Nodeable  $node
847
     *
848
     * @return bool
849
     */
850 3
    public function insertBeforeNode(Nodeable $node)
851
    {
852 3
        if ( ! $this->beforeNode($node)->save()) return false;
853
854
        // We'll update the target node since it will be moved
855 3
        $node->refreshNode();
856
857 3
        return true;
858
    }
859
860
    /**
861
     * Move node up given amount of positions.
862
     *
863
     * @param  int  $amount
864
     *
865
     * @return bool
866
     */
867 3
    public function up($amount = 1)
868
    {
869 3
        $sibling = $this->prevSiblings()
0 ignored issues
show
Documentation Bug introduced by
The method skip does not exist on object<Arcanedev\Laravel...\Eloquent\QueryBuilder>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
870 3
                        ->defaultOrder('desc')
871 3
                        ->skip($amount - 1)
872 3
                        ->first();
873
874 3
        if ( ! $sibling) return false;
875
876 3
        return $this->insertBeforeNode($sibling);
877
    }
878
879
    /**
880
     * Move node down given amount of positions.
881
     *
882
     * @param  int  $amount
883
     *
884
     * @return bool
885
     */
886 12
    public function down($amount = 1)
887
    {
888 12
        $sibling = $this->nextSiblings()
0 ignored issues
show
Documentation Bug introduced by
The method skip does not exist on object<Arcanedev\Laravel...\Eloquent\QueryBuilder>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
889 12
                        ->defaultOrder()
890 12
                        ->skip($amount - 1)
891 12
                        ->first();
892
893 12
        if ( ! $sibling) return false;
894
895 12
        return $this->insertAfterNode($sibling);
896
    }
897
898
    /**
899
     * Insert node at specific position.
900
     *
901
     * @param  int  $position
902
     *
903
     * @return bool
904
     */
905 51
    protected function insertAt($position)
906
    {
907 51
        ++static::$actionsPerformed;
908
909 51
        $result = $this->exists
910 37
            ? $this->moveNode($position)
911 51
            : $this->insertNode($position);
912
913 51
        return $result;
914
    }
915
916
    /**
917
     * Move a node to the new position.
918
     *
919
     * @param  int  $position
920
     *
921
     * @return int
922
     */
923 30
    protected function moveNode($position)
924
    {
925 30
        $updated = $this->newNestedSetQuery()
926 30
                        ->moveNode($this->getKey(), $position) > 0;
927
928 30
        if ($updated) $this->refreshNode();
929
930 30
        return $updated;
931
    }
932
933
    /**
934
     * Insert new node at specified position.
935
     *
936
     * @param  int  $position
937
     *
938
     * @return bool
939
     */
940 24
    protected function insertNode($position)
941
    {
942 24
        $this->newNestedSetQuery()->makeGap($position, 2);
943
944 24
        $height = $this->getNodeHeight();
945
946 24
        $this->setLft($position);
947 24
        $this->setRgt($position + $height - 1);
948
949 24
        return true;
950
    }
951
952
    /**
953
     * Update the tree when the node is removed physically.
954
     */
955 15
    protected function deleteDescendants()
956
    {
957 15
        $lft = $this->getLft();
958 15
        $rgt = $this->getRgt();
959
960 15
        $method = ($this->usesSoftDelete() && $this->forceDeleting)
961 11
            ? 'forceDelete'
962 15
            : 'delete';
963
964 15
        $this->descendants()->{$method}();
965
966 15
        if ($this->hardDeleting()) {
967 12
            $height = $rgt - $lft + 1;
968
969 12
            $this->newNestedSetQuery()->makeGap($rgt + 1, -$height);
970
971
            // In case if user wants to re-create the node
972 12
            $this->makeRoot();
973
974 12
            static::$actionsPerformed++;
975 4
        }
976 15
    }
977
978
    /**
979
     * Restore the descendants.
980
     *
981
     * @param  \Carbon\Carbon  $deletedAt
982
     */
983 3
    protected function restoreDescendants($deletedAt)
984
    {
985 3
        $this->descendants()
0 ignored issues
show
Documentation Bug introduced by
The method where does not exist on object<Arcanedev\Laravel...nt\DescendantsRelation>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
986 3
            ->where($this->getDeletedAtColumn(), '>=', $deletedAt)
0 ignored issues
show
Bug introduced by
It seems like getDeletedAtColumn() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
987 3
            ->applyScopes()
988 3
            ->restore();
989 3
    }
990
991
    /**
992
     * Get a new base query that includes deleted nodes.
993
     *
994
     * @param  string|null $table
995
     *
996
     * @return \Arcanedev\LaravelNestedSet\Eloquent\QueryBuilder
997
     */
998 90
    public function newNestedSetQuery($table = null)
999
    {
1000 90
        $builder = $this->usesSoftDelete()
1001 78
            ? $this->withTrashed()
0 ignored issues
show
Bug introduced by
It seems like withTrashed() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
1002 90
            : $this->newQuery();
1003
1004 90
        return $this->applyNestedSetScope($builder, $table);
1005
    }
1006
1007
    /**
1008
     * Create a new scoped query.
1009
     *
1010
     * @param  string|null  $table
1011
     *
1012
     * @return \Arcanedev\LaravelNestedSet\Eloquent\QueryBuilder
1013
     */
1014 105
    public function newScopedQuery($table = null)
1015
    {
1016 105
        return $this->applyNestedSetScope($this->newQuery(), $table);
1017
    }
1018
1019
    /**
1020
     * Apply the nested set scope.
1021
     *
1022
     * @param  \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder  $query
1023
     * @param  string                                                                    $table
1024
     *
1025
     * @return \Arcanedev\LaravelNestedSet\Eloquent\QueryBuilder|\Illuminate\Database\Query\Builder
1026
     */
1027 156
    public function applyNestedSetScope($query, $table = null)
1028
    {
1029 156
        if ( ! $scoped = $this->getScopeAttributes()) {
1030 123
            return $query;
1031
        }
1032
1033 33
        if ($table === null) {
1034 33
            $table = $this->getTable();
1035 11
        }
1036
1037 33
        foreach ($scoped as $attribute) {
1038 33
            $query->where("$table.$attribute", '=', $this->getAttributeValue($attribute));
1039 11
        }
1040
1041 33
        return $query;
1042
    }
1043
1044
    /**
1045
     * @param  array  $attributes
1046
     *
1047
     * @return self
1048
     */
1049 9
    public static function scoped(array $attributes)
1050
    {
1051 9
        $instance = new static;
1052
1053 9
        $instance->setRawAttributes($attributes);
1054
1055 9
        return $instance->newScopedQuery();
1056
    }
1057
1058
    /**
1059
     * Save a new model and return the instance.
1060
     *
1061
     * Use `children` key on `$attributes` to create child nodes.
1062
     *
1063
     * @param  array  $attributes
1064
     * @param  self   $parent
0 ignored issues
show
introduced by
The type NodeTrait for parameter $parent is a trait, and thus cannot be used for type-hinting in PHP. Maybe consider adding an interface and use that for type-hinting?
Loading history...
1065
     *
1066
     * @return self
1067
     */
1068 9
    public static function create(array $attributes = [], self $parent = null)
1069
    {
1070 9
        $children = array_pull($attributes, 'children');
1071 9
        $instance = new static($attributes);
0 ignored issues
show
Unused Code introduced by
The call to NodeTrait::__construct() has too many arguments starting with $attributes.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1072
1073 9
        if ($parent) {
1074 3
            $instance->appendToNode($parent);
1075 1
        }
1076
1077 9
        $instance->save();
1078
1079
        // Now create children
1080 9
        $relation = new EloquentCollection;
1081
1082 9
        foreach ((array) $children as $child) {
1083 3
            $relation->add($child = static::create($child, $instance));
1084
1085 3
            $child->setRelation('parent', $instance);
1086 3
        }
1087
1088 9
        return $instance->setRelation('children', $relation);
1089
    }
1090
1091
    /* ------------------------------------------------------------------------------------------------
1092
     |  Check Functions
1093
     | ------------------------------------------------------------------------------------------------
1094
     */
1095
    /**
1096
     * Get whether node is root.
1097
     *
1098
     * @return bool
1099
     */
1100 12
    public function isRoot()
1101
    {
1102 12
        return is_null($this->getParentId());
1103
    }
1104
1105
    /**
1106
     * Get whether a node is a descendant of other node.
1107
     *
1108
     * @param  \Arcanedev\LaravelNestedSet\Contracts\Nodeable  $node
1109
     *
1110
     * @return bool
1111
     */
1112 57
    public function isDescendantOf(Nodeable $node)
1113
    {
1114
        return (
1115 57
            $this->getLft() > $node->getLft() &&
1116 55
            $this->getLft() < $node->getRgt()
1117 19
        );
1118
    }
1119
1120
    /**
1121
     * Get whether the node is immediate children of other node.
1122
     *
1123
     * @param  \Arcanedev\LaravelNestedSet\Contracts\Nodeable  $node
1124
     *
1125
     * @return bool
1126
     */
1127 3
    public function isChildOf(Nodeable $node)
1128
    {
1129 3
        return $this->getParentId() == $node->getKey();
1130
    }
1131
1132
    /**
1133
     * Get whether the node is a sibling of another node.
1134
     *
1135
     * @param  \Arcanedev\LaravelNestedSet\Contracts\Nodeable  $node
1136
     *
1137
     * @return bool
1138
     */
1139 24
    public function isSiblingOf(Nodeable $node)
1140
    {
1141 24
        return $this->getParentId() == $node->getParentId();
1142
    }
1143
1144
    /**
1145
     * Get whether the node is an ancestor of other node, including immediate parent.
1146
     *
1147
     * @param  \Arcanedev\LaravelNestedSet\Contracts\Nodeable  $node
1148
     *
1149
     * @return bool
1150
     */
1151 3
    public function isAncestorOf(Nodeable $node)
1152
    {
1153
        /** @var \Arcanedev\LaravelNestedSet\Contracts\Nodeable $this */
1154 3
        return $node->isDescendantOf($this);
1155
    }
1156
1157
    /**
1158
     * Get whether the node has moved since last save.
1159
     *
1160
     * @return bool
1161
     */
1162 18
    public function hasMoved()
1163
    {
1164 18
        return $this->moved;
1165
    }
1166
1167
    /* ------------------------------------------------------------------------------------------------
1168
     |  Assertion Functions
1169
     | ------------------------------------------------------------------------------------------------
1170
     */
1171
    /**
1172
     * Assert that the node is not a descendant.
1173
     *
1174
     * @param  \Arcanedev\LaravelNestedSet\Contracts\Nodeable  $node
1175
     *
1176
     * @return self
1177
     *
1178
     * @throws \LogicException
1179
     */
1180 60
    protected function assertNotDescendant(Nodeable $node)
1181
    {
1182
        /** @var \Arcanedev\LaravelNestedSet\Contracts\Nodeable $this */
1183 60
        if ($node == $this || $node->isDescendantOf($this)) {
1184 9
            throw new LogicException('Node must not be a descendant.');
1185
        }
1186
1187 51
        return $this;
1188
    }
1189
1190
    /**
1191
     * Assert node exists.
1192
     *
1193
     * @param  \Arcanedev\LaravelNestedSet\Contracts\Nodeable  $node
1194
     *
1195
     * @return self
1196
     *
1197
     * @throws \LogicException
1198
     */
1199 63
    protected function assertNodeExists(Nodeable $node)
1200
    {
1201 63
        if ( ! $node->getLft() || ! $node->getRgt()) {
1202 3
            throw new LogicException('Node must exists.');
1203
        }
1204
1205 60
        return $this;
1206
    }
1207
}
1208