Completed
Push — master ( 9383fe...482732 )
by ARCANEDEV
08:20
created

QueryBuilder::withoutRoot()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 9.4285
cc 1
eloc 3
nc 1
nop 0
crap 1
1
<?php namespace Arcanedev\LaravelNestedSet\Eloquent;
2
3
use Arcanedev\LaravelNestedSet\Utilities\NestedSet;
4
use Arcanedev\LaravelNestedSet\Traits\NodeTrait;
5
use Arcanedev\LaravelNestedSet\Utilities\TreeHelper;
6
use Illuminate\Database\Eloquent\Builder;
7
use Illuminate\Database\Eloquent\Model;
8
use Illuminate\Database\Eloquent\ModelNotFoundException;
9
use Illuminate\Database\Query\Builder as Query;
10
use Illuminate\Database\Query\Expression;
11
use LogicException;
12
13
/**
14
 * Class     QueryBuilder
15
 *
16
 * @package  Arcanedev\LaravelNestedSet\Eloquent
17
 * @author   ARCANEDEV <[email protected]>
18
 */
19
class QueryBuilder extends Builder
20
{
21
    /* ------------------------------------------------------------------------------------------------
22
     |  Properties
23
     | ------------------------------------------------------------------------------------------------
24
     */
25
    /**
26
     * @var \Arcanedev\LaravelNestedSet\Traits\NodeTrait|\Illuminate\Database\Eloquent\Model
27
     */
28
    protected $model;
29
30
    /* ------------------------------------------------------------------------------------------------
31
     |  Main Functions
32
     | ------------------------------------------------------------------------------------------------
33
     */
34
    /**
35
     * Get node's `lft` and `rgt` values.
36
     *
37
     * @param  mixed  $id
38
     * @param  bool   $required
39
     *
40
     * @return array
41
     */
42 73
    public function getNodeData($id, $required = false)
43
    {
44 73
        $query = $this->toBase();
45
46 73
        $query->where($this->model->getKeyName(), '=', $id);
47
48 73
        $data = $query->first([
49 73
            $this->model->getLftName(),
50 73
            $this->model->getRgtName(),
51 57
        ]);
52
53 73
        if ( ! $data && $required) {
54 4
            throw new ModelNotFoundException;
55
        }
56
57 69
        return (array) $data;
58
    }
59
60
    /**
61
     * Get plain node data.
62
     *
63
     * @param  mixed  $id
64
     * @param  bool   $required
65
     *
66
     * @return array
67
     */
68 45
    public function getPlainNodeData($id, $required = false)
69
    {
70 45
        return array_values($this->getNodeData($id, $required));
71
    }
72
73
    /**
74
     * Scope limits query to select just root node.
75
     *
76
     * @return self
77
     */
78 20
    public function whereIsRoot()
79
    {
80 20
        $this->query->whereNull($this->model->getParentIdName());
81
82 20
        return $this;
83
    }
84
85
    /**
86
     * Limit results to ancestors of specified node.
87
     *
88
     * @param  mixed  $id
89
     *
90
     * @return self
91
     */
92 20
    public function whereAncestorOf($id)
93
    {
94 20
        $keyName = $this->model->getKeyName();
95
96 20
        if (NestedSet::isNode($id)) {
97 16
            $value = '?';
98
99 16
            $this->query->addBinding($id->getLft());
100
101 16
            $id = $id->getKey();
102 12
        } else {
103 4
            $valueQuery = $this->model
104 4
                ->newQuery()
105 4
                ->toBase()
106 4
                ->select("_.".$this->model->getLftName())
107 4
                ->from($this->model->getTable().' as _')
108 4
                ->where($keyName, '=', $id)
109 4
                ->limit(1);
110
111 4
            $this->query->mergeBindings($valueQuery);
112
113 4
            $value = '(' . $valueQuery->toSql() . ')';
114
        }
115
116 20
        list($lft, $rgt) = $this->wrappedColumns();
117
118 20
        $this->query->whereRaw("{$value} between {$lft} and {$rgt}");
119
120
        // Exclude the node
121 20
        $this->where($keyName, '<>', $id);
122
123 20
        return $this;
124
    }
125
126
    /**
127
     * Get ancestors of specified node.
128
     *
129
     * @param  mixed  $id
130
     * @param  array  $columns
131
     *
132
     * @return self
133
     */
134 12
    public function ancestorsOf($id, array $columns = ['*'])
135
    {
136 12
        return $this->whereAncestorOf($id)->get($columns);
137
    }
138
139
    /**
140
     * Add node selection statement between specified range.
141
     *
142
     * @param  array   $values
143
     * @param  string  $boolean
144
     * @param  bool    $not
145
     *
146
     * @return self
147
     */
148 43
    public function whereNodeBetween($values, $boolean = 'and', $not = false)
149
    {
150 43
        $this->query->whereBetween($this->model->getLftName(), $values, $boolean, $not);
151
152 43
        return $this;
153
    }
154
155
    /**
156
     * Add node selection statement between specified range joined with `or` operator.
157
     *
158
     * @param  array  $values
159
     *
160
     * @return self
161
     */
162
    public function orWhereNodeBetween($values)
163
    {
164
        return $this->whereNodeBetween($values, 'or');
165
    }
166
167
    /**
168
     * Add constraint statement to descendants of specified node.
169
     *
170
     * @param  mixed   $id
171
     * @param  string  $boolean
172
     * @param  bool    $not
173
     *
174
     * @return self
175
     */
176 47
    public function whereDescendantOf($id, $boolean = 'and', $not = false)
177
    {
178 47
        $data = NestedSet::isNode($id)
179 46
            ? $id->getBounds()
180 47
            : $this->model->newNestedSetQuery()->getPlainNodeData($id, true);
181
182
        // Don't include the node
183 43
        ++$data[0];
184
185 43
        return $this->whereNodeBetween($data, $boolean, $not);
186
    }
187
188
    /**
189
     * @param  mixed  $id
190
     *
191
     * @return self
192
     */
193
    public function whereNotDescendantOf($id)
194
    {
195
        return $this->whereDescendantOf($id, 'and', true);
196
    }
197
198
    /**
199
     * @param  mixed  $id
200
     *
201
     * @return self
202
     */
203 4
    public function orWhereDescendantOf($id)
204
    {
205 4
        return $this->whereDescendantOf($id, 'or');
206
    }
207
208
    /**
209
     * @param  mixed  $id
210
     *
211
     * @return self
212
     */
213
    public function orWhereNotDescendantOf($id)
214
    {
215
        return $this->whereDescendantOf($id, 'or', true);
216
    }
217
218
    /**
219
     * Get descendants of specified node.
220
     *
221
     * @param  mixed  $id
222
     * @param  array  $columns
223
     *
224
     * @return \Arcanedev\LaravelNestedSet\Eloquent\Collection
225
     */
226
    public function descendantsOf($id, array $columns = ['*'])
227
    {
228
        try {
229
            return $this->whereDescendantOf($id)->get($columns);
230
        }
231
        catch (ModelNotFoundException $e) {
0 ignored issues
show
Bug introduced by
The class Illuminate\Database\Eloq...\ModelNotFoundException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
232
            return $this->model->newCollection();
233
        }
234
    }
235
236
    /**
237
     * @param  mixed   $id
238
     * @param  string  $operator
239
     * @param  string  $boolean
240
     *
241
     * @return self
242
     */
243
    protected function whereIsBeforeOrAfter($id, $operator, $boolean)
244
    {
245
        if (NestedSet::isNode($id)) {
246
            $value = '?';
247
248
            $this->query->addBinding($id->getLft());
249
        } else {
250
            $valueQuery = $this->model
251
                ->newQuery()
252
                ->toBase()
253
                ->select('_n.'.$this->model->getLftName())
254
                ->from($this->model->getTable().' as _n')
255
                ->where('_n.'.$this->model->getKeyName(), '=', $id);
256
257
            $this->query->mergeBindings($valueQuery);
258
259
            $value = '('.$valueQuery->toSql().')';
260
        }
261
262
        list($lft,) = $this->wrappedColumns();
263
264
        $this->query->whereRaw("{$lft} {$operator} {$value}", [ ], $boolean);
265
266
        return $this;
267
    }
268
269
    /**
270
     * Constraint nodes to those that are after specified node.
271
     *
272
     * @param  mixed   $id
273
     * @param  string  $boolean
274
     *
275
     * @return self
276
     */
277
    public function whereIsAfter($id, $boolean = 'and')
278
    {
279
        return $this->whereIsBeforeOrAfter($id, '>', $boolean);
280
    }
281
282
    /**
283
     * Constraint nodes to those that are before specified node.
284
     *
285
     * @param  mixed   $id
286
     * @param  string  $boolean
287
     *
288
     * @return self
289
     */
290
    public function whereIsBefore($id, $boolean = 'and')
291
    {
292
        return $this->whereIsBeforeOrAfter($id, '<', $boolean);
293
    }
294
295
    /**
296
     * Include depth level into the result.
297
     *
298
     * @param  string  $as
299
     *
300
     * @return self
301
     */
302 16
    public function withDepth($as = 'depth')
303
    {
304 16
        if ($this->query->columns === null) {
305 16
            $this->query->columns = ['*'];
306 12
        }
307
308 16
        $table = $this->wrappedTable();
309
310 16
        list($lft, $rgt) = $this->wrappedColumns();
311
312 16
        $query = $this->model
313 16
            ->newScopedQuery('_d')
314 16
            ->toBase()
315 16
            ->selectRaw('count(1) - 1')
316 16
            ->from($this->model->getTable().' as _d')
317 16
            ->whereRaw("{$table}.{$lft} between _d.{$lft} and _d.{$rgt}");
318
319 16
        $this->query->selectSub($query, $as);
320
321 16
        return $this;
322
    }
323
324
    /**
325
     * Get wrapped `lft` and `rgt` column names.
326
     *
327
     * @return array
328
     */
329 48
    protected function wrappedColumns()
330
    {
331 48
        $grammar = $this->query->getGrammar();
332
333
        return [
334 48
            $grammar->wrap($this->model->getLftName()),
335 48
            $grammar->wrap($this->model->getRgtName()),
336 36
        ];
337
    }
338
339
    /**
340
     * Get a wrapped table name.
341
     *
342
     * @return string
343
     */
344 28
    protected function wrappedTable()
345
    {
346 28
        return $this->query->getGrammar()->wrapTable($this->getQuery()->from);
347
    }
348
349
    /**
350
     * Wrap model's key name.
351
     *
352
     * @return string
353
     */
354 12
    protected function wrappedKey()
355
    {
356 12
        return $this->query->getGrammar()->wrap($this->model->getKeyName());
357
    }
358
359
    /**
360
     * Exclude root node from the result.
361
     *
362
     * @return self
363
     */
364 8
    public function withoutRoot()
365
    {
366 8
        $this->query->whereNotNull($this->model->getParentIdName());
367
368 8
        return $this;
369
    }
370
371
    /**
372
     * Order by node position.
373
     *
374
     * @param  string  $dir
375
     *
376
     * @return self
377
     */
378 52
    public function defaultOrder($dir = 'asc')
379
    {
380 52
        $this->query->orders = null;
381
382 52
        $this->query->orderBy($this->model->getLftName(), $dir);
383
384 52
        return $this;
385
    }
386
387
    /**
388
     * Order by reversed node position.
389
     *
390
     * @return $this
391
     */
392 4
    public function reversed()
393
    {
394 4
        return $this->defaultOrder('desc');
395
    }
396
397
    /**
398
     * Move a node to the new position.
399
     *
400
     * @param  mixed  $key
401
     * @param  int    $position
402
     *
403
     * @return int
404
     */
405 37
    public function moveNode($key, $position)
406
    {
407 37
        list($lft, $rgt) = $this->model->newNestedSetQuery()
408 37
                                       ->getPlainNodeData($key, true);
409
410 37
        if ($lft < $position && $position <= $rgt) {
411
            throw new LogicException('Cannot move node into itself.');
412
        }
413
414
        // Get boundaries of nodes that should be moved to new position
415 37
        $from = min($lft, $position);
416 37
        $to   = max($rgt, $position - 1);
417
418
        // The height of node that is being moved
419 37
        $height   = $rgt - $lft + 1;
420
421
        // The distance that our node will travel to reach it's destination
422 37
        $distance = $to - $from + 1 - $height;
423
424
        // If no distance to travel, just return
425 37
        if ($distance === 0) {
426
            return 0;
427
        }
428
429 37
        if ($position > $lft) {
430 29
            $height *= -1;
431 24
        } else {
432 8
            $distance *= -1;
433
        }
434
435 37
        $boundary = [$from, $to];
436
        $query    = $this->toBase()->where(function (Query $inner) use ($boundary) {
437 37
            $inner->whereBetween($this->model->getLftName(), $boundary);
438 37
            $inner->orWhereBetween($this->model->getRgtName(), $boundary);
439 37
        });
440
441 37
        return $query->update($this->patch(
442 37
            compact('lft', 'rgt', 'from', 'to', 'height', 'distance')
443 30
        ));
444
    }
445
446
    /**
447
     * Make or remove gap in the tree. Negative height will remove gap.
448
     *
449
     * @param  int  $cut
450
     * @param  int  $height
451
     *
452
     * @return int
453
     */
454 47
    public function makeGap($cut, $height)
455
    {
456
        $query = $this->toBase()->whereNested(function (Query $inner) use ($cut) {
457 47
            $inner->where($this->model->getLftName(), '>=', $cut);
458 47
            $inner->orWhere($this->model->getRgtName(), '>=', $cut);
459 47
        });
460
461 47
        return $query->update($this->patch(
462 47
            compact('cut', 'height')
463 36
        ));
464
    }
465
466
    /**
467
     * Get patch for columns.
468
     *
469
     * @param  array  $params
470
     *
471
     * @return array
472
     */
473 80
    protected function patch(array $params)
474
    {
475 80
        $grammar = $this->query->getGrammar();
476 80
        $columns = [];
477
478 80
        foreach ([$this->model->getLftName(), $this->model->getRgtName()] as $col) {
479 80
            $columns[$col] = $this->columnPatch($grammar->wrap($col), $params);
480 63
        }
481
482 80
        return $columns;
483
    }
484
485
    /**
486
     * Get patch for single column.
487
     *
488
     * @param  string  $col
489
     * @param  array   $params
490
     *
491
     * @return string
492
     */
493 80
    protected function columnPatch($col, array $params)
494
    {
495
        /**
496
         * @var int $height
497
         * @var int $distance
498
         * @var int $lft
499
         * @var int $rgt
500
         * @var int $from
501
         * @var int $to
502
         */
503 80
        extract($params);
504
505 80
        if ($height > 0) $height = '+'.$height;
506
507 80
        if (isset($cut)) {
508 47
            return new Expression("case when {$col} >= {$cut} then {$col}{$height} else {$col} end");
509
        }
510
511 37
        if ($distance > 0) {
512 29
            $distance = '+'.$distance;
513 24
        }
514
515 37
        return new Expression(
516
            "case ".
517 37
            "when {$col} between {$lft} and {$rgt} then {$col}{$distance} ". // Move the node
518 37
            "when {$col} between {$from} and {$to} then {$col}{$height} ". // Move other nodes
519 37
            "else {$col} end"
520 30
        );
521
    }
522
523
    /**
524
     * Get statistics of errors of the tree.
525
     *
526
     * @return array
527
     */
528 12
    public function countErrors()
529
    {
530
        $checks = [
531 12
            'oddness'        => $this->getOddnessQuery(),      // Check if lft and rgt values are ok
532 12
            'duplicates'     => $this->getDuplicatesQuery(),   // Check if lft and rgt values are unique
533 12
            'wrong_parent'   => $this->getWrongParentQuery(),  // Check if parent_id is set correctly
534 12
            'missing_parent' => $this->getMissingParentQuery() // Check for nodes that have missing parent
535 9
        ];
536
537 12
        $query = $this->query->newQuery();
538
539 12
        foreach ($checks as $key => $inner) {
540
            /** @var \Illuminate\Database\Query\Builder $inner */
541 12
            $inner->selectRaw('count(1)');
542
543 12
            $query->selectSub($inner, $key);
544 9
        }
545
546 12
        return (array) $query->first();
547
    }
548
549
    /**
550
     * @return \Illuminate\Database\Query\Builder
551
     */
552 12
    protected function getOddnessQuery()
553
    {
554 12
        return $this->model
555 12
            ->newNestedSetQuery()
556 12
            ->toBase()
557
            ->whereNested(function (Query $inner) {
558 12
                list($lft, $rgt) = $this->wrappedColumns();
559
560 12
                $inner->whereRaw("{$lft} >= {$rgt}")
561 12
                      ->orWhereRaw("({$rgt} - {$lft}) % 2 = 0");
562 12
            });
563
    }
564
565
    /**
566
     * @return \Illuminate\Database\Query\Builder
567
     */
568 12
    protected function getDuplicatesQuery()
569
    {
570 12
        $table = $this->wrappedTable();
571
572 12
        $query = $this->model
573 12
            ->newNestedSetQuery('c1')
574 12
            ->toBase()
575 12
            ->from($this->query->raw("{$table} c1, {$table} c2"))
576 12
            ->whereRaw("c1.id < c2.id")
577
            ->whereNested(function (Query $inner) {
578 12
                list($lft, $rgt) = $this->wrappedColumns();
579
580 12
                $inner->orWhereRaw("c1.{$lft}=c2.{$lft}")
581 12
                      ->orWhereRaw("c1.{$rgt}=c2.{$rgt}")
582 12
                      ->orWhereRaw("c1.{$lft}=c2.{$rgt}")
583 12
                      ->orWhereRaw("c1.{$rgt}=c2.{$lft}");
584 12
            });
585
586 12
        return $this->model->applyNestedSetScope($query, 'c2');
587
    }
588
589
    /**
590
     * @return \Illuminate\Database\Query\Builder
591
     */
592 12
    protected function getWrongParentQuery()
593
    {
594 12
        $table = $this->wrappedTable();
595 12
        $keyName = $this->wrappedKey();
596 12
        $parentIdName = $this->query->raw($this->model->getParentIdName());
597
598 12
        $query = $this->model
599 12
            ->newNestedSetQuery('c')
600 12
            ->toBase()
601 12
            ->from($this->query->raw("{$table} c, {$table} p, $table m"))
602 12
            ->whereRaw("c.{$parentIdName}=p.{$keyName}")
603 12
            ->whereRaw("m.{$keyName} <> p.{$keyName}")
604 12
            ->whereRaw("m.{$keyName} <> c.{$keyName}")
605
            ->whereNested(function (Query $inner) {
606 12
                list($lft, $rgt) = $this->wrappedColumns();
607
608 12
                $inner->whereRaw("c.{$lft} not between p.{$lft} and p.{$rgt}")
609 12
                      ->orWhereRaw("c.{$lft} between m.{$lft} and m.{$rgt}")
610 12
                      ->whereRaw("m.{$lft} between p.{$lft} and p.{$rgt}");
611 12
            });
612
613 12
        $this->model->applyNestedSetScope($query, 'p');
614 12
        $this->model->applyNestedSetScope($query, 'm');
615
616 12
        return $query;
617
    }
618
619
    /**
620
     * @return \Illuminate\Database\Query\Builder
621
     */
622 12
    protected function getMissingParentQuery()
623
    {
624 12
        return $this->model
625 12
            ->newNestedSetQuery()
626 12
            ->toBase()
627 12
            ->whereNested(function (Query $inner) {
628 12
                $table = $this->wrappedTable();
629 12
                $keyName = $this->wrappedKey();
630 12
                $parentIdName = $this->query->raw($this->model->getParentIdName());
631
632 12
                $existsCheck = $this->model
633 12
                    ->newNestedSetQuery()
634 12
                    ->toBase()
635 12
                    ->selectRaw('1')
636 12
                    ->from($this->query->raw("{$table} p"))
637 12
                    ->whereRaw("{$table}.{$parentIdName} = p.{$keyName}")
638 12
                    ->limit(1);
639
640 12
                $this->model->applyNestedSetScope($existsCheck, 'p');
641
642 12
                $inner->whereRaw("{$parentIdName} is not null")
643 12
                      ->addWhereExistsQuery($existsCheck, 'and', true);
644 12
            });
645
    }
646
647
    /**
648
     * Get the number of total errors of the tree.
649
     *
650
     * @return int
651
     */
652 8
    public function getTotalErrors()
653
    {
654 8
        return array_sum($this->countErrors());
655
    }
656
657
    /**
658
     * Get whether the tree is broken.
659
     *
660
     * @return bool
661
     */
662 8
    public function isBroken()
663
    {
664 8
        return $this->getTotalErrors() > 0;
665
    }
666
667
    /**
668
     * Fixes the tree based on parentage info.
669
     * Nodes with invalid parent are saved as roots.
670
     *
671
     * @return int The number of fixed nodes
672
     */
673 4
    public function fixTree()
674
    {
675
        $columns   = [
676 4
            $this->model->getKeyName(),
677 4
            $this->model->getParentIdName(),
678 4
            $this->model->getLftName(),
679 4
            $this->model->getRgtName(),
680 3
        ];
681
682 4
        $dictionary = $this->defaultOrder()
683 4
                ->get($columns)
684 4
                ->groupBy($this->model->getParentIdName())
685 4
                ->all();
686
687 4
        return TreeHelper::fixNodes($dictionary);
688
    }
689
690
    /**
691
     * Rebuild the tree based on raw data.
692
     * If item data does not contain primary key, new node will be created.
693
     *
694
     * @param  array  $data
695
     * @param  bool   $delete  Whether to delete nodes that exists but not in the data array
696
     *
697
     * @return int
698
     */
699 8
    public function rebuildTree(array $data, $delete = false)
700
    {
701 8
        $existing   = $this->get()->getDictionary();
702 8
        $dictionary = [];
703 8
        $this->buildRebuildDictionary($dictionary, $data, $existing);
704
705 4
        if ( ! empty($existing)) {
706 4
            if ($delete) {
707 4
                $this->model
708 4
                    ->newScopedQuery()
709 4
                    ->whereIn($this->model->getKeyName(), array_keys($existing))
710 4
                    ->forceDelete();
711 3
            } else {
712
                /** @var NodeTrait $model */
713
                foreach ($existing as $model) {
714
                    $dictionary[$model->getParentId()][] = $model;
715
                }
716
            }
717 3
        }
718
719 4
        return TreeHelper::fixNodes($dictionary);
720
    }
721
722
    /**
723
     * @param  array  $dictionary
724
     * @param  array  $data
725
     * @param  array  $existing
726
     * @param  mixed  $parentId
727
     */
728 8
    protected function buildRebuildDictionary(
729
        array &$dictionary,
730
        array $data,
731
        array &$existing,
732
        $parentId = null
733
    ) {
734 8
        $keyName = $this->model->getKeyName();
735
736 8
        foreach ($data as $itemData) {
737 8
            if ( ! isset($itemData[$keyName])) {
738 4
                $model = $this->model->newInstance();
0 ignored issues
show
Bug introduced by
It seems like newInstance() 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...
739
                // We will save it as raw node since tree will be fixed
740 4
                $model->rawNode(0, 0, $parentId);
741 3
            } else {
742 4
                if ( ! isset($existing[$key = $itemData[$keyName]])) {
743 4
                    throw new ModelNotFoundException;
744
                }
745
                $model = $existing[$key];
746
                unset($existing[$key]);
747
            }
748
749 4
            $model->fill($itemData)->save();
750 4
            $dictionary[$parentId][] = $model;
751
752 4
            if ( ! isset($itemData['children'])) {
753 4
                continue;
754
            }
755
756
            $this->buildRebuildDictionary(
757
                $dictionary,
758
                $itemData['children'],
759
                $existing,
760
                $model->getKey()
761
            );
762 3
        }
763 4
    }
764
765
    /**
766
     * @param  string|null  $table
767
     *
768
     * @return self
769
     */
770
    public function applyNestedSetScope($table = null)
771
    {
772
        return $this->model->applyNestedSetScope($this, $table);
773
    }
774
775
    /**
776
     * Get the root node.
777
     *
778
     * @param  array  $columns
779
     *
780
     * @return self
781
     */
782 16
    public function root(array $columns = ['*'])
783
    {
784 16
        return $this->whereIsRoot()->first($columns);
785
    }
786
}
787