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 (#8)
by Haru
04:58 queued 02:20
created

MaterializedPathBehavior::moveNode()   D

Complexity

Conditions 10
Paths 96

Size

Total Lines 34
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 10

Importance

Changes 4
Bugs 3 Features 0
Metric Value
c 4
b 3
f 0
dl 0
loc 34
ccs 28
cts 28
cp 1
rs 4.8197
cc 10
eloc 22
nc 96
nop 1
crap 10

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 Yii;
11
use yii\base\Behavior;
12
use yii\base\Exception;
13
use yii\base\NotSupportedException;
14
use yii\db\ActiveRecord;
15
use yii\db\Expression;
16
use yii\db\Query;
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 $sortAttribute = 'sort';
48
49
    /**
50
     * @var string
51
     */
52
    public $itemAttribute;
53
54
    /**
55
     * @var string|null
56
     */
57
    public $treeAttribute;
58
59
    /**
60
     * @var string
61
     */
62
    public $delimiter = '/';
63
64
    /**
65
     * @var int
66
     */
67
    public $step = 100;
68
69
    /**
70
     * @var int Value of $depthAttribute for root node.
71
     */
72
    public $rootDepthValue = 0;
73
    
74
    /**
75
     * @var int|null
76
     */
77
    protected $operation;
78
79
    /**
80
     * @var ActiveRecord|self|null
81
     */
82
    protected $node;
83
84
    /**
85
     * @var bool
86
     */
87
    protected $primaryKeyMode = false;
88
89
90
    /**
91
     * @inheritdoc
92
     */
93 192
    public function events()
94
    {
95
        return [
96 192
            ActiveRecord::EVENT_BEFORE_INSERT   => 'beforeSave',
97 192
            ActiveRecord::EVENT_AFTER_INSERT    => 'afterInsert',
98 192
            ActiveRecord::EVENT_BEFORE_UPDATE   => 'beforeSave',
99 192
            ActiveRecord::EVENT_AFTER_UPDATE    => 'afterUpdate',
100 192
            ActiveRecord::EVENT_BEFORE_DELETE   => 'beforeDelete',
101 192
            ActiveRecord::EVENT_AFTER_DELETE    => 'afterDelete',
102 192
        ];
103
    }
104
105
    /**
106
     * @param ActiveRecord $owner
107
     * @throws Exception
108
     */
109 192
    public function attach($owner)
110
    {
111 192
        if ($this->itemAttribute === null) {
112 192
            $primaryKey = $owner->primaryKey();
113 192
            if (!isset($primaryKey[0])) {
114
                throw new Exception('"' . $owner->className() . '" must have a primary key.');
115
            }
116 192
            $this->itemAttribute = $primaryKey[0];
117 192
            $this->primaryKeyMode = true;
118 192
        }
119 192
        parent::attach($owner);
120 192
    }
121
122
    /**
123
     * @param int|null $depth
124
     * @return \yii\db\ActiveQuery
125
     */
126 9
    public function getParents($depth = null)
127
    {
128 9
        $path  = $this->getParentPath();
129 9
        if ($path !== null) {
130 9
            $paths = explode($this->delimiter, $path);
131 9
            if (!$this->primaryKeyMode) {
132 9
                $path  = null;
133 9
                $paths = array_map(
134
                    function ($value) use (&$path) {
135 9
                        return $path = ($path !== null ? $path . $this->delimiter : '') . $value;
136 9
                    },
137
                    $paths
138 9
                );
139 9
            }
140 9
            if ($depth !== null) {
141 9
                $paths = array_slice($paths, -$depth);
142 9
            }
143 9
        } else {
144 3
            $paths = [];
145
        }
146
147 9
        $tableName = $this->owner->tableName();
148 9
        $condition = ['and'];
149 9
        if ($this->primaryKeyMode) {
150 6
            $condition[] = ["{$tableName}.[[{$this->itemAttribute}]]" => $paths];
151 6
        } else {
152 9
            $condition[] = ["{$tableName}.[[{$this->pathAttribute}]]" => $paths];
153
        }
154
155 9
        $query = $this->owner->find()
156 9
            ->andWhere($condition)
157 9
            ->andWhere($this->treeCondition())
158 9
            ->addOrderBy(["{$tableName}.[[{$this->pathAttribute}]]" => SORT_ASC]);
159 9
        $query->multiple = true;
160
161 9
        return $query;
162
    }
163
164
    /**
165
     * @return \yii\db\ActiveQuery
166
     */
167 6
    public function getParent()
168
    {
169 6
        $query = $this->getParents(1)->limit(1);
170 6
        $query->multiple = false;
171 6
        return $query;
172
    }
173
174
    /**
175
     * @return \yii\db\ActiveQuery
176
     */
177 3
    public function getRoot()
178
    {
179 3
        $path = explode($this->delimiter, $this->owner->getAttribute($this->pathAttribute));
180 3
        $path = array_shift($path);
181 3
        $tableName = $this->owner->tableName();
182 3
        $query = $this->owner->find()
183 3
            ->andWhere(["{$tableName}.[[{$this->pathAttribute}]]" => $path])
184 3
            ->andWhere($this->treeCondition())
185 3
            ->limit(1);
186 3
        $query->multiple = false;
187 3
        return $query;
188
    }
189
190
    /**
191
     * @param int|null $depth
192
     * @param bool $andSelf
193
     * @return \yii\db\ActiveQuery
194
     */
195 63
    public function getDescendants($depth = null, $andSelf = false)
196
    {
197 63
        $tableName = $this->owner->tableName();
198 63
        $path = $this->owner->getAttribute($this->pathAttribute);
199 63
        $like = strtr($path . $this->delimiter, ['%' => '\%', '_' => '\_', '\\' => '\\\\']);
200
201 63
        $query = $this->owner->find()
202 63
            ->andWhere(['like', "{$tableName}.[[{$this->pathAttribute}]]", $like . '%', false]);
203
204 63
        if ($andSelf) {
205 9
            $query->orWhere(["{$tableName}.[[{$this->pathAttribute}]]" => $path]);
206 9
        }
207
208 63
        if ($depth !== null) {
209 57
            $query->andWhere(['<=', "{$tableName}.[[{$this->depthAttribute}]]", $this->owner->getAttribute($this->depthAttribute) + $depth]);
210 57
        }
211
212
        $query
213 63
            ->andWhere($this->treeCondition())
214 63
            ->addOrderBy([
215 63
                "{$tableName}.[[{$this->depthAttribute}]]" => SORT_ASC,
216 63
                "{$tableName}.[[{$this->sortAttribute}]]"  => SORT_ASC,
217 63
                "{$tableName}.[[{$this->itemAttribute}]]"  => SORT_ASC,
218 63
            ]);
219 63
        $query->multiple = true;
220
221 63
        return $query;
222
    }
223
224
    /**
225
     * @return \yii\db\ActiveQuery
226
     */
227 51
    public function getChildren()
228
    {
229 51
        return $this->getDescendants(1);
230
    }
231
232
    /**
233
     * @param int|null $depth
234
     * @return \yii\db\ActiveQuery
235
     */
236 3
    public function getLeaves($depth = null)
237
    {
238 3
        $tableName = $this->owner->tableName();
239
        $condition = [
240 3
            'and',
241 3
            ['like', "leaves.[[{$this->pathAttribute}]]",  new Expression($this->concatExpression(["{$tableName}.[[{$this->pathAttribute}]]", ':delimiter']), [':delimiter' => $this->delimiter . '%'])],
242 3
        ];
243
244 3
        if ($this->treeAttribute !== null) {
245 3
            $condition[] = ["leaves.[[{$this->treeAttribute}]]" => new Expression("{$tableName}.[[{$this->treeAttribute}]]")];
246 3
        }
247
248 3
        $query = $this->getDescendants($depth)
249 3
            ->leftJoin("{$tableName} leaves", $condition)
250 3
            ->andWhere(["leaves.[[{$this->pathAttribute}]]" => null]);
251 3
        $query->multiple = true;
252 3
        return $query;
253
    }
254
255
    /**
256
     * @return \yii\db\ActiveQuery
257
     */
258 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...
259
    {
260 3
        $tableName = $this->owner->tableName();
261 3
        $like = strtr($this->getParentPath() . $this->delimiter, ['%' => '\%', '_' => '\_', '\\' => '\\\\']);
262 3
        $query = $this->owner->find()
263 3
            ->andWhere(['like', "{$tableName}.[[{$this->pathAttribute}]]", $like . '%', false])
264 3
            ->andWhere(["{$tableName}.[[{$this->depthAttribute}]]" => $this->owner->getAttribute($this->depthAttribute)])
265 3
            ->andWhere(['<', "{$tableName}.[[{$this->sortAttribute}]]", $this->owner->getAttribute($this->sortAttribute)])
266 3
            ->andWhere($this->treeCondition())
267 3
            ->orderBy(["{$tableName}.[[{$this->sortAttribute}]]" => SORT_DESC])
268 3
            ->limit(1);
269 3
        $query->multiple = false;
270 3
        return $query;
271
    }
272
273
    /**
274
     * @return \yii\db\ActiveQuery
275
     */
276 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...
277
    {
278 6
        $tableName = $this->owner->tableName();
279 6
        $like = strtr($this->getParentPath() . $this->delimiter, ['%' => '\%', '_' => '\_', '\\' => '\\\\']);
280 6
        $query = $this->owner->find()
281 6
            ->andWhere(['like', "{$tableName}.[[{$this->pathAttribute}]]", $like . '%', false])
282 6
            ->andWhere(["{$tableName}.[[{$this->depthAttribute}]]" => $this->owner->getAttribute($this->depthAttribute)])
283 6
            ->andWhere(['>', "{$tableName}.[[{$this->sortAttribute}]]", $this->owner->getAttribute($this->sortAttribute)])
284 6
            ->andWhere($this->treeCondition())
285 6
            ->orderBy(["{$tableName}.[[{$this->sortAttribute}]]" => SORT_ASC])
286 6
            ->limit(1);
287 6
        $query->multiple = false;
288 6
        return $query;
289
    }
290
291
    /**
292
     * @return bool
293
     */
294 72
    public function isRoot()
295
    {
296 72
        return count(explode($this->delimiter, $this->owner->getAttribute($this->pathAttribute))) === 1;
297
    }
298
299
    /**
300
     * @param ActiveRecord $node
301
     * @return bool
302
     */
303 96
    public function isChildOf($node)
304
    {
305 96
        $nodePath  = $node->getAttribute($this->pathAttribute) . $this->delimiter;
306 96
        $result = substr($this->owner->getAttribute($this->pathAttribute), 0, strlen($nodePath)) === $nodePath;
307
308 96
        if ($result && $this->treeAttribute !== null) {
309 9
            $result = $this->owner->getAttribute($this->treeAttribute) === $node->getAttribute($this->treeAttribute);
310 9
        }
311
312 96
        return $result;
313
    }
314
315
    /**
316
     * @return bool
317
     */
318 3
    public function isLeaf()
319
    {
320 3
        return count($this->owner->children) === 0;
321
    }
322
323
    /**
324
     * @return ActiveRecord
325
     */
326 6
    public function makeRoot()
327
    {
328 6
        $this->operation = self::OPERATION_MAKE_ROOT;
329 6
        return $this->owner;
330
    }
331
332
    /**
333
     * @param ActiveRecord $node
334
     * @return ActiveRecord
335
     */
336 33
    public function prependTo($node)
337
    {
338 33
        $this->operation = self::OPERATION_PREPEND_TO;
339 33
        $this->node = $node;
340 33
        return $this->owner;
341
    }
342
343
    /**
344
     * @param ActiveRecord $node
345
     * @return ActiveRecord
346
     */
347 33
    public function appendTo($node)
348
    {
349 33
        $this->operation = self::OPERATION_APPEND_TO;
350 33
        $this->node = $node;
351 33
        return $this->owner;
352
    }
353
354
    /**
355
     * @param ActiveRecord $node
356
     * @return ActiveRecord
357
     */
358 30
    public function insertBefore($node)
359
    {
360 30
        $this->operation = self::OPERATION_INSERT_BEFORE;
361 30
        $this->node = $node;
362 30
        return $this->owner;
363
    }
364
365
    /**
366
     * @param ActiveRecord $node
367
     * @return ActiveRecord
368
     */
369 30
    public function insertAfter($node)
370
    {
371 30
        $this->operation = self::OPERATION_INSERT_AFTER;
372 30
        $this->node = $node;
373 30
        return $this->owner;
374
    }
375
376
    /**
377
     * @return bool|int
378
     * @throws \Exception
379
     * @throws \yii\db\Exception
380
     */
381 9
    public function deleteWithChildren()
382
    {
383 9
        $this->operation = self::OPERATION_DELETE_ALL;
384 9
        if (!$this->owner->isTransactional(ActiveRecord::OP_DELETE)) {
385
            $transaction = $this->owner->getDb()->beginTransaction();
386
            try {
387
                $result = $this->deleteWithChildrenInternal();
388
                if ($result === false) {
389
                    $transaction->rollBack();
390
                } else {
391
                    $transaction->commit();
392
                }
393
                return $result;
394
            } catch (\Exception $e) {
395
                $transaction->rollBack();
396
                throw $e;
397
            }
398
        } else {
399 9
            $result = $this->deleteWithChildrenInternal();
400
        }
401 6
        return $result;
402
    }
403
404
    /**
405
     * @throws Exception
406
     * @throws NotSupportedException
407
     */
408 138
    public function beforeSave()
409
    {
410 138
        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...
411 108
            $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...
412 108
        }
413
414 138
        switch ($this->operation) {
415 138
            case self::OPERATION_MAKE_ROOT:
416 6
                $this->makeRootInternal();
417
418 6
                break;
419 132
            case self::OPERATION_PREPEND_TO:
420 33
                $this->insertIntoInternal(false);
421
422 21
                break;
423 99
            case self::OPERATION_APPEND_TO:
424 33
                $this->insertIntoInternal(true);
425
426 21
                break;
427 66
            case self::OPERATION_INSERT_BEFORE:
428 30
                $this->insertNearInternal(false);
429
430 21
                break;
431
432 36
            case self::OPERATION_INSERT_AFTER:
433 30
                $this->insertNearInternal(true);
434
435 18
                break;
436
437 6
            default:
438 6
                if ($this->owner->getIsNewRecord()) {
439 3
                    throw new NotSupportedException('Method "' . $this->owner->className() . '::insert" is not supported for inserting new nodes.');
440
                }
441
442 3
                $item = $this->owner->getAttribute($this->itemAttribute);
443 3
                $path = $this->getParentPath($this->owner->getAttribute($this->pathAttribute));
444 3
                $this->owner->setAttribute($this->pathAttribute, $path . $this->delimiter . $item);
445 93
        }
446 90
    }
447
448
    /**
449
     * @throws Exception
450
     */
451 33
    public function afterInsert()
452
    {
453 33
        if ($this->operation === self::OPERATION_MAKE_ROOT && $this->treeAttribute !== null && $this->owner->getAttribute($this->treeAttribute) === null) {
454 3
            $id = $this->owner->getPrimaryKey();
455 3
            $this->owner->setAttribute($this->treeAttribute, $id);
456
457 3
            $primaryKey = $this->owner->primaryKey();
458 3 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...
459
                throw new Exception('"' . $this->owner->className() . '" must have a primary key.');
460
            }
461
462 3
            $this->owner->updateAll([$this->treeAttribute => $id], [$primaryKey[0] => $id]);
463 3
        }
464 33
        if ($this->owner->getAttribute($this->pathAttribute) === null) {
465 33
            $primaryKey = $this->owner->primaryKey();
466 33 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...
467
                throw new Exception('"' . $this->owner->className() . '" must have a primary key.');
468
            }
469 33
            $id = $this->owner->getPrimaryKey();
470 33
            if ($this->operation === self::OPERATION_MAKE_ROOT) {
471 3
                $path = $id;
472 3
            } else {
473 30
                $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...
474 30
                if ($this->operation === self::OPERATION_INSERT_BEFORE || $this->operation === self::OPERATION_INSERT_AFTER) {
475 18
                    $path = $this->getParentPath($path);
476 18
                }
477 30
                $path = $path . $this->delimiter . $id;
478
            }
479 33
            $this->owner->setAttribute($this->pathAttribute, $path);
480 33
            $this->owner->updateAll([$this->pathAttribute => $path], [$primaryKey[0] => $id]);
481 33
        }
482 33
        $this->operation = null;
483 33
        $this->node      = null;
484 33
    }
485
486
    /**
487
     * @param \yii\db\AfterSaveEvent $event
488
     */
489 57
    public function afterUpdate($event)
490
    {
491 57
        $this->moveNode($event->changedAttributes);
492 57
        $this->operation = null;
493 57
        $this->node      = null;
494 57
    }
495
496
    /**
497
     * @param \yii\base\ModelEvent $event
498
     * @throws Exception
499
     */
500 18
    public function beforeDelete($event)
501
    {
502 18
        if ($this->owner->getIsNewRecord()) {
503 6
            throw new Exception('Can not delete a node when it is new record.');
504
        }
505 12
        if ($this->isRoot() && $this->operation !== self::OPERATION_DELETE_ALL) {
506 3
            throw new Exception('Method "'. $this->owner->className() . '::delete" is not supported for deleting root nodes.');
507
        }
508 9
        $this->owner->refresh();
509 9
        if ($this->operation !== static::OPERATION_DELETE_ALL && !$this->primaryKeyMode) {
510
            /** @var self $parent */
511 3
            $parent =$this->getParent()->one();
512 3
            $slugs1 = $parent->getChildren()
513 3
                ->andWhere(['<>', $this->itemAttribute, $this->owner->getAttribute($this->itemAttribute)])
514 3
                ->select([$this->itemAttribute])
515 3
                ->column();
516 3
            $slugs2 = $this->getChildren()
517 3
                ->select([$this->itemAttribute])
518 3
                ->column();
519 3
            if (array_intersect($slugs1, $slugs2)) {
520
                $event->isValid = false;
521
            }
522 3
        }
523 9
    }
524
525
    /**
526
     *
527
     */
528 9
    public function afterDelete()
529
    {
530 9
        if ($this->operation !== static::OPERATION_DELETE_ALL) {
531 3
            foreach ($this->owner->children as $child) {
532
                /** @var self $child */
533 3
                if ($this->owner->next === null) {
534
                    $child->appendTo($this->owner->parent)->save();
535
                } else {
536 3
                    $child->insertBefore($this->owner->next)->save();
537
                }
538 3
            }
539 3
        }
540 9
        $this->operation = null;
541 9
        $this->node      = null;
542 9
    }
543
    
544
    /**
545
     * Reorders children with values of $sortAttribute begin from zero.
546
     * 
547
     * @throws \Exception
548
     */
549
    public function reorderChildren()
550
    {
551
        \Yii::$app->getDb()->transaction(function () {
552
            foreach ($this->getChildren()->each() as $i => $child) {
553
                $child->{$this->sortAttribute} = ($i - 1) * $this->step;
554
                $child->save();
555
            }
556
        });
557
    }
558
559
    /**
560
     * Returns descendants nodes as tree with self node in the root.
561
     *
562
     * @param int $depth = null
563
     * @return static
564
     */
565
    public function populateTree($depth = null)
566
    {
567
        /** @var static[] $nodes */
568
        $nodes = $this
569
            ->getDescendants($depth)
570
            ->indexBy($this->itemAttribute)
571
            ->all();
572
573
        $relates = [];
574
        foreach ($nodes as $key => $node) {
575
            $path = $node->getParentPath(false, true);
576
            $parentKey = array_pop($path);
577
            if (!isset($relates[$parentKey])) {
578
                $relates[$parentKey] = [];
579
            }
580
            $relates[$parentKey][] = $node;
581
        }
582
583
        $nodes[$this->owner->{$this->itemAttribute}] = $this->owner;
584
        foreach ($relates as $key => $children) {
585
            $nodes[$key]->populateRelation('children', $children);
0 ignored issues
show
Bug introduced by
The method populateRelation 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...
586
        }
587
588
        return $this->owner;
589
    }
590
591
    /**
592
     * @param string|bool $path
593
     * @param bool $asArray = false
594
     * @return null|string|array
595
     */
596 54
    public function getParentPath($path = false, $asArray = false)
597
    {
598 54
        if ($path === false) {
599 15
            $path = $this->owner->getAttribute($this->pathAttribute);
600 15
        }
601 54
        $path = explode($this->delimiter, $path);
602 54
        array_pop($path);
603 54
        if ($asArray) {
604
            return $path;
605
        }
606 54
        return count($path) > 0 ? implode($this->delimiter, $path) : null;
607
    }
608
609
    /**
610
     * @param bool $forInsertNear
611
     * @throws Exception
612
     */
613 126
    protected function checkNode($forInsertNear = false)
614
    {
615 126
        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...
616 9
            throw new Exception('Can not move a node before/after root.');
617
        }
618 117
        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...
619 12
            throw new Exception('Can not move a node when the target node is new record.');
620
        }
621
622 105
        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...
623 12
            throw new Exception('Can not move a node when the target node is same.');
624
        }
625
626 105
        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...
627 12
            throw new Exception('Can not move a node when the target node is child.');
628
        }
629 81
    }
630
631
    /**
632
     * @param int $to
633
     * @param bool $forward
634
     */
635 39
    protected function moveTo($to, $forward)
636
    {
637 39
        $this->owner->setAttribute($this->sortAttribute, $to + ($forward ? 1 : -1));
638
639 39
        $tableName = $this->owner->tableName();
640 39
        $path = $this->getParentPath($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...
641 39
        $like = strtr($path . $this->delimiter, ['%' => '\%', '_' => '\_', '\\' => '\\\\']);
642
643
        $joinCondition = [
644 39
            'and',
645 39
            ['like', "n.[[{$this->pathAttribute}]]", $like . '%', false],
646
            [
647 39
                "n.[[{$this->depthAttribute}]]" => $this->node->getAttribute($this->depthAttribute),
648 39
                "n.[[{$this->sortAttribute}]]"  => new Expression("{$tableName}.[[{$this->sortAttribute}]] " . ($forward ? '+' : '-') . " 1"),
649 39
            ],
650 39
        ];
651 39
        if (!$this->owner->getIsNewRecord()) {
652 21
            $joinCondition[] = ['<>', "n.[[{$this->pathAttribute}]]", $this->owner->getAttribute($this->pathAttribute)];
653 21
        }
654 39
        if ($this->treeAttribute !== null) {
655 39
            $joinCondition[] = ["n.[[{$this->treeAttribute}]]" => new Expression("{$tableName}.[[{$this->treeAttribute}]]")];
656 39
        }
657
658 39
        $unallocated = (new Query())
659 39
            ->select("{$tableName}.[[{$this->sortAttribute}]]")
660 39
            ->from("{$tableName}")
661 39
            ->leftJoin("{$tableName} n", $joinCondition)
662 39
            ->where([
663 39
                'and',
664 39
                ['like', "{$tableName}.[[{$this->pathAttribute}]]", $like . '%', false],
665 39
                $this->treeCondition(),
666 39
                [$forward ? '>=' : '<=', "{$tableName}.[[{$this->sortAttribute}]]", $to],
667
                [
668 39
                    "{$tableName}.[[{$this->depthAttribute}]]" => $this->node->getAttribute($this->depthAttribute),
669 39
                    "n.[[{$this->sortAttribute}]]"             => null,
670 39
                ],
671 39
            ])
672 39
            ->orderBy(["{$tableName}.[[{$this->sortAttribute}]]" => $forward ? SORT_ASC : SORT_DESC])
673 39
            ->limit(1)
674 39
            ->scalar($this->owner->getDb());
675
676 39
        $this->owner->updateAll(
677 39
            [$this->sortAttribute => new Expression("[[{$this->sortAttribute}]] " . ($forward ? '+' : '-') . " 1")],
678
            [
679 39
                'and',
680 39
                ['like', "[[{$this->pathAttribute}]]", $like . '%', false],
681 39
                $this->treeCondition(),
682 39
                ["[[{$this->depthAttribute}]]" => $this->node->getAttribute($this->depthAttribute)],
683 39
                ['between', $this->sortAttribute, $forward ? $to + 1 : $unallocated, $forward ? $unallocated : $to - 1],
684
            ]
685 39
        );
686 39
    }
687
688
    /**
689
     * Make root operation internal handler
690
     */
691 6
    protected function makeRootInternal()
692
    {
693 6
        $item = $this->owner->getAttribute($this->itemAttribute);
694
695 6
        if ($item !== null) {
696 6
            $this->owner->setAttribute($this->pathAttribute, $item);
697 6
        }
698
699 6
        if ($this->sortAttribute !== null) {
700 6
            $this->owner->setAttribute($this->sortAttribute, 0);
701 6
        }
702
703 6
        if ($this->treeAttribute !== null) {
704 6
            if ($this->owner->getOldAttribute($this->treeAttribute) !== $this->owner->getAttribute($this->treeAttribute)) {
705 3
                $this->owner->setAttribute($this->treeAttribute, $this->owner->getAttribute($this->treeAttribute));
706 6
            } elseif (!$this->owner->getIsNewRecord()) {
707 3
                $this->owner->setAttribute($this->treeAttribute, $this->owner->getPrimaryKey());
708 3
            }
709 6
        }
710
711 6
        $this->owner->setAttribute($this->depthAttribute, $this->rootDepthValue);
712 6
    }
713
714
    /**
715
     * Append to operation internal handler
716
     * @param bool $append
717
     * @throws Exception
718
     */
719 66
    protected function insertIntoInternal($append)
720
    {
721 66
        $this->checkNode(false);
722 42
        $item = $this->owner->getAttribute($this->itemAttribute);
723
724 42 View Code Duplication
        if ($item !== null) {
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...
725 42
            $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...
726 42
            $this->owner->setAttribute($this->pathAttribute, $path . $this->delimiter . $item);
727 42
        }
728
729 42
        $this->owner->setAttribute($this->depthAttribute, $this->node->getAttribute($this->depthAttribute) + 1);
730
731 42 View Code Duplication
        if ($this->treeAttribute !== null) {
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...
732 42
            $this->owner->setAttribute($this->treeAttribute, $this->node->getAttribute($this->treeAttribute));
733 42
        }
734
735 42
        if ($this->sortAttribute !== null) {
736 42
            $to = $this->node->getChildren()->orderBy(null);
0 ignored issues
show
Bug introduced by
The method getChildren 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...
737 42
            $to = $append ? $to->max($this->sortAttribute) : $to->min($this->sortAttribute);
738
            if (
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
739 42
                !$this->owner->getIsNewRecord() && (int)$to === $this->owner->getAttribute($this->sortAttribute)
740 42
                && !$this->owner->getDirtyAttributes([$this->pathAttribute])
741 42
            ) {
742 42
            } elseif ($to !== null) {
743 24
                $to += $append ? $this->step : -$this->step;
744 24
            } else {
745 12
                $to = 0;
746
            }
747 42
            $this->owner->setAttribute($this->sortAttribute, $to);
748 42
        }
749 42
    }
750
751
    /**
752
     * Insert operation internal handler
753
     * @param bool $forward
754
     * @throws Exception
755
     */
756 60
    protected function insertNearInternal($forward)
757
    {
758 60
        $this->checkNode(true);
759 39
        $item = $this->owner->getAttribute($this->itemAttribute);
760
761 39 View Code Duplication
        if ($item !== null) {
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...
762 39
            $path = $this->getParentPath($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...
763 39
            $this->owner->setAttribute($this->pathAttribute, $path . $this->delimiter . $item);
764 39
        }
765
766 39
        $this->owner->setAttribute($this->depthAttribute, $this->node->getAttribute($this->depthAttribute));
767
768 39 View Code Duplication
        if ($this->treeAttribute !== null) {
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...
769 39
            $this->owner->setAttribute($this->treeAttribute, $this->node->getAttribute($this->treeAttribute));
770 39
        }
771
772 39
        if ($this->sortAttribute !== null) {
773 39
            $this->moveTo($this->node->getAttribute($this->sortAttribute), $forward);
774 39
        }
775 39
    }
776
777
    /**
778
     * @return int
779
     */
780 9
    protected function deleteWithChildrenInternal()
781
    {
782 9
        if (!$this->owner->beforeDelete()) {
783
            return false;
784
        }
785 6
        $result = $this->owner->deleteAll($this->getDescendants(null, true)->where);
786 6
        $this->owner->setOldAttributes(null);
787 6
        $this->owner->afterDelete();
788 6
        return $result;
789
    }
790
791
    /**
792
     * @param array $changedAttributes
793
     * @throws Exception
794
     */
795 57
    protected function moveNode($changedAttributes)
796
    {
797 57
        $path = isset($changedAttributes[$this->pathAttribute]) ? $changedAttributes[$this->pathAttribute] : $this->owner->getAttribute($this->pathAttribute);
798 57
        $like = strtr($path . $this->delimiter, ['%' => '\%', '_' => '\_', '\\' => '\\\\']);
799 57
        $update = [];
800
        $condition = [
801 57
            'and',
802 57
            ['like', "[[{$this->pathAttribute}]]", $like . '%', false],
803 57
        ];
804 57
        if ($this->treeAttribute !== null) {
805 54
            $tree = isset($changedAttributes[$this->treeAttribute]) ? $changedAttributes[$this->treeAttribute] : $this->owner->getAttribute($this->treeAttribute);
806 54
            $condition[] = [$this->treeAttribute => $tree];
807 54
        }
808 57
        $params = [];
809
810 57
        if (isset($changedAttributes[$this->pathAttribute])) {
811 30
            $update['path']     = new Expression($this->concatExpression([':pathNew', $this->substringExpression('[[path]]', 'LENGTH(:pathOld) + 1', 'LENGTH([[path]]) - LENGTH(:pathOld)')]));
812 30
            $params[':pathOld'] = $path;
813 30
            $params[':pathNew'] = $this->owner->getAttribute($this->pathAttribute);
814 30
        }
815
816 57
        if ($this->treeAttribute !== null && isset($changedAttributes[$this->treeAttribute])) {
817 15
            $update[$this->treeAttribute] = $this->owner->getAttribute($this->treeAttribute);
818 15
        }
819
820 57
        if ($this->depthAttribute !== null && isset($changedAttributes[$this->depthAttribute])) {
821 30
            $delta = $this->owner->getAttribute($this->depthAttribute) - $changedAttributes[$this->depthAttribute];
822 30
            $update[$this->depthAttribute] = new Expression("[[{$this->depthAttribute}]]" . sprintf('%+d', $delta));
823 30
        }
824
825 57
        if (!empty($update)) {
826 30
            $this->owner->updateAll($update, $condition, $params);
827 30
        }
828 57
    }
829
830
    /**
831
     * @return array
832
     */
833 114
    protected function treeCondition()
834
    {
835 114
        $tableName = $this->owner->tableName();
836 114
        if ($this->treeAttribute === null) {
837 114
            return [];
838
        } else {
839 114
            return ["{$tableName}.[[{$this->treeAttribute}]]" => $this->owner->getAttribute($this->treeAttribute)];
840
        }
841
    }
842
843
    /**
844
     * @param array $items
845
     * @return string
846
     */
847 33
    protected function concatExpression($items)
848
    {
849 33
        if ($this->owner->getDb()->driverName === 'sqlite' || $this->owner->getDb()->driverName === 'pgsql') {
850 22
            return implode(' || ', $items);
851
        }
852 11
        return 'CONCAT(' . implode(',', $items) . ')';
853
    }
854
855 30
    protected function substringExpression($string, $from, $length)
856
    {
857 30
        if ($this->owner->getDb()->driverName === 'sqlite') {
858 10
            return "SUBSTR({$string}, {$from}, {$length})";
859
        }
860 20
        return "SUBSTRING({$string}, {$from}, {$length})";
861
    }
862
}
863