GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Pull Request — master (#9)
by Haru
36:29
created

MaterializedPathBehavior::getSiblings()   B

Complexity

Conditions 3
Paths 4

Size

Total Lines 27
Code Lines 19

Duplication

Lines 3
Ratio 11.11 %

Code Coverage

Tests 15
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 3
loc 27
ccs 15
cts 15
cp 1
rs 8.8571
cc 3
eloc 19
nc 4
nop 1
crap 3
1
<?php
2
/**
3
 * @link https://github.com/paulzi/yii2-materialized-path
4
 * @copyright Copyright (c) 2015 PaulZi <[email protected]>
5
 * @license MIT (https://github.com/paulzi/yii2-materialized-path/blob/master/LICENSE)
6
 */
7
8
namespace paulzi\materializedPath;
9
10
use paulzi\sortable\SortableBehavior;
11
use Yii;
12
use yii\base\Behavior;
13
use yii\base\Exception;
14
use yii\base\NotSupportedException;
15
use yii\db\ActiveRecord;
16
use yii\db\Expression;
17
18
/**
19
 * Materialized Path Behavior for Yii2
20
 * @author PaulZi <[email protected]>
21
 *
22
 * @property ActiveRecord $owner
23
 */
24
class MaterializedPathBehavior extends Behavior
25
{
26
    const OPERATION_MAKE_ROOT       = 1;
27
    const OPERATION_PREPEND_TO      = 2;
28
    const OPERATION_APPEND_TO       = 3;
29
    const OPERATION_INSERT_BEFORE   = 4;
30
    const OPERATION_INSERT_AFTER    = 5;
31
    const OPERATION_DELETE_ALL      = 6;
32
33
34
    /**
35
     * @var string
36
     */
37
    public $pathAttribute = 'path';
38
39
    /**
40
     * @var string
41
     */
42
    public $depthAttribute = 'depth';
43
44
    /**
45
     * @var string
46
     */
47
    public $itemAttribute;
48
49
    /**
50
     * @var string|null
51
     */
52
    public $treeAttribute;
53
54
    /**
55
     * @var array|false SortableBehavior config
56
     */
57
    public $sortable = [];
58
59
    /**
60
     * @var string
61
     */
62
    public $delimiter = '/';
63
64
    /**
65
     * @var int Value of $depthAttribute for root node.
66
     */
67
    public $rootDepthValue = 0;
68
    
69
    /**
70
     * @var int|null
71
     */
72
    protected $operation;
73
74
    /**
75
     * @var ActiveRecord|self|null
76
     */
77
    protected $node;
78
79
    /**
80
     * @var SortableBehavior
81
     */
82
    protected $behavior;
83
84
    /**
85
     * @var bool
86
     */
87
    protected $primaryKeyMode = false;
88
89
90
    /**
91
     * @inheritdoc
92
     */
93 201
    public function events()
94
    {
95
        return [
96 201
            ActiveRecord::EVENT_BEFORE_INSERT   => 'beforeSave',
97 201
            ActiveRecord::EVENT_AFTER_INSERT    => 'afterInsert',
98 201
            ActiveRecord::EVENT_BEFORE_UPDATE   => 'beforeSave',
99 201
            ActiveRecord::EVENT_AFTER_UPDATE    => 'afterUpdate',
100 201
            ActiveRecord::EVENT_BEFORE_DELETE   => 'beforeDelete',
101 201
            ActiveRecord::EVENT_AFTER_DELETE    => 'afterDelete',
102 201
        ];
103
    }
104
105
    /**
106
     * @param ActiveRecord $owner
107
     * @throws Exception
108
     */
109 201
    public function attach($owner)
110
    {
111 201
        parent::attach($owner);
112 201
        if ($this->itemAttribute === null) {
113 201
            $primaryKey = $owner->primaryKey();
114 201
            if (!isset($primaryKey[0])) {
115
                throw new Exception('"' . $owner->className() . '" must have a primary key.');
116
            }
117 201
            $this->itemAttribute = $primaryKey[0];
118 201
            $this->primaryKeyMode = true;
119 201
        }
120 201
        if ($this->sortable !== false) {
121 201
            $this->behavior = Yii::createObject(array_merge(
122
                [
123 201
                    'class' => SortableBehavior::className(),
124
                    'query' => function () {
125 87
                        return $this->getSortableQuery();
126 201
                    },
127 201
                ],
128 201
                $this->sortable
129 201
            ));
130 201
            $owner->attachBehavior('materialized-path-sortable', $this->behavior);
131 201
        }
132 201
    }
133
134
    /**
135
     * @param int|null $depth
136
     * @return \yii\db\ActiveQuery
137
     */
138 9
    public function getParents($depth = null)
139
    {
140 9
        $path  = $this->getParentPath();
141 9
        if ($path !== null) {
142 9
            $paths = explode($this->delimiter, $path);
143 9
            if (!$this->primaryKeyMode) {
144 9
                $path  = null;
145 9
                $paths = array_map(
146 9
                    function ($value) use (&$path) {
147 9
                        return $path = ($path !== null ? $path . $this->delimiter : '') . $value;
148 9
                    },
149
                    $paths
150 9
                );
151 9
            }
152 9
            if ($depth !== null) {
153 9
                $paths = array_slice($paths, -$depth);
154 9
            }
155 9
        } else {
156 3
            $paths = [];
157
        }
158
159 9
        $tableName = $this->owner->tableName();
160 9
        $condition = ['and'];
161 9
        if ($this->primaryKeyMode) {
162 6
            $condition[] = ["{$tableName}.[[{$this->itemAttribute}]]" => $paths];
163 6
        } else {
164 9
            $condition[] = ["{$tableName}.[[{$this->pathAttribute}]]" => $paths];
165
        }
166
167 9
        $query = $this->owner->find()
168 9
            ->andWhere($condition)
169 9
            ->andWhere($this->treeCondition())
170 9
            ->addOrderBy(["{$tableName}.[[{$this->pathAttribute}]]" => SORT_ASC]);
171 9
        $query->multiple = true;
172
173 9
        return $query;
174
    }
175
176
    /**
177
     * @return \yii\db\ActiveQuery
178
     */
179 6
    public function getParent()
180
    {
181 6
        $query = $this->getParents(1)->limit(1);
182 6
        $query->multiple = false;
183 6
        return $query;
184
    }
185
186
    /**
187
     * @return \yii\db\ActiveQuery
188
     */
189 3
    public function getRoot()
190
    {
191 3
        $path = explode($this->delimiter, $this->owner->getAttribute($this->pathAttribute));
192 3
        $path = array_shift($path);
193 3
        $tableName = $this->owner->tableName();
194 3
        $query = $this->owner->find()
195 3
            ->andWhere(["{$tableName}.[[{$this->pathAttribute}]]" => $path])
196 3
            ->andWhere($this->treeCondition())
197 3
            ->limit(1);
198 3
        $query->multiple = false;
199 3
        return $query;
200
    }
201
202
    /**
203
     * @param int|null $depth
204
     * @param bool $andSelf
205
     * @return \yii\db\ActiveQuery
206
     */
207 27
    public function getDescendants($depth = null, $andSelf = false)
208
    {
209 27
        $tableName = $this->owner->tableName();
210 27
        $path = $this->owner->getAttribute($this->pathAttribute);
211 27
        $query = $this->owner->find()
212 27
            ->andWhere(['like', "{$tableName}.[[{$this->pathAttribute}]]", $this->getLike($path), false]);
213
214 27
        if ($andSelf) {
215 9
            $query->orWhere(["{$tableName}.[[{$this->pathAttribute}]]" => $path]);
216 9
        }
217
218 27
        if ($depth !== null) {
219 21
            $query->andWhere(['<=', "{$tableName}.[[{$this->depthAttribute}]]", $this->owner->getAttribute($this->depthAttribute) + $depth]);
220 21
        }
221
222 27
        $orderBy = [];
223 27
        $orderBy["{$tableName}.[[{$this->depthAttribute}]]"] = SORT_ASC;
224 27 View Code Duplication
        if ($this->sortable !== false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
225 27
            $orderBy["{$tableName}.[[{$this->behavior->sortAttribute}]]"] = SORT_ASC;
226 27
        }
227 27
        $orderBy["{$tableName}.[[{$this->itemAttribute}]]"]  = SORT_ASC;
228
229
        $query
230 27
            ->andWhere($this->treeCondition())
231 27
            ->addOrderBy($orderBy);
232 27
        $query->multiple = true;
233
234 27
        return $query;
235
    }
236
237
    /**
238
     * @return \yii\db\ActiveQuery
239
     */
240 12
    public function getChildren()
241
    {
242 12
        return $this->getDescendants(1);
243
    }
244
245
    /**
246
     * @param int|null $depth
247
     * @return \yii\db\ActiveQuery
248
     */
249 3
    public function getLeaves($depth = null)
250
    {
251 3
        $tableName = $this->owner->tableName();
252
        $condition = [
253 3
            'and',
254 3
            ['like', "leaves.[[{$this->pathAttribute}]]",  new Expression($this->concatExpression(["{$tableName}.[[{$this->pathAttribute}]]", ':delimiter']), [':delimiter' => $this->delimiter . '%'])],
255 3
        ];
256
257 3
        if ($this->treeAttribute !== null) {
258 3
            $condition[] = ["leaves.[[{$this->treeAttribute}]]" => new Expression("{$tableName}.[[{$this->treeAttribute}]]")];
259 3
        }
260
261 3
        $query = $this->getDescendants($depth)
262 3
            ->leftJoin("{$tableName} leaves", $condition)
263 3
            ->andWhere(["leaves.[[{$this->pathAttribute}]]" => null]);
264 3
        $query->multiple = true;
265 3
        return $query;
266
    }
267
268
    /**
269
     * @return \yii\db\ActiveQuery
270
     * @throws NotSupportedException
271
     */
272 3 View Code Duplication
    public function getPrev()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
273
    {
274 3
        if ($this->sortable === false) {
275
            throw new NotSupportedException('prev() not allow if not set sortable');
276
        }
277 3
        $tableName = $this->owner->tableName();
278 3
        $query = $this->owner->find()
279 3
            ->andWhere(['like', "{$tableName}.[[{$this->pathAttribute}]]", $this->getLike($this->getParentPath()), false])
0 ignored issues
show
Bug introduced by
It seems like $this->getParentPath() targeting paulzi\materializedPath\...havior::getParentPath() can also be of type array or null; however, paulzi\materializedPath\...PathBehavior::getLike() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
280 3
            ->andWhere(["{$tableName}.[[{$this->depthAttribute}]]" => $this->owner->getAttribute($this->depthAttribute)])
281 3
            ->andWhere(['<', "{$tableName}.[[{$this->behavior->sortAttribute}]]", $this->owner->getSortablePosition()])
282 3
            ->andWhere($this->treeCondition())
283 3
            ->orderBy(["{$tableName}.[[{$this->behavior->sortAttribute}]]" => SORT_DESC])
284 3
            ->limit(1);
285 3
        $query->multiple = false;
286 3
        return $query;
287
    }
288
289
    /**
290
     * @return \yii\db\ActiveQuery
291
     * @throws NotSupportedException
292
     */
293 6 View Code Duplication
    public function getNext()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
294
    {
295 6
        if ($this->sortable === false) {
296
            throw new NotSupportedException('prev() not allow if not set sortable');
297
        }
298 6
        $tableName = $this->owner->tableName();
299 6
        $query = $this->owner->find()
300 6
            ->andWhere(['like', "{$tableName}.[[{$this->pathAttribute}]]", $this->getLike($this->getParentPath()), false])
0 ignored issues
show
Bug introduced by
It seems like $this->getParentPath() targeting paulzi\materializedPath\...havior::getParentPath() can also be of type array or null; however, paulzi\materializedPath\...PathBehavior::getLike() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
301 6
            ->andWhere(["{$tableName}.[[{$this->depthAttribute}]]" => $this->owner->getAttribute($this->depthAttribute)])
302 6
            ->andWhere(['>', "{$tableName}.[[{$this->behavior->sortAttribute}]]", $this->owner->getSortablePosition()])
303 6
            ->andWhere($this->treeCondition())
304 6
            ->orderBy(["{$tableName}.[[{$this->behavior->sortAttribute}]]" => SORT_ASC])
305 6
            ->limit(1);
306 6
        $query->multiple = false;
307 6
        return $query;
308
    }
309
310
    /**
311
     * Returns all sibilings of node.
312
     * 
313
     * @param bool $andSelf = false Include self node into result.
314 66
     * @return \yii\db\ActiveQuery
315
     */
316 66
    public function getSiblings($andSelf = false)
317
    {
318
        $tableName = $this->owner->tableName();
319
        $path = $this->getParentPath();
320
        $like = strtr($path . $this->delimiter, ['%' => '\%', '_' => '\_', '\\' => '\\\\']);
321
322
        $query = $this->owner->find()
323
            ->andWhere(['like', "{$tableName}.[[{$this->pathAttribute}]]", $like . '%', false])
324
            ->andWhere(['<=', "{$tableName}.[[{$this->depthAttribute}]]", $this->owner->{$this->depthAttribute}]);
325 3
326
        if (!$andSelf) {
327
            $query->andWhere(["!=", "{$tableName}.[[{$this->itemAttribute}]]", $this->owner->{$this->itemAttribute}]);
328 3
        }
329 3
330 3
        $orderBy = [];
331 3
        $orderBy["{$tableName}.[[{$this->depthAttribute}]]"] = SORT_ASC;
332 View Code Duplication
        if ($this->sortable !== false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
333
            $orderBy["{$tableName}.[[{$this->behavior->sortAttribute}]]"] = SORT_ASC;
334 3
        }
335 3
        $orderBy["{$tableName}.[[{$this->itemAttribute}]]"] = SORT_ASC;
336 3
337 3
        $query
338 3
            ->andWhere($this->treeCondition())
339 3
            ->addOrderBy($orderBy);
340 3
        $query->multiple = true;
341 3
        return $query;
342 3
    }
343
344 3
    /**
345 3
     * @param bool $asArray = false
346 3
     * @return null|string|array
347 3
     */
348 3
    public function getParentPath($asArray = false)
349 3
    {
350 3
        return static::getParentPathInternal($this->owner->getAttribute($this->pathAttribute), $this->delimiter, $asArray);
351 3
    }
352 3
353
    /**
354 3
     * Populate children relations for self and all descendants
355
     *
356
     * @param int $depth = null
357
     * @return static
358
     */
359
    public function populateTree($depth = null)
360 72
    {
361
        /** @var ActiveRecord[]|static[] $nodes */
362 72
        if ($depth === null) {
363
            $nodes = $this->owner->descendants;
364
        } else {
365
            $nodes = $this->getDescendants($depth)->all();
366
        }
367
368
        $relates = [];
369 96
        foreach ($nodes as $node) {
370
            $path = $node->getParentPath(true);
371 96
            $key = array_pop($path);
372 96
            if (!isset($relates[$key])) {
373
                $relates[$key] = [];
374 96
            }
375 9
            $relates[$key][] = $node;
376 9
        }
377
378 96
        $nodes[$this->owner->getAttribute($this->itemAttribute)] = $this->owner;
379
        foreach ($nodes as $node) {
380
            $key = $node->getAttribute($this->itemAttribute);
381
            if (isset($relates[$key])) {
382
                $node->populateRelation('children', $relates[$key]);
383
            } elseif ($depth === null) {
384 3
                $node->populateRelation('children', []);
385
            }
386 3
        }
387
388
        return $this->owner;
389
    }
390
391
    /**
392 6
     * @return bool
393
     */
394 6
    public function isRoot()
395 6
    {
396
        return count(explode($this->delimiter, $this->owner->getAttribute($this->pathAttribute))) === 1;
397
    }
398
399
    /**
400
     * @param ActiveRecord $node
401
     * @return bool
402 33
     */
403
    public function isChildOf($node)
404 33
    {
405 33
        $nodePath  = $node->getAttribute($this->pathAttribute) . $this->delimiter;
406 33
        $result = substr($this->owner->getAttribute($this->pathAttribute), 0, strlen($nodePath)) === $nodePath;
407
408
        if ($result && $this->treeAttribute !== null) {
409
            $result = $this->owner->getAttribute($this->treeAttribute) === $node->getAttribute($this->treeAttribute);
410
        }
411
412
        return $result;
413 33
    }
414
415 33
    /**
416 33
     * @return bool
417 33
     */
418
    public function isLeaf()
419
    {
420
        return count($this->owner->children) === 0;
421
    }
422
423
    /**
424 30
     * @return ActiveRecord
425
     */
426 30
    public function makeRoot()
427 30
    {
428 30
        $this->operation = self::OPERATION_MAKE_ROOT;
429
        return $this->owner;
430
    }
431
432
    /**
433
     * @param ActiveRecord $node
434
     * @return ActiveRecord
435 30
     */
436
    public function prependTo($node)
437 30
    {
438 30
        $this->operation = self::OPERATION_PREPEND_TO;
439 30
        $this->node = $node;
440
        return $this->owner;
441
    }
442
443
    /**
444
     * @param ActiveRecord $node
445
     * @return ActiveRecord
446
     */
447 9
    public function appendTo($node)
448
    {
449 9
        $this->operation = self::OPERATION_APPEND_TO;
450 9
        $this->node = $node;
451
        return $this->owner;
452
    }
453
454
    /**
455
     * @param ActiveRecord $node
456
     * @return ActiveRecord
457
     */
458
    public function insertBefore($node)
459
    {
460
        $this->operation = self::OPERATION_INSERT_BEFORE;
461
        $this->node = $node;
462
        return $this->owner;
463
    }
464
465 9
    /**
466
     * @param ActiveRecord $node
467 6
     * @return ActiveRecord
468
     */
469
    public function insertAfter($node)
470
    {
471
        $this->operation = self::OPERATION_INSERT_AFTER;
472
        $this->node = $node;
473
        return $this->owner;
474 3
    }
475
476
    /**
477 3
     * @return bool|int
478 3
     * @throws \Exception
479 3
     * @throws \yii\db\Exception
480
     */
481
    public function deleteWithChildren()
482
    {
483
        $this->operation = self::OPERATION_DELETE_ALL;
484
        if (!$this->owner->isTransactional(ActiveRecord::OP_DELETE)) {
485
            $transaction = $this->owner->getDb()->beginTransaction();
486
            try {
487
                $result = $this->deleteWithChildrenInternal();
488
                if ($result === false) {
489 138
                    $transaction->rollBack();
490
                } else {
491 138
                    $transaction->commit();
492 108
                }
493 108
                return $result;
494
            } catch (\Exception $e) {
495 138
                $transaction->rollBack();
496 138
                throw $e;
497 6
            }
498
        } else {
499 6
            $result = $this->deleteWithChildrenInternal();
500 132
        }
501 33
        return $result;
502
    }
503 21
504 99
    /**
505 33
     * @param bool $middle
506
     * @return int
507 21
     */
508 66
    public function reorderChildren($middle = true)
509 30
    {
510
        /** @var ActiveRecord|SortableBehavior $item */
511 21
        $item = $this->owner->children[0];
512
        if ($item) {
513 36
            return $item->reorder($middle);
0 ignored issues
show
Bug introduced by
The method reorder does only exist in paulzi\sortable\SortableBehavior, but not in yii\db\ActiveRecord.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
514 30
        } else {
515
            return 0;
516 18
        }
517
    }
518 6
519 6
    /**
520 3
     * @throws Exception
521
     * @throws NotSupportedException
522
     */
523 3
    public function beforeSave()
524 3
    {
525 3
        if ($this->node !== null && !$this->node->getIsNewRecord()) {
0 ignored issues
show
Bug introduced by
The method getIsNewRecord does only exist in yii\db\ActiveRecord, but not in paulzi\materializedPath\MaterializedPathBehavior.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
526 93
            $this->node->refresh();
0 ignored issues
show
Bug introduced by
The method refresh does only exist in yii\db\ActiveRecord, but not in paulzi\materializedPath\MaterializedPathBehavior.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
527 90
        }
528
529
        switch ($this->operation) {
530
            case self::OPERATION_MAKE_ROOT:
531
                $this->makeRootInternal();
532 33
533
                break;
534 33
            case self::OPERATION_PREPEND_TO:
535 3
                $this->insertIntoInternal(false);
536 3
537
                break;
538 3
            case self::OPERATION_APPEND_TO:
539 3
                $this->insertIntoInternal(true);
540
541
                break;
542
            case self::OPERATION_INSERT_BEFORE:
543 3
                $this->insertNearInternal(false);
544 3
545 33
                break;
546 33
547 33
            case self::OPERATION_INSERT_AFTER:
548
                $this->insertNearInternal(true);
549
550 33
                break;
551 33
552 3
            default:
553 3
                if ($this->owner->getIsNewRecord()) {
554 30
                    throw new NotSupportedException('Method "' . $this->owner->className() . '::insert" is not supported for inserting new nodes.');
555 18
                }
556 18
557 12
                $item = $this->owner->getAttribute($this->itemAttribute);
558
                $path = $this->getParentPath();
559 30
                $this->owner->setAttribute($this->pathAttribute, $path . $this->delimiter . $item);
560
        }
561 33
    }
562 33
563 33
    /**
564 33
     * @throws Exception
565 33
     */
566 33
    public function afterInsert()
567
    {
568
        if ($this->operation === self::OPERATION_MAKE_ROOT && $this->treeAttribute !== null && $this->owner->getAttribute($this->treeAttribute) === null) {
569
            $id = $this->owner->getPrimaryKey();
570
            $this->owner->setAttribute($this->treeAttribute, $id);
571 57
572
            $primaryKey = $this->owner->primaryKey();
573 57 View Code Duplication
            if (!isset($primaryKey[0])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
574 57
                throw new Exception('"' . $this->owner->className() . '" must have a primary key.');
575 57
            }
576 57
577
            $this->owner->updateAll([$this->treeAttribute => $id], [$primaryKey[0] => $id]);
578
        }
579
        if ($this->owner->getAttribute($this->pathAttribute) === null) {
580
            $primaryKey = $this->owner->primaryKey();
581 View Code Duplication
            if (!isset($primaryKey[0])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
582 18
                throw new Exception('"' . $this->owner->className() . '" must have a primary key.');
583
            }
584 18
            $id = $this->owner->getPrimaryKey();
585 6
            if ($this->operation === self::OPERATION_MAKE_ROOT) {
586
                $path = $id;
587 12
            } else {
588 3
                if ($this->operation === self::OPERATION_INSERT_BEFORE || $this->operation === self::OPERATION_INSERT_AFTER) {
589
                    $path = $this->node->getParentPath();
0 ignored issues
show
Bug introduced by
The method getParentPath does only exist in paulzi\materializedPath\MaterializedPathBehavior, but not in yii\db\ActiveRecord.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
590 9
                } else {
591 9
                    $path = $this->node->getAttribute($this->pathAttribute);
0 ignored issues
show
Bug introduced by
The method getAttribute does only exist in yii\db\ActiveRecord, but not in paulzi\materializedPath\MaterializedPathBehavior.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
592
                }
593 3
                $path = $path . $this->delimiter . $id;
594 3
            }
595 3
            $this->owner->setAttribute($this->pathAttribute, $path);
596 3
            $this->owner->updateAll([$this->pathAttribute => $path], [$primaryKey[0] => $id]);
597 3
        }
598 3
        $this->operation = null;
599 3
        $this->node      = null;
600 3
    }
601 3
602
    /**
603
     * @param \yii\db\AfterSaveEvent $event
604 3
     */
605 9
    public function afterUpdate($event)
606
    {
607
        $this->moveNode($event->changedAttributes);
608
        $this->operation = null;
609
        $this->node      = null;
610 9
    }
611
612 9
    /**
613 3
     * @param \yii\base\ModelEvent $event
614
     * @throws Exception
615 3
     */
616
    public function beforeDelete($event)
617
    {
618 3
        if ($this->owner->getIsNewRecord()) {
619
            throw new Exception('Can not delete a node when it is new record.');
620 3
        }
621 3
        if ($this->isRoot() && $this->operation !== self::OPERATION_DELETE_ALL) {
622 9
            throw new Exception('Method "'. $this->owner->className() . '::delete" is not supported for deleting root nodes.');
623 9
        }
624 9
        $this->owner->refresh();
625
        if ($this->operation !== static::OPERATION_DELETE_ALL && !$this->primaryKeyMode) {
626
            /** @var self $parent */
627
            $parent =$this->getParent()->one();
628
            $slugs1 = $parent->getChildren()
629
                ->andWhere(['<>', $this->itemAttribute, $this->owner->getAttribute($this->itemAttribute)])
630
                ->select([$this->itemAttribute])
631
                ->column();
632 126
            $slugs2 = $this->getChildren()
633
                ->select([$this->itemAttribute])
634 126
                ->column();
635 9
            if (array_intersect($slugs1, $slugs2)) {
636
                $event->isValid = false;
637 117
            }
638 12
        }
639
    }
640
641 105
    /**
642 12
     *
643
     */
644
    public function afterDelete()
645 93
    {
646 12
        if ($this->operation !== static::OPERATION_DELETE_ALL) {
647
            foreach ($this->owner->children as $child) {
648 81
                /** @var self $child */
649
                if ($this->owner->next === null) {
650
                    $child->appendTo($this->owner->parent)->save();
651
                } else {
652
                    $child->insertBefore($this->owner->next)->save();
653 6
                }
654
            }
655 6
        }
656
        $this->operation = null;
657 6
        $this->node      = null;
658 6
    }
659 6
    
660
    
661 6
    
662 6
    /**
663 6
     * @param bool $forInsertNear
664
     * @throws Exception
665 6
     */
666 6
    protected function checkNode($forInsertNear = false)
667 3
    {
668 6
        if ($forInsertNear && $this->node->isRoot()) {
0 ignored issues
show
Bug introduced by
The method isRoot does only exist in paulzi\materializedPath\MaterializedPathBehavior, but not in yii\db\ActiveRecord.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
669 3
            throw new Exception('Can not move a node before/after root.');
670 3
        }
671 6
        if ($this->node->getIsNewRecord()) {
0 ignored issues
show
Bug introduced by
The method getIsNewRecord does only exist in yii\db\ActiveRecord, but not in paulzi\materializedPath\MaterializedPathBehavior.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
672
            throw new Exception('Can not move a node when the target node is new record.');
673 6
        }
674 6
675
        if ($this->owner->equals($this->node)) {
0 ignored issues
show
Bug introduced by
It seems like $this->node can also be of type null or object<paulzi\materializ...terializedPathBehavior>; however, yii\db\ActiveRecord::equals() does only seem to accept object<yii\db\ActiveRecord>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
676
            throw new Exception('Can not move a node when the target node is same.');
677
        }
678
679
        if ($this->node->isChildOf($this->owner)) {
0 ignored issues
show
Bug introduced by
The method isChildOf does only exist in paulzi\materializedPath\MaterializedPathBehavior, but not in yii\db\ActiveRecord.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
680
            throw new Exception('Can not move a node when the target node is child.');
681 66
        }
682
    }
683 66
684 42
    /**
685
     * Make root operation internal handler
686 42
     */
687 42
    protected function makeRootInternal()
688 42
    {
689 42
        $item = $this->owner->getAttribute($this->itemAttribute);
690
691 42
        if ($item !== null) {
692
            $this->owner->setAttribute($this->pathAttribute, $item);
693 42
        }
694 42
695 42
        if ($this->sortable !== false) {
696
            $this->owner->setAttribute($this->behavior->sortAttribute, 0);
697 42
        }
698 42
699 21
        if ($this->treeAttribute !== null) {
700 21
            if ($this->owner->getOldAttribute($this->treeAttribute) !== $this->owner->getAttribute($this->treeAttribute)) {
701 21
                $this->owner->setAttribute($this->treeAttribute, $this->owner->getAttribute($this->treeAttribute));
702
            } elseif (!$this->owner->getIsNewRecord()) {
703 42
                $this->owner->setAttribute($this->treeAttribute, $this->owner->getPrimaryKey());
704 42
            }
705
        }
706
707
        $this->owner->setAttribute($this->depthAttribute, $this->rootDepthValue);
708
    }
709
710
    /**
711 60
     * Append to operation internal handler
712
     * @param bool $append
713 60
     * @throws Exception
714 39
     */
715 View Code Duplication
    protected function insertIntoInternal($append)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
716 39
    {
717 39
        $this->checkNode(false);
718 39
        $item = $this->owner->getAttribute($this->itemAttribute);
719 39
720
        if ($item !== null) {
721 39
            $path = $this->node->getAttribute($this->pathAttribute);
0 ignored issues
show
Bug introduced by
The method getAttribute does only exist in yii\db\ActiveRecord, but not in paulzi\materializedPath\MaterializedPathBehavior.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
722
            $this->owner->setAttribute($this->pathAttribute, $path . $this->delimiter . $item);
723 39
        }
724 39
725 39
        $this->owner->setAttribute($this->depthAttribute, $this->node->getAttribute($this->depthAttribute) + 1);
726
727 39
        if ($this->treeAttribute !== null) {
728 39
            $this->owner->setAttribute($this->treeAttribute, $this->node->getAttribute($this->treeAttribute));
729 18
        }
730 18
731 21
        if ($this->sortable !== false) {
732
            if ($append) {
733 39
                $this->owner->moveLast();
734 39
            } else {
735
                $this->owner->moveFirst();
736
            }
737
        }
738
    }
739 9
740
    /**
741 9
     * Insert operation internal handler
742
     * @param bool $forward
743
     * @throws Exception
744 6
     */
745 6 View Code Duplication
    protected function insertNearInternal($forward)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
746 6
    {
747 6
        $this->checkNode(true);
748
        $item = $this->owner->getAttribute($this->itemAttribute);
749
750
        if ($item !== null) {
751
            $path = $this->node->getParentPath();
0 ignored issues
show
Bug introduced by
The method getParentPath does only exist in paulzi\materializedPath\MaterializedPathBehavior, but not in yii\db\ActiveRecord.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
752
            $this->owner->setAttribute($this->pathAttribute, $path . $this->delimiter . $item);
753
        }
754 57
755
        $this->owner->setAttribute($this->depthAttribute, $this->node->getAttribute($this->depthAttribute));
0 ignored issues
show
Bug introduced by
The method getAttribute does only exist in yii\db\ActiveRecord, but not in paulzi\materializedPath\MaterializedPathBehavior.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
756 57
757 57
        if ($this->treeAttribute !== null) {
758
            $this->owner->setAttribute($this->treeAttribute, $this->node->getAttribute($this->treeAttribute));
759 57
        }
760 57
761 57
        if ($this->sortable !== false) {
762 57
            if ($forward) {
763 54
                $this->owner->moveAfter($this->node);
764 54
            } else {
765 54
                $this->owner->moveBefore($this->node);
766 57
            }
767
        }
768 57
    }
769 30
770 30
    /**
771 30
     * @return int
772 30
     */
773
    protected function deleteWithChildrenInternal()
774 57
    {
775 15
        if (!$this->owner->beforeDelete()) {
776 15
            return false;
777
        }
778 57
        $result = $this->owner->deleteAll($this->getDescendants(null, true)->where);
779 30
        $this->owner->setOldAttributes(null);
780 30
        $this->owner->afterDelete();
781 30
        return $result;
782
    }
783 57
784 30
    /**
785 30
     * @param array $changedAttributes
786 57
     * @throws Exception
787
     */
788
    protected function moveNode($changedAttributes)
789
    {
790
        $path = isset($changedAttributes[$this->pathAttribute]) ? $changedAttributes[$this->pathAttribute] : $this->owner->getAttribute($this->pathAttribute);
791
        $update = [];
792
        $condition = [
793
            'and',
794 66
            ['like', "[[{$this->pathAttribute}]]", $this->getLike($path), false],
795
        ];
796 66
        if ($this->treeAttribute !== null) {
797 66
            $tree = isset($changedAttributes[$this->treeAttribute]) ? $changedAttributes[$this->treeAttribute] : $this->owner->getAttribute($this->treeAttribute);
798 66
            $condition[] = [$this->treeAttribute => $tree];
799 6
        }
800
        $params = [];
801 63
802
        if (isset($changedAttributes[$this->pathAttribute])) {
803
            $update['path']     = new Expression($this->concatExpression([':pathNew', $this->substringExpression('[[path]]', 'LENGTH(:pathOld) + 1', 'LENGTH([[path]]) - LENGTH(:pathOld)')]));
804
            $params[':pathOld'] = $path;
805
            $params[':pathNew'] = $this->owner->getAttribute($this->pathAttribute);
806
        }
807 123
808
        if ($this->treeAttribute !== null && isset($changedAttributes[$this->treeAttribute])) {
809 123
            $update[$this->treeAttribute] = $this->owner->getAttribute($this->treeAttribute);
810 123
        }
811 123
812
        if ($this->depthAttribute !== null && isset($changedAttributes[$this->depthAttribute])) {
813 123
            $delta = $this->owner->getAttribute($this->depthAttribute) - $changedAttributes[$this->depthAttribute];
814
            $update[$this->depthAttribute] = new Expression("[[{$this->depthAttribute}]]" . sprintf('%+d', $delta));
815
        }
816
817
        if (!empty($update)) {
818
            $this->owner->updateAll($update, $condition, $params);
819
        }
820 87
    }
821
822 87
    /**
823 87
     * @param string $path
824 87
     * @param string $delimiter
825 42
     * @param bool $asArray = false
826 42
     * @return null|string|array
827 42
     */
828
    protected static function getParentPathInternal($path, $delimiter, $asArray = false)
829 45
    {
830 45
        $path = explode($delimiter, $path);
831 39
        array_pop($path);
832 39
        if ($asArray) {
833 39
            return $path;
834
        }
835 6
        return count($path) > 0 ? implode($delimiter, $path) : null;
836 6
    }
837 6
838 87
    /**
839 87
     * @return array
840
     */
841 87
    protected function treeCondition()
842 87
    {
843 87
        $tableName = $this->owner->tableName();
844 87
        if ($this->treeAttribute === null) {
845
            return [];
846
        } else {
847
            return ["{$tableName}.[[{$this->treeAttribute}]]" => $this->owner->getAttribute($this->treeAttribute)];
848
        }
849
    }
850
851 117
    /**
852
     * @return \yii\db\ActiveQuery
853 117
     */
854
    protected function getSortableQuery()
855
    {
856
        switch ($this->operation) {
857
            case self::OPERATION_PREPEND_TO:
858
            case self::OPERATION_APPEND_TO:
859
                $path  = $this->node->getAttribute($this->pathAttribute);
0 ignored issues
show
Bug introduced by
The method getAttribute does only exist in yii\db\ActiveRecord, but not in paulzi\materializedPath\MaterializedPathBehavior.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
860 33
                $depth = $this->node->getAttribute($this->depthAttribute) + 1;
861
                break;
862 33
863 22
            case self::OPERATION_INSERT_BEFORE:
864
            case self::OPERATION_INSERT_AFTER:
865 11
                $path  = $this->node->getParentPath();
0 ignored issues
show
Bug introduced by
The method getParentPath does only exist in paulzi\materializedPath\MaterializedPathBehavior, but not in yii\db\ActiveRecord.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
866
                $depth = $this->node->getAttribute($this->depthAttribute);
867
                break;
868 30
869
            default:
870 30
                $path  = $this->getParentPath();
871 10
                $depth = $this->owner->getAttribute($this->depthAttribute);
872
        }
873 20
        $tableName = $this->owner->tableName();
874
875
        return $this->owner->find()
876
            ->andWhere($this->treeCondition())
877
            ->andWhere($path !== null ? ['like', "{$tableName}.[[{$this->pathAttribute}]]", $this->getLike($path), false] : '1=0')
878
            ->andWhere(["{$tableName}.[[{$this->depthAttribute}]]" => $depth]);
879
    }
880
881
    /**
882
     * @param string $path
883
     * @return string
884
     */
885
    protected function getLike($path)
886
    {
887
        return strtr($path . $this->delimiter, ['%' => '\%', '_' => '\_', '\\' => '\\\\']) . '%';
888
    }
889
890
    /**
891
     * @param array $items
892
     * @return string
893
     */
894
    protected function concatExpression($items)
895
    {
896
        if ($this->owner->getDb()->driverName === 'sqlite' || $this->owner->getDb()->driverName === 'pgsql') {
897
            return implode(' || ', $items);
898
        }
899
        return 'CONCAT(' . implode(',', $items) . ')';
900
    }
901
902
    protected function substringExpression($string, $from, $length)
903
    {
904
        if ($this->owner->getDb()->driverName === 'sqlite') {
905
            return "SUBSTR({$string}, {$from}, {$length})";
906
        }
907
        return "SUBSTRING({$string}, {$from}, {$length})";
908
    }
909
}
910