Completed
Push — master ( 6cb9bf...d960fb )
by ARCANEDEV
07:01
created

NodeTrait   D

Complexity

Total Complexity 113

Size/Duplication

Total Lines 1155
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Test Coverage

Coverage 98.68%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 1155
wmc 113
lcom 1
cbo 9
ccs 299
cts 303
cp 0.9868
rs 4.4102

76 Methods

Rating   Name   Duplication   Size   Complexity  
B bootNodeTrait() 0 45 2
A parent() 0 5 1
A children() 0 5 1
A descendants() 0 4 1
A siblings() 0 6 1
A getLftName() 0 4 1
A getRgtName() 0 4 1
A getParentIdName() 0 4 1
A getLft() 0 4 1
A setLft() 0 6 1
A getRgt() 0 4 1
A setRgt() 0 6 1
A getParentId() 0 4 1
A setParentId() 0 6 1
A setParent() 0 7 2
A setParentIdAttribute() 0 13 3
A getBounds() 0 4 1
A getScopeAttributes() 0 4 1
A dirtyBounds() 0 4 1
A getNextNode() 0 4 1
A getPrevNode() 0 4 1
A getAncestors() 0 6 1
A getDescendants() 0 4 1
A getSiblings() 0 4 1
A getNextSiblings() 0 4 1
A getPrevSiblings() 0 4 1
A getNextSibling() 0 4 1
A getPrevSibling() 0 4 1
A getNodeHeight() 0 6 2
A getDescendantCount() 0 4 1
A rawNode() 0 6 1
A setNodeAction() 0 7 1
A getLowerBound() 0 4 1
A callPendingAction() 0 16 4
A actionRaw() 0 4 1
A actionRoot() 0 20 3
A actionAppendOrPrepend() 0 14 3
A actionBeforeOrAfter() 0 6 2
A refreshNode() 0 9 3
A nextSiblings() 0 5 1
A prevSiblings() 0 5 1
A nextNodes() 0 5 1
A prevNodes() 0 5 1
A ancestors() 0 5 1
A makeRoot() 0 4 1
A saveAsRoot() 0 8 3
A appendNode() 0 4 1
A prependNode() 0 4 1
A appendToNode() 0 4 1
A prependToNode() 0 4 1
A appendOrPrependTo() 0 9 1
A afterNode() 0 4 1
A beforeNode() 0 4 1
A beforeOrAfterNode() 0 12 2
A insertAfterNode() 0 4 1
A insertBeforeNode() 0 9 2
A up() 0 11 2
A down() 0 11 2
A insertAt() 0 10 2
A moveNode() 0 9 2
A insertNode() 0 11 1
B deleteDescendants() 0 22 4
A restoreDescendants() 0 7 1
A newNestedSetQuery() 0 8 2
A newScopedQuery() 0 4 1
A applyNestedSetScope() 0 16 4
A scoped() 0 8 1
A create() 0 22 3
A isRoot() 0 4 1
A isDescendantOf() 0 7 2
A isChildOf() 0 4 1
A isSiblingOf() 0 4 1
A isAncestorOf() 0 5 1
A hasMoved() 0 4 1
A assertNotDescendant() 0 9 3
A assertNodeExists() 0 8 3

How to fix   Complexity   

Complex Class

Complex classes like NodeTrait often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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

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

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) {
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...
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) {
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...
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()};
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...
121 240
            });
122
123 240
            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...
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  \Illuminate\Database\Eloquent\Model|null  $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);
0 ignored issues
show
Documentation Bug introduced by
The method get 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...
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());
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...
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);
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
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...
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();
0 ignored issues
show
Documentation introduced by
$parent is of type object<Arcanedev\Laravel...Set\Contracts\Nodeable>, but the function expects a object<Illuminate\Database\Eloquent\Model>|null.

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...
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'));
0 ignored issues
show
Bug introduced by
The method getRelationValue() does not seem to exist on object<Arcanedev\Laravel...Set\Contracts\Nodeable>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
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()
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...
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()
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...
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()
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...
983 4
            ->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...
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()
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...
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
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...
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);
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...
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);
0 ignored issues
show
Documentation introduced by
$this is of type object<Arcanedev\Laravel...Set\Contracts\Nodeable>, but the function expects a object<self>.

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...
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)) {
0 ignored issues
show
Documentation introduced by
$this is of type object<Arcanedev\Laravel...Set\Contracts\Nodeable>, but the function expects a object<self>.

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...
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