Completed
Push — master ( 5a4264...db73cc )
by ARCANEDEV
06:52
created

src/Eloquent/QueryBuilder.php (4 issues)

Upgrade to new PHP Analysis Engine

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

1
<?php namespace Arcanedev\LaravelNestedSet\Eloquent;
2
3
use Arcanedev\LaravelNestedSet\Traits\NodeTrait;
4
use Arcanedev\LaravelNestedSet\Utilities\NestedSet;
5
use Arcanedev\LaravelNestedSet\Utilities\TreeHelper;
6
use Illuminate\Database\Eloquent\Builder;
7
use Illuminate\Database\Eloquent\ModelNotFoundException;
8
use Illuminate\Database\Query\Builder as Query;
9
use Illuminate\Database\Query\Expression;
10
use LogicException;
11
12
/**
13
 * Class     QueryBuilder
14
 *
15
 * @package  Arcanedev\LaravelNestedSet\Eloquent
16
 * @author   ARCANEDEV <[email protected]>
17
 */
18
class QueryBuilder extends Builder
19
{
20
    /* ------------------------------------------------------------------------------------------------
21
     |  Properties
22
     | ------------------------------------------------------------------------------------------------
23
     */
24
    /**
25
     * @var \Arcanedev\LaravelNestedSet\Traits\NodeTrait
26
     */
27
    protected $model;
28
29
    /* ------------------------------------------------------------------------------------------------
30
     |  Main Functions
31
     | ------------------------------------------------------------------------------------------------
32
     */
33
    /**
34
     * Get node's `lft` and `rgt` values.
35
     *
36
     * @param  mixed  $id
37
     * @param  bool   $required
38
     *
39
     * @return array
40
     */
41 76
    public function getNodeData($id, $required = false)
42
    {
43 76
        $query = $this->toBase();
44
45 76
        $query->where($this->model->getKeyName(), '=', $id);
46
47 76
        $data  = $query->first([
48 76
            $this->model->getLftName(),
49 76
            $this->model->getRgtName(),
50 57
        ]);
51
52 76
        if ( ! $data && $required) {
53 4
            throw new ModelNotFoundException;
54
        }
55
56 72
        return (array) $data;
57
    }
58
59
    /**
60
     * Get plain node data.
61
     *
62
     * @param  mixed  $id
63
     * @param  bool   $required
64
     *
65
     * @return array
66
     */
67 48
    public function getPlainNodeData($id, $required = false)
68
    {
69 48
        return array_values($this->getNodeData($id, $required));
70
    }
71
72
    /**
73
     * Scope limits query to select just root node.
74
     *
75
     * @return self
76
     */
77 20
    public function whereIsRoot()
78
    {
79 20
        $this->query->whereNull($this->model->getParentIdName());
80
81 20
        return $this;
82
    }
83
84
    /**
85
     * Limit results to ancestors of specified node.
86
     *
87
     * @param  mixed  $id
88
     *
89
     * @return self
90
     */
91 20
    public function whereAncestorOf($id)
92
    {
93 20
        $keyName = $this->model->getKeyName();
94
95 20
        if (NestedSet::isNode($id)) {
96 16
            $value = '?';
97
98 16
            $this->query->addBinding($id->getLft());
99
100 16
            $id = $id->getKey();
101 12
        } else {
102 4
            $valueQuery = $this->model
103 4
                ->newQuery()
104 4
                ->toBase()
105 4
                ->select("_.".$this->model->getLftName())
106 4
                ->from($this->model->getTable().' as _')
107 4
                ->where($keyName, '=', $id)
108 4
                ->limit(1);
109
110 4
            $this->query->mergeBindings($valueQuery);
111
112 4
            $value = '(' . $valueQuery->toSql() . ')';
113
        }
114
115 20
        list($lft, $rgt) = $this->wrappedColumns();
116
117 20
        $this->query->whereRaw("{$value} between {$lft} and {$rgt}");
118
119
        // Exclude the node
120 20
        $this->where($keyName, '<>', $id);
121
122 20
        return $this;
123
    }
124
125
    /**
126
     * Get ancestors of specified node.
127
     *
128
     * @param  mixed  $id
129
     * @param  array  $columns
130
     *
131
     * @return self
132
     */
133 12
    public function ancestorsOf($id, array $columns = ['*'])
134
    {
135 12
        return $this->whereAncestorOf($id)->get($columns);
136
    }
137
138
    /**
139
     * Add node selection statement between specified range.
140
     *
141
     * @param  array   $values
142
     * @param  string  $boolean
143
     * @param  bool    $not
144
     *
145
     * @return self
146
     */
147 44
    public function whereNodeBetween($values, $boolean = 'and', $not = false)
148
    {
149 44
        $this->query->whereBetween($this->model->getLftName(), $values, $boolean, $not);
150
151 44
        return $this;
152
    }
153
154
    /**
155
     * Add node selection statement between specified range joined with `or` operator.
156
     *
157
     * @param  array  $values
158
     *
159
     * @return self
160
     */
161
    public function orWhereNodeBetween($values)
162
    {
163
        return $this->whereNodeBetween($values, 'or');
164
    }
165
166
    /**
167
     * Add constraint statement to descendants of specified node.
168
     *
169
     * @param  mixed   $id
170
     * @param  string  $boolean
171
     * @param  bool    $not
172
     *
173
     * @return self
174
     */
175 48
    public function whereDescendantOf($id, $boolean = 'and', $not = false)
176
    {
177 48
        $data = NestedSet::isNode($id)
178 47
            ? $id->getBounds()
179 48
            : $this->model->newNestedSetQuery()->getPlainNodeData($id, true);
180
181
        // Don't include the node
182 44
        ++$data[0];
183
184 44
        return $this->whereNodeBetween($data, $boolean, $not);
185
    }
186
187
    /**
188
     * @param  mixed  $id
189
     *
190
     * @return self
191
     */
192
    public function whereNotDescendantOf($id)
193
    {
194
        return $this->whereDescendantOf($id, 'and', true);
195
    }
196
197
    /**
198
     * @param  mixed  $id
199
     *
200
     * @return self
201
     */
202 4
    public function orWhereDescendantOf($id)
203
    {
204 4
        return $this->whereDescendantOf($id, 'or');
205
    }
206
207
    /**
208
     * @param  mixed  $id
209
     *
210
     * @return self
211
     */
212
    public function orWhereNotDescendantOf($id)
213
    {
214
        return $this->whereDescendantOf($id, 'or', true);
215
    }
216
217
    /**
218
     * Get descendants of specified node.
219
     *
220
     * @param  mixed  $id
221
     * @param  array  $columns
222
     *
223
     * @return \Arcanedev\LaravelNestedSet\Eloquent\Collection
224
     */
225
    public function descendantsOf($id, array $columns = ['*'])
226
    {
227
        try {
228
            return $this->whereDescendantOf($id)->get($columns);
229
        }
230
        catch (ModelNotFoundException $e) {
231
            return $this->model->newCollection();
232
        }
233
    }
234
235
    /**
236
     * @param  mixed   $id
237
     * @param  string  $operator
238
     * @param  string  $boolean
239
     *
240
     * @return self
241
     */
242
    protected function whereIsBeforeOrAfter($id, $operator, $boolean)
243
    {
244
        if (NestedSet::isNode($id)) {
245
            $value = '?';
246
247
            $this->query->addBinding($id->getLft());
248
        } else {
249
            $valueQuery = $this->model
250
                ->newQuery()
251
                ->toBase()
252
                ->select('_n.'.$this->model->getLftName())
253
                ->from($this->model->getTable().' as _n')
254
                ->where('_n.'.$this->model->getKeyName(), '=', $id);
255
256
            $this->query->mergeBindings($valueQuery);
257
258
            $value = '('.$valueQuery->toSql().')';
259
        }
260
261
        list($lft,) = $this->wrappedColumns();
262
263
        $this->query->whereRaw("{$lft} {$operator} {$value}", [ ], $boolean);
264
265
        return $this;
266
    }
267
268
    /**
269
     * Constraint nodes to those that are after specified node.
270
     *
271
     * @param  mixed   $id
272
     * @param  string  $boolean
273
     *
274
     * @return self
275
     */
276
    public function whereIsAfter($id, $boolean = 'and')
277
    {
278
        return $this->whereIsBeforeOrAfter($id, '>', $boolean);
279
    }
280
281
    /**
282
     * Constraint nodes to those that are before specified node.
283
     *
284
     * @param  mixed   $id
285
     * @param  string  $boolean
286
     *
287
     * @return self
288
     */
289
    public function whereIsBefore($id, $boolean = 'and')
290
    {
291
        return $this->whereIsBeforeOrAfter($id, '<', $boolean);
292
    }
293
294
    /**
295
     * Include depth level into the result.
296
     *
297
     * @param  string  $as
298
     *
299
     * @return self
300
     */
301 16
    public function withDepth($as = 'depth')
302
    {
303 16
        if ($this->query->columns === null) {
304 16
            $this->query->columns = ['*'];
305 12
        }
306
307 16
        $table = $this->wrappedTable();
308
309 16
        list($lft, $rgt) = $this->wrappedColumns();
310
311 16
        $query = $this->model
312 16
            ->newScopedQuery('_d')
313 16
            ->toBase()
314 16
            ->selectRaw('count(1) - 1')
315 16
            ->from($this->model->getTable().' as _d')
316 16
            ->whereRaw("{$table}.{$lft} between _d.{$lft} and _d.{$rgt}");
317
318 16
        $this->query->selectSub($query, $as);
319
320 16
        return $this;
321
    }
322
323
    /**
324
     * Get wrapped `lft` and `rgt` column names.
325
     *
326
     * @return array
327
     */
328 48
    protected function wrappedColumns()
329
    {
330 48
        $grammar = $this->query->getGrammar();
331
332
        return [
333 48
            $grammar->wrap($this->model->getLftName()),
334 48
            $grammar->wrap($this->model->getRgtName()),
335 36
        ];
336
    }
337
338
    /**
339
     * Get a wrapped table name.
340
     *
341
     * @return string
342
     */
343 28
    protected function wrappedTable()
344
    {
345 28
        return $this->query->getGrammar()->wrapTable($this->getQuery()->from);
346
    }
347
348
    /**
349
     * Wrap model's key name.
350
     *
351
     * @return string
352
     */
353 12
    protected function wrappedKey()
354
    {
355 12
        return $this->query->getGrammar()->wrap($this->model->getKeyName());
356
    }
357
358
    /**
359
     * Exclude root node from the result.
360
     *
361
     * @return self
362
     */
363 8
    public function withoutRoot()
364
    {
365 8
        $this->query->whereNotNull($this->model->getParentIdName());
366
367 8
        return $this;
368
    }
369
370
    /**
371
     * Order by node position.
372
     *
373
     * @param  string  $dir
374
     *
375
     * @return self
376
     */
377 52
    public function defaultOrder($dir = 'asc')
378
    {
379 52
        $this->query->orders = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $orders.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
380
381 52
        $this->query->orderBy($this->model->getLftName(), $dir);
382
383 52
        return $this;
384
    }
385
386
    /**
387
     * Order by reversed node position.
388
     *
389
     * @return $this
390
     */
391 4
    public function reversed()
392
    {
393 4
        return $this->defaultOrder('desc');
394
    }
395
396
    /**
397
     * Move a node to the new position.
398
     *
399
     * @param  mixed  $key
400
     * @param  int    $position
401
     *
402
     * @return int
403
     */
404 40
    public function moveNode($key, $position)
405
    {
406 40
        list($lft, $rgt) = $this->model->newNestedSetQuery()
407 40
                                       ->getPlainNodeData($key, true);
408
409 40
        if ($lft < $position && $position <= $rgt) {
410
            throw new LogicException('Cannot move node into itself.');
411
        }
412
413
        // Get boundaries of nodes that should be moved to new position
414 40
        $from = min($lft, $position);
415 40
        $to   = max($rgt, $position - 1);
416
417
        // The height of node that is being moved
418 40
        $height   = $rgt - $lft + 1;
419
420
        // The distance that our node will travel to reach it's destination
421 40
        $distance = $to - $from + 1 - $height;
422
423
        // If no distance to travel, just return
424 40
        if ($distance === 0) {
425
            return 0;
426
        }
427
428 40
        if ($position > $lft) {
429 32
            $height *= -1;
430 24
        } else {
431 8
            $distance *= -1;
432
        }
433
434 40
        $boundary = [$from, $to];
435
        $query    = $this->toBase()->where(function (Query $inner) use ($boundary) {
436 40
            $inner->whereBetween($this->model->getLftName(), $boundary);
437 40
            $inner->orWhereBetween($this->model->getRgtName(), $boundary);
438 40
        });
439
440 40
        return $query->update($this->patch(
441 40
            compact('lft', 'rgt', 'from', 'to', 'height', 'distance')
442 30
        ));
443
    }
444
445
    /**
446
     * Make or remove gap in the tree. Negative height will remove gap.
447
     *
448
     * @param  int  $cut
449
     * @param  int  $height
450
     *
451
     * @return int
452
     */
453 48
    public function makeGap($cut, $height)
454
    {
455
        $query = $this->toBase()->whereNested(function (Query $inner) use ($cut) {
456 48
            $inner->where($this->model->getLftName(), '>=', $cut);
457 48
            $inner->orWhere($this->model->getRgtName(), '>=', $cut);
458 48
        });
459
460 48
        return $query->update($this->patch(
461 48
            compact('cut', 'height')
462 36
        ));
463
    }
464
465
    /**
466
     * Get patch for columns.
467
     *
468
     * @param  array  $params
469
     *
470
     * @return array
471
     */
472 84
    protected function patch(array $params)
473
    {
474 84
        $grammar = $this->query->getGrammar();
475 84
        $columns = [];
476
477 84
        foreach ([$this->model->getLftName(), $this->model->getRgtName()] as $col) {
478 84
            $columns[$col] = $this->columnPatch($grammar->wrap($col), $params);
479 63
        }
480
481 84
        return $columns;
482
    }
483
484
    /**
485
     * Get patch for single column.
486
     *
487
     * @param  string  $col
488
     * @param  array   $params
489
     *
490
     * @return string
491
     */
492 84
    protected function columnPatch($col, array $params)
493
    {
494
        /**
495
         * @var int $height
496
         * @var int $distance
497
         * @var int $lft
498
         * @var int $rgt
499
         * @var int $from
500
         * @var int $to
501
         */
502 84
        extract($params);
503
504 84
        if ($height > 0) $height = '+'.$height;
505
506 84
        if (isset($cut)) {
507 48
            return new Expression("case when {$col} >= {$cut} then {$col}{$height} else {$col} end");
508
        }
509
510 40
        if ($distance > 0) {
511 32
            $distance = '+'.$distance;
512 24
        }
513
514 40
        return new Expression(
515
            "case ".
516 40
            "when {$col} between {$lft} and {$rgt} then {$col}{$distance} ". // Move the node
517 40
            "when {$col} between {$from} and {$to} then {$col}{$height} ". // Move other nodes
518 40
            "else {$col} end"
519 30
        );
520
    }
521
522
    /**
523
     * Get statistics of errors of the tree.
524
     *
525
     * @return array
526
     */
527 12
    public function countErrors()
528
    {
529
        $checks = [
530 12
            'oddness'        => $this->getOddnessQuery(),      // Check if lft and rgt values are ok
531 12
            'duplicates'     => $this->getDuplicatesQuery(),   // Check if lft and rgt values are unique
532 12
            'wrong_parent'   => $this->getWrongParentQuery(),  // Check if parent_id is set correctly
533 12
            'missing_parent' => $this->getMissingParentQuery() // Check for nodes that have missing parent
534 9
        ];
535
536 12
        $query = $this->query->newQuery();
537
538 12
        foreach ($checks as $key => $inner) {
539
            /** @var \Illuminate\Database\Query\Builder $inner */
540 12
            $inner->selectRaw('count(1)');
541
542 12
            $query->selectSub($inner, $key);
543 9
        }
544
545 12
        return (array) $query->first();
546
    }
547
548
    /**
549
     * @return \Illuminate\Database\Query\Builder
550
     */
551 12
    protected function getOddnessQuery()
552
    {
553 12
        return $this->model
554 12
            ->newNestedSetQuery()
555 12
            ->toBase()
556
            ->whereNested(function (Query $inner) {
557 12
                list($lft, $rgt) = $this->wrappedColumns();
558
559 12
                $inner->whereRaw("{$lft} >= {$rgt}")
560 12
                      ->orWhereRaw("({$rgt} - {$lft}) % 2 = 0");
561 12
            });
562
    }
563
564
    /**
565
     * @return \Illuminate\Database\Query\Builder
566
     */
567 12
    protected function getDuplicatesQuery()
568
    {
569 12
        $table = $this->wrappedTable();
570
571 12
        $query = $this->model
572 12
            ->newNestedSetQuery('c1')
573 12
            ->toBase()
574 12
            ->from($this->query->raw("{$table} c1, {$table} c2"))
575 12
            ->whereRaw("c1.id < c2.id")
576
            ->whereNested(function (Query $inner) {
577 12
                list($lft, $rgt) = $this->wrappedColumns();
578
579 12
                $inner->orWhereRaw("c1.{$lft}=c2.{$lft}")
580 12
                      ->orWhereRaw("c1.{$rgt}=c2.{$rgt}")
581 12
                      ->orWhereRaw("c1.{$lft}=c2.{$rgt}")
582 12
                      ->orWhereRaw("c1.{$rgt}=c2.{$lft}");
583 12
            });
584
585 12
        return $this->model->applyNestedSetScope($query, 'c2');
586
    }
587
588
    /**
589
     * @return \Illuminate\Database\Query\Builder
590
     */
591 12
    protected function getWrongParentQuery()
592
    {
593 12
        $table        = $this->wrappedTable();
594 12
        $keyName      = $this->wrappedKey();
595 12
        $parentIdName = $this->query->raw($this->model->getParentIdName());
596 12
        $query        = $this->model
597 12
            ->newNestedSetQuery('c')
598 12
            ->toBase()
599 12
            ->from($this->query->raw("{$table} c, {$table} p, $table m"))
600 12
            ->whereRaw("c.{$parentIdName}=p.{$keyName}")
601 12
            ->whereRaw("m.{$keyName} <> p.{$keyName}")
602 12
            ->whereRaw("m.{$keyName} <> c.{$keyName}")
603
            ->whereNested(function (Query $inner) {
604 12
                list($lft, $rgt) = $this->wrappedColumns();
605
606 12
                $inner->whereRaw("c.{$lft} not between p.{$lft} and p.{$rgt}")
607 12
                      ->orWhereRaw("c.{$lft} between m.{$lft} and m.{$rgt}")
608 12
                      ->whereRaw("m.{$lft} between p.{$lft} and p.{$rgt}");
609 12
            });
610
611 12
        $this->model->applyNestedSetScope($query, 'p');
612 12
        $this->model->applyNestedSetScope($query, 'm');
613
614 12
        return $query;
615
    }
616
617
    /**
618
     * @return \Illuminate\Database\Query\Builder
619
     */
620 12
    protected function getMissingParentQuery()
621
    {
622 12
        return $this->model
623 12
            ->newNestedSetQuery()
624 12
            ->toBase()
625 12
            ->whereNested(function (Query $inner) {
626 12
                $table = $this->wrappedTable();
627 12
                $keyName = $this->wrappedKey();
628 12
                $parentIdName = $this->query->raw($this->model->getParentIdName());
629
630 12
                $existsCheck = $this->model
631 12
                    ->newNestedSetQuery()
632 12
                    ->toBase()
633 12
                    ->selectRaw('1')
634 12
                    ->from($this->query->raw("{$table} p"))
635 12
                    ->whereRaw("{$table}.{$parentIdName} = p.{$keyName}")
636 12
                    ->limit(1);
637
638 12
                $this->model->applyNestedSetScope($existsCheck, 'p');
639
640 12
                $inner->whereRaw("{$parentIdName} is not null")
641 12
                      ->addWhereExistsQuery($existsCheck, 'and', true);
642 12
            });
643
    }
644
645
    /**
646
     * Get the number of total errors of the tree.
647
     *
648
     * @return int
649
     */
650 8
    public function getTotalErrors()
651
    {
652 8
        return array_sum($this->countErrors());
653
    }
654
655
    /**
656
     * Get whether the tree is broken.
657
     *
658
     * @return bool
659
     */
660 8
    public function isBroken()
661
    {
662 8
        return $this->getTotalErrors() > 0;
663
    }
664
665
    /**
666
     * Fixes the tree based on parentage info.
667
     * Nodes with invalid parent are saved as roots.
668
     *
669
     * @return int The number of fixed nodes
670
     */
671 4
    public function fixTree()
672
    {
673
        $columns   = [
674 4
            $this->model->getKeyName(),
675 4
            $this->model->getParentIdName(),
676 4
            $this->model->getLftName(),
677 4
            $this->model->getRgtName(),
678 3
        ];
679
680 4
        $dictionary = $this->defaultOrder()
681 4
                ->get($columns)
682 4
                ->groupBy($this->model->getParentIdName())
683 4
                ->all();
684
685 4
        return TreeHelper::fixNodes($dictionary);
686
    }
687
688
    /**
689
     * Rebuild the tree based on raw data.
690
     * If item data does not contain primary key, new node will be created.
691
     *
692
     * @param  array  $data
693
     * @param  bool   $delete  Whether to delete nodes that exists but not in the data array
694
     *
695
     * @return int
696
     */
697 8
    public function rebuildTree(array $data, $delete = false)
698
    {
699 8
        $existing   = $this->get()->getDictionary();
700 8
        $dictionary = [];
701 8
        $this->buildRebuildDictionary($dictionary, $data, $existing);
702
703 4
        if ( ! empty($existing)) {
704 4
            if ($delete) {
705 4
                $this->model
0 ignored issues
show
The method forceDelete() does not exist on Illuminate\Database\Query\Builder. Did you maybe mean delete()?

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...
706 4
                    ->newScopedQuery()
707 4
                    ->whereIn($this->model->getKeyName(), array_keys($existing))
708 4
                    ->forceDelete();
709 3
            } else {
710
                /** @var NodeTrait $model */
711
                foreach ($existing as $model) {
712
                    $dictionary[$model->getParentId()][] = $model;
713
                }
714
            }
715 3
        }
716
717 4
        return TreeHelper::fixNodes($dictionary);
718
    }
719
720
    /**
721
     * @param  array  $dictionary
722
     * @param  array  $data
723
     * @param  array  $existing
724
     * @param  mixed  $parentId
725
     */
726 8
    protected function buildRebuildDictionary(
727
        array &$dictionary,
728
        array $data,
729
        array &$existing,
730
        $parentId = null
731
    ) {
732 8
        $keyName = $this->model->getKeyName();
733
734 8
        foreach ($data as $itemData) {
735 8
            if ( ! isset($itemData[$keyName])) {
736 4
                $model = $this->model->newInstance();
737
                // We will save it as raw node since tree will be fixed
738 4
                $model->rawNode(0, 0, $parentId);
739 3
            } else {
740 4
                if ( ! isset($existing[$key = $itemData[$keyName]])) {
741 4
                    throw new ModelNotFoundException;
742
                }
743
                $model = $existing[$key];
744
                unset($existing[$key]);
745
            }
746
747 4
            $model->fill($itemData)->save();
748 4
            $dictionary[$parentId][] = $model;
749
750 4
            if ( ! isset($itemData['children'])) {
751 4
                continue;
752
            }
753
754
            $this->buildRebuildDictionary(
755
                $dictionary,
756
                $itemData['children'],
757
                $existing,
758
                $model->getKey()
759
            );
760 3
        }
761 4
    }
762
763
    /**
764
     * @param  string|null  $table
765
     *
766
     * @return self
767
     */
768
    public function applyNestedSetScope($table = null)
769
    {
770
        return $this->model->applyNestedSetScope($this, $table);
0 ignored issues
show
$this is of type this<Arcanedev\LaravelNe...\Eloquent\QueryBuilder>, but the function expects a object<Illuminate\Database\Query\Builder>.

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...
771
    }
772
773
    /**
774
     * Get the root node.
775
     *
776
     * @param  array  $columns
777
     *
778
     * @return self
779
     */
780 16
    public function root(array $columns = ['*'])
781
    {
782 16
        return $this->whereIsRoot()->first($columns);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->whereIsRoot()->first($columns); of type Illuminate\Database\Eloq...e\Eloquent\Builder|null adds the type Illuminate\Database\Eloquent\Model to the return on line 782 which is incompatible with the return type documented by Arcanedev\LaravelNestedS...uent\QueryBuilder::root of type Arcanedev\LaravelNestedS...quent\QueryBuilder|null.
Loading history...
783
    }
784
}
785