Completed
Push — master ( dddeda...6cb9bf )
by ARCANEDEV
04:15
created

QueryBuilder::rebuildTree()   B

Complexity

Conditions 4
Paths 3

Size

Total Lines 22
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 22
ccs 16
cts 16
cp 1
rs 8.9197
cc 4
eloc 14
nc 3
nop 2
crap 4
1
<?php namespace Arcanedev\LaravelNestedSet\Eloquent;
2
3
use Arcanedev\LaravelNestedSet\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
     * The model being queried.
26
     *
27
     * @var \Arcanedev\LaravelNestedSet\NodeTrait
28
     */
29
    protected $model;
30
31
    /* ------------------------------------------------------------------------------------------------
32
     |  Main Functions
33
     | ------------------------------------------------------------------------------------------------
34
     */
35
    /**
36
     * Get node's `lft` and `rgt` values.
37
     *
38
     * @param  mixed  $id
39
     * @param  bool   $required
40
     *
41
     * @return array
42
     */
43 76
    public function getNodeData($id, $required = false)
44
    {
45 76
        $query = $this->toBase();
46
47 76
        $query->where($this->model->getKeyName(), '=', $id);
48
49 76
        $data  = $query->first([
50 76
            $this->model->getLftName(),
51 76
            $this->model->getRgtName(),
52 57
        ]);
53
54 76
        if ( ! $data && $required) {
55 4
            throw new ModelNotFoundException;
56
        }
57
58 72
        return (array) $data;
59
    }
60
61
    /**
62
     * Get plain node data.
63
     *
64
     * @param  mixed  $id
65
     * @param  bool   $required
66
     *
67
     * @return array
68
     */
69 48
    public function getPlainNodeData($id, $required = false)
70
    {
71 48
        return array_values($this->getNodeData($id, $required));
72
    }
73
74
    /**
75
     * Scope limits query to select just root node.
76
     *
77
     * @return self
78
     */
79 20
    public function whereIsRoot()
80
    {
81 20
        $this->query->whereNull($this->model->getParentIdName());
82
83 20
        return $this;
84
    }
85
86
    /**
87
     * Limit results to ancestors of specified node.
88
     *
89
     * @param  mixed  $id
90
     *
91
     * @return self
92
     */
93 20
    public function whereAncestorOf($id)
94
    {
95 20
        $keyName = $this->model->getKeyName();
96
97 20
        if (NestedSet::isNode($id)) {
98 16
            $value = '?';
99
100 16
            $this->query->addBinding($id->getLft());
101
102 16
            $id = $id->getKey();
103 12
        } else {
104 4
            $valueQuery = $this->model
105 4
                ->newQuery()
106 4
                ->toBase()
107 4
                ->select("_.".$this->model->getLftName())
108 4
                ->from($this->model->getTable().' as _')
109 4
                ->where($keyName, '=', $id)
110 4
                ->limit(1);
111
112 4
            $this->query->mergeBindings($valueQuery);
113
114 4
            $value = '(' . $valueQuery->toSql() . ')';
115
        }
116
117 20
        list($lft, $rgt) = $this->wrappedColumns();
118
119 20
        $this->query->whereRaw("{$value} between {$lft} and {$rgt}");
120
121
        // Exclude the node
122 20
        $this->where($keyName, '<>', $id);
123
124 20
        return $this;
125
    }
126
127
    /**
128
     * Get ancestors of specified node.
129
     *
130
     * @param  mixed  $id
131
     * @param  array  $columns
132
     *
133
     * @return self
134
     */
135 12
    public function ancestorsOf($id, array $columns = ['*'])
136
    {
137 12
        return $this->whereAncestorOf($id)->get($columns);
138
    }
139
140
    /**
141
     * Add node selection statement between specified range.
142
     *
143
     * @param  array   $values
144
     * @param  string  $boolean
145
     * @param  bool    $not
146
     *
147
     * @return self
148
     */
149 44
    public function whereNodeBetween($values, $boolean = 'and', $not = false)
150
    {
151 44
        $this->query->whereBetween($this->model->getLftName(), $values, $boolean, $not);
152
153 44
        return $this;
154
    }
155
156
    /**
157
     * Add node selection statement between specified range joined with `or` operator.
158
     *
159
     * @param  array  $values
160
     *
161
     * @return self
162
     */
163
    public function orWhereNodeBetween($values)
164
    {
165
        return $this->whereNodeBetween($values, 'or');
166
    }
167
168
    /**
169
     * Add constraint statement to descendants of specified node.
170
     *
171
     * @param  mixed   $id
172
     * @param  string  $boolean
173
     * @param  bool    $not
174
     *
175
     * @return self
176
     */
177 48
    public function whereDescendantOf($id, $boolean = 'and', $not = false)
178
    {
179 48
        $data = NestedSet::isNode($id)
180 47
            ? $id->getBounds()
181 48
            : $this->model->newNestedSetQuery()->getPlainNodeData($id, true);
182
183
        // Don't include the node
184 44
        ++$data[0];
185
186 44
        return $this->whereNodeBetween($data, $boolean, $not);
187
    }
188
189
    /**
190
     * @param  mixed  $id
191
     *
192
     * @return self
193
     */
194
    public function whereNotDescendantOf($id)
195
    {
196
        return $this->whereDescendantOf($id, 'and', true);
197
    }
198
199
    /**
200
     * @param  mixed  $id
201
     *
202
     * @return self
203
     */
204 4
    public function orWhereDescendantOf($id)
205
    {
206 4
        return $this->whereDescendantOf($id, 'or');
207
    }
208
209
    /**
210
     * @param  mixed  $id
211
     *
212
     * @return self
213
     */
214
    public function orWhereNotDescendantOf($id)
215
    {
216
        return $this->whereDescendantOf($id, 'or', true);
217
    }
218
219
    /**
220
     * Get descendants of specified node.
221
     *
222
     * @param  mixed  $id
223
     * @param  array  $columns
224
     *
225
     * @return \Arcanedev\LaravelNestedSet\Eloquent\Collection
226
     */
227
    public function descendantsOf($id, array $columns = ['*'])
228
    {
229
        try {
230
            return $this->whereDescendantOf($id)->get($columns);
231
        }
232
        catch (ModelNotFoundException $e) {
233
            return $this->model->newCollection();
234
        }
235
    }
236
237
    /**
238
     * @param  mixed   $id
239
     * @param  string  $operator
240
     * @param  string  $boolean
241
     *
242
     * @return self
243
     */
244
    protected function whereIsBeforeOrAfter($id, $operator, $boolean)
245
    {
246
        if (NestedSet::isNode($id)) {
247
            $value = '?';
248
249
            $this->query->addBinding($id->getLft());
250
        } else {
251
            $valueQuery = $this->model
252
                ->newQuery()
253
                ->toBase()
254
                ->select('_n.'.$this->model->getLftName())
255
                ->from($this->model->getTable().' as _n')
256
                ->where('_n.'.$this->model->getKeyName(), '=', $id);
257
258
            $this->query->mergeBindings($valueQuery);
259
260
            $value = '('.$valueQuery->toSql().')';
261
        }
262
263
        list($lft,) = $this->wrappedColumns();
264
265
        $this->query->whereRaw("{$lft} {$operator} {$value}", [ ], $boolean);
266
267
        return $this;
268
    }
269
270
    /**
271
     * Constraint nodes to those that are after specified node.
272
     *
273
     * @param  mixed   $id
274
     * @param  string  $boolean
275
     *
276
     * @return self
277
     */
278
    public function whereIsAfter($id, $boolean = 'and')
279
    {
280
        return $this->whereIsBeforeOrAfter($id, '>', $boolean);
281
    }
282
283
    /**
284
     * Constraint nodes to those that are before specified node.
285
     *
286
     * @param  mixed   $id
287
     * @param  string  $boolean
288
     *
289
     * @return self
290
     */
291
    public function whereIsBefore($id, $boolean = 'and')
292
    {
293
        return $this->whereIsBeforeOrAfter($id, '<', $boolean);
294
    }
295
296
    /**
297
     * Include depth level into the result.
298
     *
299
     * @param  string  $as
300
     *
301
     * @return self
302
     */
303 16
    public function withDepth($as = 'depth')
304
    {
305 16
        if ($this->query->columns === null) {
306 16
            $this->query->columns = ['*'];
307 12
        }
308
309 16
        $table = $this->wrappedTable();
310
311 16
        list($lft, $rgt) = $this->wrappedColumns();
312
313 16
        $query = $this->model
314 16
            ->newScopedQuery('_d')
315 16
            ->toBase()
316 16
            ->selectRaw('count(1) - 1')
317 16
            ->from($this->model->getTable().' as _d')
318 16
            ->whereRaw("{$table}.{$lft} between _d.{$lft} and _d.{$rgt}");
319
320 16
        $this->query->selectSub($query, $as);
321
322 16
        return $this;
323
    }
324
325
    /**
326
     * Get wrapped `lft` and `rgt` column names.
327
     *
328
     * @return array
329
     */
330 48
    protected function wrappedColumns()
331
    {
332 48
        $grammar = $this->query->getGrammar();
333
334
        return [
335 48
            $grammar->wrap($this->model->getLftName()),
336 48
            $grammar->wrap($this->model->getRgtName()),
337 36
        ];
338
    }
339
340
    /**
341
     * Get a wrapped table name.
342
     *
343
     * @return string
344
     */
345 28
    protected function wrappedTable()
346
    {
347 28
        return $this->query->getGrammar()->wrapTable($this->getQuery()->from);
348
    }
349
350
    /**
351
     * Wrap model's key name.
352
     *
353
     * @return string
354
     */
355 12
    protected function wrappedKey()
356
    {
357 12
        return $this->query->getGrammar()->wrap($this->model->getKeyName());
358
    }
359
360
    /**
361
     * Exclude root node from the result.
362
     *
363
     * @return self
364
     */
365 8
    public function withoutRoot()
366
    {
367 8
        $this->query->whereNotNull($this->model->getParentIdName());
368
369 8
        return $this;
370
    }
371
372
    /**
373
     * Order by node position.
374
     *
375
     * @param  string  $dir
376
     *
377
     * @return self
378
     */
379 52
    public function defaultOrder($dir = 'asc')
380
    {
381 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...
382
383 52
        $this->query->orderBy($this->model->getLftName(), $dir);
384
385 52
        return $this;
386
    }
387
388
    /**
389
     * Order by reversed node position.
390
     *
391
     * @return $this
392
     */
393 4
    public function reversed()
394
    {
395 4
        return $this->defaultOrder('desc');
396
    }
397
398
    /**
399
     * Move a node to the new position.
400
     *
401
     * @param  mixed  $key
402
     * @param  int    $position
403
     *
404
     * @return int
405
     */
406 40
    public function moveNode($key, $position)
407
    {
408 40
        list($lft, $rgt) = $this->model->newNestedSetQuery()
409 40
                                       ->getPlainNodeData($key, true);
410
411 40
        if ($lft < $position && $position <= $rgt) {
412
            throw new LogicException('Cannot move node into itself.');
413
        }
414
415
        // Get boundaries of nodes that should be moved to new position
416 40
        $from = min($lft, $position);
417 40
        $to   = max($rgt, $position - 1);
418
419
        // The height of node that is being moved
420 40
        $height   = $rgt - $lft + 1;
421
422
        // The distance that our node will travel to reach it's destination
423 40
        $distance = $to - $from + 1 - $height;
424
425
        // If no distance to travel, just return
426 40
        if ($distance === 0) {
427
            return 0;
428
        }
429
430 40
        if ($position > $lft) {
431 32
            $height *= -1;
432 24
        } else {
433 8
            $distance *= -1;
434
        }
435
436 40
        $boundary = [$from, $to];
437
        $query    = $this->toBase()->where(function (Query $inner) use ($boundary) {
438 40
            $inner->whereBetween($this->model->getLftName(), $boundary);
439 40
            $inner->orWhereBetween($this->model->getRgtName(), $boundary);
440 40
        });
441
442 40
        return $query->update($this->patch(
443 40
            compact('lft', 'rgt', 'from', 'to', 'height', 'distance')
444 30
        ));
445
    }
446
447
    /**
448
     * Make or remove gap in the tree. Negative height will remove gap.
449
     *
450
     * @param  int  $cut
451
     * @param  int  $height
452
     *
453
     * @return int
454
     */
455 48
    public function makeGap($cut, $height)
456
    {
457
        $query = $this->toBase()->whereNested(function (Query $inner) use ($cut) {
458 48
            $inner->where($this->model->getLftName(), '>=', $cut);
459 48
            $inner->orWhere($this->model->getRgtName(), '>=', $cut);
460 48
        });
461
462 48
        return $query->update($this->patch(
463 48
            compact('cut', 'height')
464 36
        ));
465
    }
466
467
    /**
468
     * Get patch for columns.
469
     *
470
     * @param  array  $params
471
     *
472
     * @return array
473
     */
474 84
    protected function patch(array $params)
475
    {
476 84
        $grammar = $this->query->getGrammar();
477 84
        $columns = [];
478
479 84
        foreach ([$this->model->getLftName(), $this->model->getRgtName()] as $col) {
480 84
            $columns[$col] = $this->columnPatch($grammar->wrap($col), $params);
481 63
        }
482
483 84
        return $columns;
484
    }
485
486
    /**
487
     * Get patch for single column.
488
     *
489
     * @param  string  $col
490
     * @param  array   $params
491
     *
492
     * @return string
493
     */
494 84
    protected function columnPatch($col, array $params)
495
    {
496
        /**
497
         * @var int $height
498
         * @var int $distance
499
         * @var int $lft
500
         * @var int $rgt
501
         * @var int $from
502
         * @var int $to
503
         */
504 84
        extract($params);
505
506 84
        if ($height > 0) $height = '+'.$height;
507
508 84
        if (isset($cut)) {
509 48
            return new Expression("case when {$col} >= {$cut} then {$col}{$height} else {$col} end");
510
        }
511
512 40
        if ($distance > 0) {
513 32
            $distance = '+'.$distance;
514 24
        }
515
516 40
        return new Expression(
517
            "case ".
518 40
            "when {$col} between {$lft} and {$rgt} then {$col}{$distance} ". // Move the node
519 40
            "when {$col} between {$from} and {$to} then {$col}{$height} ". // Move other nodes
520 40
            "else {$col} end"
521 30
        );
522
    }
523
524
    /**
525
     * Get statistics of errors of the tree.
526
     *
527
     * @return array
528
     */
529 12
    public function countErrors()
530
    {
531
        $checks = [
532 12
            'oddness'        => $this->getOddnessQuery(),      // Check if lft and rgt values are ok
533 12
            'duplicates'     => $this->getDuplicatesQuery(),   // Check if lft and rgt values are unique
534 12
            'wrong_parent'   => $this->getWrongParentQuery(),  // Check if parent_id is set correctly
535 12
            'missing_parent' => $this->getMissingParentQuery() // Check for nodes that have missing parent
536 9
        ];
537
538 12
        $query = $this->query->newQuery();
539
540 12
        foreach ($checks as $key => $inner) {
541
            /** @var \Illuminate\Database\Query\Builder $inner */
542 12
            $inner->selectRaw('count(1)');
543
544 12
            $query->selectSub($inner, $key);
545 9
        }
546
547 12
        return (array) $query->first();
548
    }
549
550
    /**
551
     * Get the oddness errors query.
552
     *
553
     * @return \Illuminate\Database\Query\Builder
554
     */
555 12
    protected function getOddnessQuery()
556
    {
557 12
        return $this->model
558 12
            ->newNestedSetQuery()
559 12
            ->toBase()
560
            ->whereNested(function (Query $inner) {
561 12
                list($lft, $rgt) = $this->wrappedColumns();
562
563 12
                $inner->whereRaw("{$lft} >= {$rgt}")
564 12
                      ->orWhereRaw("({$rgt} - {$lft}) % 2 = 0");
565 12
            });
566
    }
567
568
    /**
569
     * Get the duplicates errors query.
570
     *
571
     * @return \Illuminate\Database\Query\Builder
572
     */
573 12
    protected function getDuplicatesQuery()
574
    {
575 12
        $table = $this->wrappedTable();
576
577 12
        $query = $this->model
578 12
            ->newNestedSetQuery('c1')
579 12
            ->toBase()
580 12
            ->from($this->query->raw("{$table} c1, {$table} c2"))
581 12
            ->whereRaw("c1.id < c2.id")
582
            ->whereNested(function (Query $inner) {
583 12
                list($lft, $rgt) = $this->wrappedColumns();
584
585 12
                $inner->orWhereRaw("c1.{$lft}=c2.{$lft}")
586 12
                      ->orWhereRaw("c1.{$rgt}=c2.{$rgt}")
587 12
                      ->orWhereRaw("c1.{$lft}=c2.{$rgt}")
588 12
                      ->orWhereRaw("c1.{$rgt}=c2.{$lft}");
589 12
            });
590
591 12
        return $this->model->applyNestedSetScope($query, 'c2');
0 ignored issues
show
Documentation introduced by
$query is of type object<Illuminate\Database\Query\Builder>, but the function expects a object<Illuminate\Database\Eloquent\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...
592
    }
593
594
    /**
595
     * Get the wrong parent query.
596
     *
597
     * @return \Illuminate\Database\Query\Builder
598
     */
599 12
    protected function getWrongParentQuery()
600
    {
601 12
        $table        = $this->wrappedTable();
602 12
        $keyName      = $this->wrappedKey();
603 12
        $parentIdName = $this->query->raw($this->model->getParentIdName());
604 12
        $query        = $this->model
605 12
            ->newNestedSetQuery('c')
606 12
            ->toBase()
607 12
            ->from($this->query->raw("{$table} c, {$table} p, $table m"))
608 12
            ->whereRaw("c.{$parentIdName}=p.{$keyName}")
609 12
            ->whereRaw("m.{$keyName} <> p.{$keyName}")
610 12
            ->whereRaw("m.{$keyName} <> c.{$keyName}")
611
            ->whereNested(function (Query $inner) {
612 12
                list($lft, $rgt) = $this->wrappedColumns();
613
614 12
                $inner->whereRaw("c.{$lft} not between p.{$lft} and p.{$rgt}")
615 12
                      ->orWhereRaw("c.{$lft} between m.{$lft} and m.{$rgt}")
616 12
                      ->whereRaw("m.{$lft} between p.{$lft} and p.{$rgt}");
617 12
            });
618
619 12
        $this->model->applyNestedSetScope($query, 'p');
0 ignored issues
show
Documentation introduced by
$query is of type object<Illuminate\Database\Query\Builder>, but the function expects a object<Illuminate\Database\Eloquent\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...
620 12
        $this->model->applyNestedSetScope($query, 'm');
0 ignored issues
show
Documentation introduced by
$query is of type object<Illuminate\Database\Query\Builder>, but the function expects a object<Illuminate\Database\Eloquent\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...
621
622 12
        return $query;
623
    }
624
625
    /**
626
     * Get the missing parent query.
627
     *
628
     * @return \Illuminate\Database\Query\Builder
629
     */
630 12
    protected function getMissingParentQuery()
631
    {
632 12
        return $this->model
633 12
            ->newNestedSetQuery()
634 12
            ->toBase()
635 12
            ->whereNested(function (Query $inner) {
636 12
                $table = $this->wrappedTable();
637 12
                $keyName = $this->wrappedKey();
638 12
                $parentIdName = $this->query->raw($this->model->getParentIdName());
639
640 12
                $query = $this->model
641 12
                    ->newNestedSetQuery()
642 12
                    ->toBase()
643 12
                    ->selectRaw('1')
644 12
                    ->from($this->query->raw("{$table} p"))
645 12
                    ->whereRaw("{$table}.{$parentIdName} = p.{$keyName}")
646 12
                    ->limit(1);
647
648 12
                $this->model->applyNestedSetScope($query, 'p');
0 ignored issues
show
Documentation introduced by
$query is of type object<Illuminate\Database\Query\Builder>, but the function expects a object<Illuminate\Database\Eloquent\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...
649
650 12
                $inner->whereRaw("{$parentIdName} is not null")
651 12
                      ->addWhereExistsQuery($query, 'and', true);
652 12
            });
653
    }
654
655
    /**
656
     * Get the number of total errors of the tree.
657
     *
658
     * @return int
659
     */
660 8
    public function getTotalErrors()
661
    {
662 8
        return array_sum($this->countErrors());
663
    }
664
665
    /**
666
     * Get whether the tree is broken.
667
     *
668
     * @return bool
669
     */
670 8
    public function isBroken()
671
    {
672 8
        return $this->getTotalErrors() > 0;
673
    }
674
675
    /**
676
     * Fixes the tree based on parentage info.
677
     * Nodes with invalid parent are saved as roots.
678
     *
679
     * @return int The number of fixed nodes
680
     */
681 4
    public function fixTree()
682
    {
683
        $columns   = [
684 4
            $this->model->getKeyName(),
685 4
            $this->model->getParentIdName(),
686 4
            $this->model->getLftName(),
687 4
            $this->model->getRgtName(),
688 3
        ];
689
690 4
        $dictionary = $this->defaultOrder()
691 4
                ->get($columns)
692 4
                ->groupBy($this->model->getParentIdName())
693 4
                ->all();
694
695 4
        return TreeHelper::fixNodes($dictionary);
696
    }
697
698
    /**
699
     * Rebuild the tree based on raw data.
700
     * If item data does not contain primary key, new node will be created.
701
     *
702
     * @param  array  $data
703
     * @param  bool   $delete  Whether to delete nodes that exists but not in the data array
704
     *
705
     * @return int
706
     */
707 12
    public function rebuildTree(array $data, $delete = false)
708
    {
709 12
        $existing   = $this->get()->getDictionary();
710 12
        $dictionary = [];
711 12
        $this->buildRebuildDictionary($dictionary, $data, $existing);
712
713 8
        if ( ! empty($existing)) {
714 8
            if ($delete) {
715 4
                $this->model
0 ignored issues
show
Documentation Bug introduced by
The method whereIn 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...
716 4
                    ->newScopedQuery()
717 4
                    ->whereIn($this->model->getKeyName(), array_keys($existing))
718 4
                    ->forceDelete();
719 3
            } else {
720
                /** @var NodeTrait $model */
721 4
                foreach ($existing as $model) {
722 4
                    $dictionary[$model->getParentId()][] = $model;
723 3
                }
724
            }
725 6
        }
726
727 8
        return TreeHelper::fixNodes($dictionary);
728
    }
729
730
    /**
731
     * @param  array  $dictionary
732
     * @param  array  $data
733
     * @param  array  $existing
734
     * @param  mixed  $parentId
735
     */
736 12
    protected function buildRebuildDictionary(
737
        array &$dictionary,
738
        array $data,
739
        array &$existing,
740
        $parentId = null
741
    ) {
742 12
        $keyName = $this->model->getKeyName();
743
744 12
        foreach ($data as $itemData) {
745 12
            if ( ! isset($itemData[$keyName])) {
746 8
                $model = $this->model->newInstance();
747
                // We will save it as raw node since tree will be fixed
748 8
                $model->rawNode(0, 0, $parentId);
749 6
            } else {
750 8
                if ( ! isset($existing[$key = $itemData[$keyName]])) {
751 4
                    throw new ModelNotFoundException;
752
                }
753 4
                $model = $existing[$key];
754 4
                unset($existing[$key]);
755
            }
756
757 8
            $model->fill($itemData)->save();
758 8
            $dictionary[$parentId][] = $model;
759
760 8
            if ( ! isset($itemData['children'])) {
761 8
                continue;
762
            }
763
764 4
            $this->buildRebuildDictionary(
765 3
                $dictionary,
766 4
                $itemData['children'],
767 3
                $existing,
768 4
                $model->getKey()
769 3
            );
770 6
        }
771 8
    }
772
773
    /**
774
     * @param  string|null  $table
775
     *
776
     * @return self
777
     */
778
    public function applyNestedSetScope($table = null)
779
    {
780
        return $this->model->applyNestedSetScope($this, $table);
781
    }
782
783
    /**
784
     * Get the root node.
785
     *
786
     * @param  array  $columns
787
     *
788
     * @return self
789
     */
790 16
    public function root(array $columns = ['*'])
791
    {
792 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 792 which is incompatible with the return type documented by Arcanedev\LaravelNestedS...uent\QueryBuilder::root of type Arcanedev\LaravelNestedS...quent\QueryBuilder|null.
Loading history...
793
    }
794
}
795