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
Push — master ( af10c0...a54314 )
by Pavel
02:45
created

MaterializedPathBehavior::concatExpression()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 7
ccs 0
cts 0
cp 0
rs 9.4286
cc 3
eloc 4
nc 2
nop 1
crap 12
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 126
     * @param int $depth = null
563
     * @return static
564 126
     */
565 9
    public function populateTree($depth = null)
566
    {
567 117
        /** @var static[] $nodes */
568 12
        $nodes = $this
569
            ->getDescendants($depth)
570
            ->indexBy($this->itemAttribute)
571 105
            ->all();
572 12
573
        $relates = [];
574
        foreach ($nodes as $key => $node) {
575 93
            $path = $node->getParentPath(false, true);
576 12
            $parentKey = array_pop($path);
577
            if (!isset($relates[$parentKey])) {
578 81
                $relates[$parentKey] = [];
579
            }
580
            $relates[$parentKey][] = $node;
581
        }
582
583
        $nodes[$this->owner->{$this->itemAttribute}] = $this->owner;
584 105
        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 39
        }
587
588 39
        return $this->owner;
589 39
    }
590 39
591
    /**
592
     * @param string|bool $path
593 39
     * @param bool $asArray = false
594 39
     * @return null|string|array
595
     */
596 39
    public function getParentPath($path = false, $asArray = false)
597 39
    {
598 39
        if ($path === false) {
599 39
            $path = $this->owner->getAttribute($this->pathAttribute);
600 39
        }
601 21
        $path = explode($this->delimiter, $path);
602 21
        array_pop($path);
603 39
        if ($asArray) {
604 39
            return $path;
605 39
        }
606
        return count($path) > 0 ? implode($this->delimiter, $path) : null;
607 39
    }
608 39
609 39
    /**
610 39
     * @param bool $forInsertNear
611 39
     * @throws Exception
612 39
     */
613 39
    protected function checkNode($forInsertNear = false)
614 39
    {
615 39
        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
            throw new Exception('Can not move a node before/after root.');
617 39
        }
618 39
        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 39
            throw new Exception('Can not move a node when the target node is new record.');
620 39
        }
621 39
622 39
        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 39
            throw new Exception('Can not move a node when the target node is same.');
624
        }
625 39
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
            throw new Exception('Can not move a node when the target node is child.');
628 39
        }
629 39
    }
630 39
631 39
    /**
632 39
     * @param int $to
633
     * @param bool $forward
634 39
     */
635 39
    protected function moveTo($to, $forward)
636
    {
637
        $this->owner->setAttribute($this->sortAttribute, $to + ($forward ? 1 : -1));
638
639
        $tableName = $this->owner->tableName();
640 6
        $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
        $like = strtr($path . $this->delimiter, ['%' => '\%', '_' => '\_', '\\' => '\\\\']);
642 6
643
        $joinCondition = [
644 6
            'and',
645 6
            ['like', "n.[[{$this->pathAttribute}]]", $like . '%', false],
646 6
            [
647
                "n.[[{$this->depthAttribute}]]" => $this->node->getAttribute($this->depthAttribute),
648 6
                "n.[[{$this->sortAttribute}]]"  => new Expression("{$tableName}.[[{$this->sortAttribute}]] " . ($forward ? '+' : '-') . " 1"),
649 6
            ],
650 6
        ];
651
        if (!$this->owner->getIsNewRecord()) {
652 6
            $joinCondition[] = ['<>', "n.[[{$this->pathAttribute}]]", $this->owner->getAttribute($this->pathAttribute)];
653 6
        }
654 3
        if ($this->treeAttribute !== null) {
655 6
            $joinCondition[] = ["n.[[{$this->treeAttribute}]]" => new Expression("{$tableName}.[[{$this->treeAttribute}]]")];
656 3
        }
657 3
658 6
        $unallocated = (new Query())
659
            ->select("{$tableName}.[[{$this->sortAttribute}]]")
660 6
            ->from("{$tableName}")
661 6
            ->leftJoin("{$tableName} n", $joinCondition)
662
            ->where([
663
                'and',
664
                ['like', "{$tableName}.[[{$this->pathAttribute}]]", $like . '%', false],
665
                $this->treeCondition(),
666
                [$forward ? '>=' : '<=', "{$tableName}.[[{$this->sortAttribute}]]", $to],
667
                [
668 66
                    "{$tableName}.[[{$this->depthAttribute}]]" => $this->node->getAttribute($this->depthAttribute),
669
                    "n.[[{$this->sortAttribute}]]"             => null,
670 66
                ],
671 42
            ])
672
            ->orderBy(["{$tableName}.[[{$this->sortAttribute}]]" => $forward ? SORT_ASC : SORT_DESC])
673 42
            ->limit(1)
674 42
            ->scalar($this->owner->getDb());
675 42
676 42
        $this->owner->updateAll(
677
            [$this->sortAttribute => new Expression("[[{$this->sortAttribute}]] " . ($forward ? '+' : '-') . " 1")],
678 42
            [
679
                'and',
680 42
                ['like', "[[{$this->pathAttribute}]]", $like . '%', false],
681 42
                $this->treeCondition(),
682 42
                ["[[{$this->depthAttribute}]]" => $this->node->getAttribute($this->depthAttribute)],
683
                ['between', $this->sortAttribute, $forward ? $to + 1 : $unallocated, $forward ? $unallocated : $to - 1],
684 42
            ]
685 42
        );
686 42
    }
687
688 42
    /**
689 42
     * Make root operation internal handler
690 42
     */
691 42
    protected function makeRootInternal()
692 24
    {
693 24
        $item = $this->owner->getAttribute($this->itemAttribute);
694 12
695
        if ($item !== null) {
696 42
            $this->owner->setAttribute($this->pathAttribute, $item);
697 42
        }
698 42
699
        if ($this->sortAttribute !== null) {
700
            $this->owner->setAttribute($this->sortAttribute, 0);
701
        }
702
703
        if ($this->treeAttribute !== null) {
704
            if ($this->owner->getOldAttribute($this->treeAttribute) !== $this->owner->getAttribute($this->treeAttribute)) {
705 60
                $this->owner->setAttribute($this->treeAttribute, $this->owner->getAttribute($this->treeAttribute));
706
            } elseif (!$this->owner->getIsNewRecord()) {
707 60
                $this->owner->setAttribute($this->treeAttribute, $this->owner->getPrimaryKey());
708 39
            }
709
        }
710 39
711 39
        $this->owner->setAttribute($this->depthAttribute, $this->rootDepthValue);
712 39
    }
713 39
714
    /**
715 39
     * Append to operation internal handler
716
     * @param bool $append
717 39
     * @throws Exception
718 39
     */
719 39
    protected function insertIntoInternal($append)
720
    {
721 39
        $this->checkNode(false);
722 39
        $item = $this->owner->getAttribute($this->itemAttribute);
723 39
724 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...
725
            $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
            $this->owner->setAttribute($this->pathAttribute, $path . $this->delimiter . $item);
727
        }
728
729 9
        $this->owner->setAttribute($this->depthAttribute, $this->node->getAttribute($this->depthAttribute) + 1);
730
731 9 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
            $this->owner->setAttribute($this->treeAttribute, $this->node->getAttribute($this->treeAttribute));
733
        }
734 6
735 6
        if ($this->sortAttribute !== null) {
736 6
            $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 6
            $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
                !$this->owner->getIsNewRecord() && (int)$to === $this->owner->getAttribute($this->sortAttribute)
740
                && !$this->owner->getDirtyAttributes([$this->pathAttribute])
741
            ) {
742
            } elseif ($to !== null) {
743
                $to += $append ? $this->step : -$this->step;
744 57
            } else {
745
                $to = 0;
746 57
            }
747 57
            $this->owner->setAttribute($this->sortAttribute, $to);
748 57
        }
749
    }
750 57
751 57
    /**
752 57
     * Insert operation internal handler
753 57
     * @param bool $forward
754 54
     * @throws Exception
755 54
     */
756 54
    protected function insertNearInternal($forward)
757 57
    {
758
        $this->checkNode(true);
759 57
        $item = $this->owner->getAttribute($this->itemAttribute);
760 30
761 30 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 30
            $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 30
            $this->owner->setAttribute($this->pathAttribute, $path . $this->delimiter . $item);
764
        }
765 57
766 15
        $this->owner->setAttribute($this->depthAttribute, $this->node->getAttribute($this->depthAttribute));
767 15
768 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 57
            $this->owner->setAttribute($this->treeAttribute, $this->node->getAttribute($this->treeAttribute));
770 30
        }
771 30
772 30
        if ($this->sortAttribute !== null) {
773
            $this->moveTo($this->node->getAttribute($this->sortAttribute), $forward);
774 57
        }
775 30
    }
776 30
777 57
    /**
778
     * @return int
779
     */
780
    protected function deleteWithChildrenInternal()
781
    {
782
        if (!$this->owner->beforeDelete()) {
783 54
            return false;
784
        }
785 54
        $result = $this->owner->deleteAll($this->getDescendants(null, true)->where);
786 15
        $this->owner->setOldAttributes(null);
787 15
        $this->owner->afterDelete();
788 54
        return $result;
789 54
    }
790 54
791
    /**
792
     * @param array $changedAttributes
793
     * @throws Exception
794
     */
795
    protected function moveNode($changedAttributes)
796 114
    {
797
        $path = isset($changedAttributes[$this->pathAttribute]) ? $changedAttributes[$this->pathAttribute] : $this->owner->getAttribute($this->pathAttribute);
798 114
        $like = strtr($path . $this->delimiter, ['%' => '\%', '_' => '\_', '\\' => '\\\\']);
799 114
        $update = [];
800 114
        $condition = [
801
            'and',
802 114
            ['like', "[[{$this->pathAttribute}]]", $like . '%', false],
803
        ];
804
        if ($this->treeAttribute !== null) {
805
            $tree = isset($changedAttributes[$this->treeAttribute]) ? $changedAttributes[$this->treeAttribute] : $this->owner->getAttribute($this->treeAttribute);
806
            $condition[] = [$this->treeAttribute => $tree];
807
        }
808
        $params = [];
809
810 33
        if (isset($changedAttributes[$this->pathAttribute])) {
811
            $update['path']     = new Expression($this->concatExpression([':pathNew', $this->substringExpression('[[path]]', 'LENGTH(:pathOld) + 1', 'LENGTH([[path]]) - LENGTH(:pathOld)')]));
812 33
            $params[':pathOld'] = $path;
813 22
            $params[':pathNew'] = $this->owner->getAttribute($this->pathAttribute);
814
        }
815 11
816
        if ($this->treeAttribute !== null && isset($changedAttributes[$this->treeAttribute])) {
817
            $update[$this->treeAttribute] = $this->owner->getAttribute($this->treeAttribute);
818 30
        }
819
820 30
        if ($this->depthAttribute !== null && isset($changedAttributes[$this->depthAttribute])) {
821 10
            $delta = $this->owner->getAttribute($this->depthAttribute) - $changedAttributes[$this->depthAttribute];
822
            $update[$this->depthAttribute] = new Expression("[[{$this->depthAttribute}]]" . sprintf('%+d', $delta));
823 20
        }
824
825
        if (!empty($update)) {
826
            $this->owner->updateAll($update, $condition, $params);
827
        }
828
    }
829
830
    /**
831
     * @return array
832
     */
833
    protected function treeCondition()
834
    {
835
        $tableName = $this->owner->tableName();
836
        if ($this->treeAttribute === null) {
837
            return [];
838
        } else {
839
            return ["{$tableName}.[[{$this->treeAttribute}]]" => $this->owner->getAttribute($this->treeAttribute)];
840
        }
841
    }
842
843
    /**
844
     * @param array $items
845
     * @return string
846
     */
847
    protected function concatExpression($items)
848
    {
849
        if ($this->owner->getDb()->driverName === 'sqlite' || $this->owner->getDb()->driverName === 'pgsql') {
850
            return implode(' || ', $items);
851
        }
852
        return 'CONCAT(' . implode(',', $items) . ')';
853
    }
854
855
    protected function substringExpression($string, $from, $length)
856
    {
857
        if ($this->owner->getDb()->driverName === 'sqlite') {
858
            return "SUBSTR({$string}, {$from}, {$length})";
859
        }
860
        return "SUBSTRING({$string}, {$from}, {$length})";
861
    }
862
}
863