NestedIntervalsBehavior   F
last analyzed

Complexity

Total Complexity 201

Size/Duplication

Total Lines 1151
Duplicated Lines 9.38 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 95.03%

Importance

Changes 0
Metric Value
wmc 201
lcom 1
cbo 10
dl 108
loc 1151
ccs 555
cts 584
cp 0.9503
rs 0.996
c 0
b 0
f 0

43 Methods

Rating   Name   Duplication   Size   Complexity  
A getParent() 0 9 1
A getRoot() 0 10 1
A getChildren() 0 4 1
A isChildOf() 0 11 4
A afterInsert() 0 10 4
A afterDelete() 0 8 2
A findPrependRange() 13 13 2
A findAppendRange() 13 13 2
A findInsertBeforeRange() 17 17 2
A findInsertAfterRange() 17 17 2
B findUnallocatedAll() 0 16 10
A events() 0 11 1
A getParents() 3 20 2
B getDescendants() 3 22 6
A getLeaves() 0 19 2
A getPrev() 15 15 1
A getNext() 15 15 1
B populateTree() 0 43 9
A isRoot() 0 4 1
A isLeaf() 0 4 1
A makeRoot() 0 5 1
A prependTo() 0 6 1
A appendTo() 0 6 1
A insertBefore() 0 6 1
A insertAfter() 0 6 1
A preDeleteWithChildren() 0 4 1
A deleteWithChildren() 0 22 4
A optimize() 0 16 3
B beforeInsert() 0 40 9
C beforeUpdate() 0 38 14
B afterUpdate() 12 38 10
A beforeDelete() 0 10 4
A getPrimaryKey() 0 8 2
A getNodeBehavior() 0 9 3
A deleteWithChildrenInternal() 0 10 2
F insertNode() 0 77 42
B moveNode() 0 53 6
A moveNodeAsRoot() 0 29 2
A optimizeInternal() 0 11 1
F optimizeAttribute() 0 121 22
B findUnallocated() 0 32 10
A shift() 0 15 4
A treeCondition() 0 9 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like NestedIntervalsBehavior often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use NestedIntervalsBehavior, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @link https://github.com/paulzi/yii2-nested-intervals
4
 * @copyright Copyright (c) 2015 PaulZi <[email protected]>
5
 * @license MIT (https://github.com/paulzi/yii2-nested-intervals/blob/master/LICENSE)
6
 */
7
8
namespace paulzi\nestedintervals;
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
 * Nested Intervals Behavior for Yii2
20
 * @author PaulZi <[email protected]>
21
 *
22
 * @property ActiveRecord $owner
23
 */
24
class NestedIntervalsBehavior extends Behavior
25
{
26
27
    const OPERATION_MAKE_ROOT       = 1;
28
    const OPERATION_PREPEND_TO      = 2;
29
    const OPERATION_APPEND_TO       = 3;
30
    const OPERATION_INSERT_BEFORE   = 4;
31
    const OPERATION_INSERT_AFTER    = 5;
32
    const OPERATION_DELETE_ALL      = 6;
33
34
    /**
35
     * @var string|null
36
     */
37
    public $treeAttribute;
38
39
    /**
40
     * @var string
41
     */
42
    public $leftAttribute = 'lft';
43
44
    /**
45
     * @var string
46
     */
47
    public $rightAttribute = 'rgt';
48
49
    /**
50
     * @var string
51
     */
52
    public $depthAttribute = 'depth';
53
54
    /**
55
     * @var int[]
56
     */
57
    public $range = [0, 2147483647];
58
59
    /**
60
     * @var int|int[] Average amount of children in each level
61
     */
62
    public $amountOptimize = 10;
63
64
    /**
65
     * @var float
66
     */
67
    public $reserveFactor = 1;
68
69
    /**
70
     * @var bool
71
     */
72
    public $noPrepend = false;
73
74
    /**
75
     * @var bool
76
     */
77
    public $noAppend = false;
78
79
    /**
80
     * @var bool
81
     */
82
    public $noInsert = false;
83
84
    /**
85
     * @var string|null
86
     */
87
    protected $operation;
88
89
    /**
90
     * @var ActiveRecord|self|null
91
     */
92
    protected $node;
93
94
    /**
95
     * @var string
96
     */
97
    protected $treeChange;
98
99
100
    /**
101
     * @inheritdoc
102
     */
103 299
    public function events()
104
    {
105
        return [
106 299
            ActiveRecord::EVENT_BEFORE_INSERT   => 'beforeInsert',
107
            ActiveRecord::EVENT_AFTER_INSERT    => 'afterInsert',
108
            ActiveRecord::EVENT_BEFORE_UPDATE   => 'beforeUpdate',
109
            ActiveRecord::EVENT_AFTER_UPDATE    => 'afterUpdate',
110
            ActiveRecord::EVENT_BEFORE_DELETE   => 'beforeDelete',
111
            ActiveRecord::EVENT_AFTER_DELETE    => 'afterDelete',
112
        ];
113
    }
114
115
    /**
116
     * @param int|null $depth
117
     * @return \yii\db\ActiveQuery
118
     */
119 73
    public function getParents($depth = null)
120
    {
121 73
        $tableName = $this->owner->tableName();
122
        $condition = [
123 73
            'and',
124 73
            ['<', "{$tableName}.[[{$this->leftAttribute}]]",  $this->owner->getAttribute($this->leftAttribute)],
125 73
            ['>', "{$tableName}.[[{$this->rightAttribute}]]", $this->owner->getAttribute($this->rightAttribute)],
126
        ];
127 73 View Code Duplication
        if ($depth !== 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...
128 73
            $condition[] = ['>=', "{$tableName}.[[{$this->depthAttribute}]]", $this->owner->getAttribute($this->depthAttribute) - $depth];
129
        }
130
131 73
        $query = $this->owner->find()
132 73
            ->andWhere($condition)
133 73
            ->andWhere($this->treeCondition())
134 73
            ->addOrderBy(["{$tableName}.[[{$this->leftAttribute}]]" => SORT_ASC]);
135 73
        $query->multiple = true;
136
137 73
        return $query;
138
    }
139
140
    /**
141
     * @return \yii\db\ActiveQuery
142
     */
143 68
    public function getParent()
144
    {
145 68
        $tableName = $this->owner->tableName();
146 68
        $query = $this->getParents(1)
147 68
            ->orderBy(["{$tableName}.[[{$this->leftAttribute}]]" => SORT_DESC])
148 68
            ->limit(1);
149 68
        $query->multiple = false;
150 68
        return $query;
151
    }
152
153
    /**
154
     * @return \yii\db\ActiveQuery
155
     */
156 21
    public function getRoot()
157
    {
158 21
        $tableName = $this->owner->tableName();
159 21
        $query = $this->owner->find()
160 21
            ->andWhere(["{$tableName}.[[{$this->leftAttribute}]]" => $this->range[0]])
161 21
            ->andWhere($this->treeCondition())
162 21
            ->limit(1);
163 21
        $query->multiple = false;
164 21
        return $query;
165
    }
166
167
    /**
168
     * @param int|null $depth
169
     * @param bool $andSelf
170
     * @param bool $backOrder
171
     * @return \yii\db\ActiveQuery
172
     */
173 174
    public function getDescendants($depth = null, $andSelf = false, $backOrder = false)
174
    {
175 174
        $tableName = $this->owner->tableName();
176 174
        $attribute = $backOrder ? $this->rightAttribute : $this->leftAttribute;
177
        $condition = [
178 174
            'and',
179 174
            [$andSelf ? '>=' : '>', "{$tableName}.[[{$attribute}]]",  $this->owner->getAttribute($this->leftAttribute)],
180 174
            [$andSelf ? '<=' : '<', "{$tableName}.[[{$attribute}]]",  $this->owner->getAttribute($this->rightAttribute)],
181
        ];
182
183 174 View Code Duplication
        if ($depth !== 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...
184 129
            $condition[] = ['<=', "{$tableName}.[[{$this->depthAttribute}]]", $this->owner->getAttribute($this->depthAttribute) + $depth];
185
        }
186
187 174
        $query = $this->owner->find()
188 174
            ->andWhere($condition)
189 174
            ->andWhere($this->treeCondition())
190 174
            ->addOrderBy(["{$tableName}.[[{$attribute}]]" => $backOrder ? SORT_DESC : SORT_ASC]);
191 174
        $query->multiple = true;
192
193 174
        return $query;
194
    }
195
196
    /**
197
     * @return \yii\db\ActiveQuery
198
     */
199 114
    public function getChildren()
200
    {
201 114
        return $this->getDescendants(1);
202
    }
203
204
    /**
205
     * @param int|null $depth
206
     * @return \yii\db\ActiveQuery
207
     */
208 5
    public function getLeaves($depth = null)
209
    {
210 5
        $tableName = $this->owner->tableName();
211
        $condition = [
212 5
            'and',
213 5
            ['>', "leaves.[[{$this->leftAttribute}]]",  new Expression("{$tableName}.[[{$this->leftAttribute}]]")],
214 5
            ['<', "leaves.[[{$this->leftAttribute}]]",  new Expression("{$tableName}.[[{$this->rightAttribute}]]")],
215
        ];
216
217 5
        if ($this->treeAttribute !== null) {
218 5
            $condition[] = ["leaves.[[{$this->treeAttribute}]]" => new Expression("{$tableName}.[[{$this->treeAttribute}]]")];
219
        }
220
221 5
        $query = $this->getDescendants($depth)
222 5
            ->leftJoin("{$tableName} leaves", $condition)
223 5
            ->andWhere(["leaves.[[{$this->leftAttribute}]]" => null]);
224 5
        $query->multiple = true;
225 5
        return $query;
226
    }
227
228
    /**
229
     * @return \yii\db\ActiveQuery
230
     */
231 30 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...
232
    {
233 30
        $tableName = $this->owner->tableName();
234 30
        $query = $this->owner->find()
235 30
            ->andWhere([
236 30
                'and',
237 30
                ['<', "{$tableName}.[[{$this->rightAttribute}]]", $this->owner->getAttribute($this->leftAttribute)],
238 30
                ['>', "{$tableName}.[[{$this->rightAttribute}]]", $this->getParent()->select([$this->leftAttribute])],
239
            ])
240 30
            ->andWhere($this->treeCondition())
241 30
            ->orderBy(["{$tableName}.[[{$this->rightAttribute}]]" => SORT_DESC])
242 30
            ->limit(1);
243 30
        $query->multiple = false;
244 30
        return $query;
245
    }
246
247
    /**
248
     * @return \yii\db\ActiveQuery
249
     */
250 33 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...
251
    {
252 33
        $tableName = $this->owner->tableName();
253 33
        $query = $this->owner->find()
254 33
            ->andWhere([
255 33
                'and',
256 33
                ['>', "{$tableName}.[[{$this->leftAttribute}]]", $this->owner->getAttribute($this->rightAttribute)],
257 33
                ['<', "{$tableName}.[[{$this->leftAttribute}]]", $this->getParent()->select([$this->rightAttribute])],
258
            ])
259 33
            ->andWhere($this->treeCondition())
260 33
            ->orderBy(["{$tableName}.[[{$this->leftAttribute}]]" => SORT_ASC])
261 33
            ->limit(1);
262 33
        $query->multiple = false;
263 33
        return $query;
264
    }
265
266
    /**
267
     * Populate children relations for self and all descendants
268
     * @param int $depth = null
269
     * @param string|array $with = null
270
     * @return static
271
     */
272 5
    public function populateTree($depth = null, $with = null)
273
    {
274
        /** @var ActiveRecord[]|static[] $nodes */
275 5
        $query = $this->getDescendants($depth);
276 5
        if ($with) {
277
            $query->with($with);
278
        }
279 5
        $nodes = $query->all();
280
281 5
        $key = $this->owner->getAttribute($this->leftAttribute);
282 5
        $relates = [];
283 5
        $parents = [$key];
284 5
        $prev = $this->owner->getAttribute($this->depthAttribute);
285 5
        foreach($nodes as $node)
286
        {
287 5
            $level = $node->getAttribute($this->depthAttribute);
288 5
            if ($level <= $prev) {
289 5
                $parents = array_slice($parents, 0, $level - $prev - 1);
290
            }
291
292 5
            $key = end($parents);
293 5
            if (!isset($relates[$key])) {
294 5
                $relates[$key] = [];
295
            }
296 5
            $relates[$key][] = $node;
297
298 5
            $parents[] = $node->getAttribute($this->leftAttribute);
299 5
            $prev = $level;
300
        }
301
302 5
        $ownerDepth = $this->owner->getAttribute($this->depthAttribute);
303 5
        $nodes[] = $this->owner;
304 5
        foreach ($nodes as $node) {
305 5
            $key = $node->getAttribute($this->leftAttribute);
306 5
            if (isset($relates[$key])) {
307 5
                $node->populateRelation('children', $relates[$key]);
308 5
            } elseif ($depth === null || $ownerDepth + $depth > $node->getAttribute($this->depthAttribute)) {
309 5
                $node->populateRelation('children', []);
310
            }
311
        }
312
313 5
        return $this->owner;
314
    }
315
316
    /**
317
     * @return bool
318
     */
319 80
    public function isRoot()
320
    {
321 80
        return $this->owner->getAttribute($this->leftAttribute) == $this->range[0];
322
    }
323
324
    /**
325
     * @param ActiveRecord $node
326
     * @return bool
327
     */
328 83
    public function isChildOf($node)
329
    {
330 83
        $result = $this->owner->getAttribute($this->leftAttribute) > $node->getAttribute($this->leftAttribute)
331 83
            && $this->owner->getAttribute($this->rightAttribute) < $node->getAttribute($this->rightAttribute);
332
333 83
        if ($result && $this->treeAttribute !== null) {
334 11
            $result = $this->owner->getAttribute($this->treeAttribute) === $node->getAttribute($this->treeAttribute);
335
        }
336
337 83
        return $result;
338
    }
339
340
    /**
341
     * @return bool
342
     */
343 5
    public function isLeaf()
344
    {
345 5
        return count($this->owner->children) === 0;
346
    }
347
348
    /**
349
     * @return ActiveRecord
350
     */
351 13
    public function makeRoot()
352
    {
353 13
        $this->operation = self::OPERATION_MAKE_ROOT;
0 ignored issues
show
Documentation Bug introduced by
It seems like self::OPERATION_MAKE_ROOT of type integer is incompatible with the declared type string|null of property $operation.

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

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

Loading history...
354 13
        return $this->owner;
355
    }
356
357
    /**
358
     * @param ActiveRecord $node
359
     * @return ActiveRecord
360
     */
361 61
    public function prependTo($node)
362
    {
363 61
        $this->operation = self::OPERATION_PREPEND_TO;
0 ignored issues
show
Documentation Bug introduced by
It seems like self::OPERATION_PREPEND_TO of type integer is incompatible with the declared type string|null of property $operation.

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

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

Loading history...
364 61
        $this->node = $node;
365 61
        return $this->owner;
366
    }
367
368
    /**
369
     * @param ActiveRecord $node
370
     * @return ActiveRecord
371
     */
372 61
    public function appendTo($node)
373
    {
374 61
        $this->operation = self::OPERATION_APPEND_TO;
0 ignored issues
show
Documentation Bug introduced by
It seems like self::OPERATION_APPEND_TO of type integer is incompatible with the declared type string|null of property $operation.

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

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

Loading history...
375 61
        $this->node = $node;
376 61
        return $this->owner;
377
    }
378
379
    /**
380
     * @param ActiveRecord $node
381
     * @return ActiveRecord
382
     */
383 31
    public function insertBefore($node)
384
    {
385 31
        $this->operation = self::OPERATION_INSERT_BEFORE;
0 ignored issues
show
Documentation Bug introduced by
It seems like self::OPERATION_INSERT_BEFORE of type integer is incompatible with the declared type string|null of property $operation.

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

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

Loading history...
386 31
        $this->node = $node;
387 31
        return $this->owner;
388
    }
389
390
    /**
391
     * @param ActiveRecord $node
392
     * @return ActiveRecord
393
     */
394 34
    public function insertAfter($node)
395
    {
396 34
        $this->operation = self::OPERATION_INSERT_AFTER;
0 ignored issues
show
Documentation Bug introduced by
It seems like self::OPERATION_INSERT_AFTER of type integer is incompatible with the declared type string|null of property $operation.

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

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

Loading history...
397 34
        $this->node = $node;
398 34
        return $this->owner;
399
    }
400
401
    /**
402
     * Need for paulzi/auto-tree
403
     */
404
    public function preDeleteWithChildren()
405
    {
406
        $this->operation = self::OPERATION_DELETE_ALL;
0 ignored issues
show
Documentation Bug introduced by
It seems like self::OPERATION_DELETE_ALL of type integer is incompatible with the declared type string|null of property $operation.

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

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

Loading history...
407
    }
408
409
    /**
410
     * @return bool|int
411
     * @throws \Exception
412
     * @throws \yii\db\Exception
413
     */
414 11
    public function deleteWithChildren()
415
    {
416 11
        $this->operation = self::OPERATION_DELETE_ALL;
0 ignored issues
show
Documentation Bug introduced by
It seems like self::OPERATION_DELETE_ALL of type integer is incompatible with the declared type string|null of property $operation.

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

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

Loading history...
417 11
        if (!$this->owner->isTransactional(ActiveRecord::OP_DELETE)) {
418
            $transaction = $this->owner->getDb()->beginTransaction();
419
            try {
420
                $result = $this->deleteWithChildrenInternal();
421
                if ($result === false) {
422
                    $transaction->rollBack();
423
                } else {
424
                    $transaction->commit();
425
                }
426
                return $result;
427
            } catch (\Exception $e) {
428
                $transaction->rollBack();
429
                throw $e;
430
            }
431
        } else {
432 11
            $result = $this->deleteWithChildrenInternal();
433
        }
434 8
        return $result;
435
    }
436
437
    /**
438
     * @return ActiveRecord
439
     * @throws \Exception
440
     * @throws \yii\db\Exception
441
     */
442 5
    public function optimize()
443
    {
444 5
        if (!$this->owner->isTransactional(ActiveRecord::OP_UPDATE)) {
445
            $transaction = $this->owner->getDb()->beginTransaction();
446
            try {
447
                $this->optimizeInternal();
448
                $transaction->commit();
449
            } catch (\Exception $e) {
450
                $transaction->rollBack();
451
                throw $e;
452
            }
453
        } else {
454 5
            $this->optimizeInternal();
455
        }
456 5
        return $this->owner;
457
    }
458
459
    /**
460
     * @throws Exception
461
     * @throws NotSupportedException
462
     */
463 102
    public function beforeInsert()
464
    {
465 102
        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\nestedintervals\NestedIntervalsBehavior.

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...
466 79
            $this->node->refresh();
0 ignored issues
show
Bug introduced by
The method refresh does only exist in yii\db\ActiveRecord, but not in paulzi\nestedintervals\NestedIntervalsBehavior.

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...
467
        }
468 102
        switch ($this->operation) {
469 102
            case self::OPERATION_MAKE_ROOT:
470 8
                $condition = array_merge([$this->leftAttribute => $this->range[0]], $this->treeCondition());
471 8
                if ($this->owner->find()->andWhere($condition)->one() !== null) {
472 3
                    throw new Exception('Can not create more than one root.');
473
                }
474 5
                $this->owner->setAttribute($this->leftAttribute,  $this->range[0]);
475 5
                $this->owner->setAttribute($this->rightAttribute, $this->range[1]);
476 5
                $this->owner->setAttribute($this->depthAttribute, 0);
477 5
                break;
478
479 94
            case self::OPERATION_PREPEND_TO:
480 33
                $this->findPrependRange($left, $right);
481 33
                $this->insertNode($left, $right, 1, false);
482 30
                break;
483
484 61
            case self::OPERATION_APPEND_TO:
485 33
                $this->findAppendRange($left, $right);
486 33
                $this->insertNode($left, $right, 1, true);
487 30
                break;
488
489 28
            case self::OPERATION_INSERT_BEFORE:
490 11
                $parent = $this->findInsertBeforeRange($left, $right);
491 11
                $this->insertNode($left, $right, 0, false, $parent);
492 8
                break;
493
494 17
            case self::OPERATION_INSERT_AFTER:
495 14
                $parent = $this->findInsertAfterRange($left, $right);
496 14
                $this->insertNode($left, $right, 0, true, $parent);
497 8
                break;
498
499
            default:
500 3
                throw new NotSupportedException('Method "'. $this->owner->className() . '::insert" is not supported for inserting new nodes.');
0 ignored issues
show
Deprecated Code introduced by
The method yii\base\BaseObject::className() has been deprecated with message: since 2.0.14. On PHP >=5.5, use `::class` instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
501
        }
502 81
    }
503
504
    /**
505
     * @throws Exception
506
     */
507 81
    public function afterInsert()
508
    {
509 81
        if ($this->operation === self::OPERATION_MAKE_ROOT && $this->treeAttribute !== null && $this->owner->getAttribute($this->treeAttribute) === null) {
510 5
            $id = $this->owner->getPrimaryKey();
511 5
            $this->owner->setAttribute($this->treeAttribute, $id);
512 5
            $this->owner->updateAll([$this->treeAttribute => $id], [$this->getPrimaryKey() => $id]);
513
        }
514 81
        $this->operation = null;
515 81
        $this->node      = null;
516 81
    }
517
518
    /**
519
     * @throws Exception
520
     */
521 104
    public function beforeUpdate()
522
    {
523 104
        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\nestedintervals\NestedIntervalsBehavior.

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...
524 90
            $this->node->refresh();
0 ignored issues
show
Bug introduced by
The method refresh does only exist in yii\db\ActiveRecord, but not in paulzi\nestedintervals\NestedIntervalsBehavior.

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...
525
        }
526
527 104
        switch ($this->operation) {
528 104
            case self::OPERATION_MAKE_ROOT:
529 5
                if ($this->treeAttribute === null) {
530
                    throw new Exception('Can not move a node as the root when "treeAttribute" is not set.');
531
                }
532 5
                if ($this->owner->getOldAttribute($this->treeAttribute) !== $this->owner->getAttribute($this->treeAttribute)) {
533 5
                    $this->treeChange = $this->owner->getAttribute($this->treeAttribute);
534 5
                    $this->owner->setAttribute($this->treeAttribute, $this->owner->getOldAttribute($this->treeAttribute));
535
                }
536 5
                break;
537
538 99
            case self::OPERATION_INSERT_BEFORE:
539 79
            case self::OPERATION_INSERT_AFTER:
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
540 40
                if ($this->node->isRoot()) {
0 ignored issues
show
Bug introduced by
The method isRoot does only exist in paulzi\nestedintervals\NestedIntervalsBehavior, 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...
541
                    throw new Exception('Can not move a node before/after root.');
542
                }
543
544 59
            case self::OPERATION_PREPEND_TO:
545 31
            case self::OPERATION_APPEND_TO:
546 96
                if ($this->node->getIsNewRecord()) {
547 6
                    throw new Exception('Can not move a node when the target node is new record.');
548
                }
549
550 90
                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\nestedinte...estedIntervalsBehavior>; 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...
551 12
                    throw new Exception('Can not move a node when the target node is same.');
552
                }
553
554 78
                if ($this->node->isChildOf($this->owner)) {
0 ignored issues
show
Bug introduced by
The method isChildOf does only exist in paulzi\nestedintervals\NestedIntervalsBehavior, 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...
555 12
                    throw new Exception('Can not move a node when the target node is child.');
556
                }
557
        }
558 74
    }
559
560
    /**
561
     *
562
     */
563 74
    public function afterUpdate()
564
    {
565 74
        switch ($this->operation) {
566 74
            case self::OPERATION_MAKE_ROOT:
567 5
                $this->moveNodeAsRoot();
568 5
                break;
569
570 69
            case self::OPERATION_PREPEND_TO:
571 19
                $this->findPrependRange($left, $right);
572 19
                if ($right !== $this->owner->getAttribute($this->leftAttribute)) {
573 16
                    $this->moveNode($left, $right, 1);
574
                }
575 19
                break;
576
577 50 View Code Duplication
            case self::OPERATION_APPEND_TO:
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...
578 19
                $this->findAppendRange($left, $right);
579 19
                if ($left !== $this->owner->getAttribute($this->rightAttribute)) {
580 16
                    $this->moveNode($left, $right, 1);
581
                }
582 19
                break;
583
584 31 View Code Duplication
            case self::OPERATION_INSERT_BEFORE:
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...
585 14
                $this->findInsertBeforeRange($left, $right);
586 14
                if ($left !== $this->owner->getAttribute($this->rightAttribute)) {
587 11
                    $this->moveNode($left, $right);
588
                }
589 14
                break;
590
591 17
            case self::OPERATION_INSERT_AFTER:
592 14
                $this->findInsertAfterRange($left, $right);
593 14
                if ($right !== $this->owner->getAttribute($this->leftAttribute)) {
594 11
                    $this->moveNode($left, $right);
595
                }
596 14
                break;
597
        }
598 74
        $this->operation = null;
599 74
        $this->node      = null;
600 74
    }
601
602
    /**
603
     * @throws Exception
604
     */
605 22
    public function beforeDelete()
606
    {
607 22
        if ($this->owner->getIsNewRecord()) {
608 6
            throw new Exception('Can not delete a node when it is new record.');
609
        }
610 16
        if ($this->isRoot() && $this->operation !== self::OPERATION_DELETE_ALL) {
611 3
            throw new Exception('Method "'. $this->owner->className() . '::delete" is not supported for deleting root nodes.');
0 ignored issues
show
Deprecated Code introduced by
The method yii\base\BaseObject::className() has been deprecated with message: since 2.0.14. On PHP >=5.5, use `::class` instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
612
        }
613 13
        $this->owner->refresh();
614 13
    }
615
616
    /**
617
     *
618
     */
619 13
    public function afterDelete()
620
    {
621 13
        if ($this->operation !== self::OPERATION_DELETE_ALL) {
622 5
            $this->owner->updateAll([$this->depthAttribute => new Expression("[[{$this->depthAttribute}]] - 1")], $this->getDescendants()->where);
623
        }
624 13
        $this->operation = null;
625 13
        $this->node      = null;
626 13
    }
627
628
    /**
629
     * @return mixed
630
     * @throws Exception
631
     */
632 31
    protected function getPrimaryKey()
633
    {
634 31
        $primaryKey = $this->owner->primaryKey();
635 31
        if (!isset($primaryKey[0])) {
636
            throw new Exception('"' . $this->owner->className() . '" must have a primary key.');
0 ignored issues
show
Deprecated Code introduced by
The method yii\base\BaseObject::className() has been deprecated with message: since 2.0.14. On PHP >=5.5, use `::class` instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
637
        }
638 31
        return $primaryKey[0];
639
    }
640
641
    /**
642
     * @return self
643
     */
644 157
    public function getNodeBehavior()
645
    {
646 157
        foreach ($this->node->behaviors as $behavior) {
647 157
            if ($behavior instanceof NestedIntervalsBehavior) {
648 157
                return $behavior;
649
            }
650
        }
651
        return null;
652
    }
653
654
    /**
655
     * @return int
656
     */
657 11
    protected function deleteWithChildrenInternal()
658
    {
659 11
        if (!$this->owner->beforeDelete()) {
660
            return false;
661
        }
662 8
        $result = $this->owner->deleteAll($this->getDescendants(null, true)->where);
663 8
        $this->owner->setOldAttributes(null);
664 8
        $this->owner->afterDelete();
665 8
        return $result;
666
    }
667
668
    /**
669
     * @param int $left
670
     * @param int $right
671
     * @param int $depth
672
     * @param bool $forward
673
     * @param array|null $parent
674
     * @throws Exception
675
     */
676 91
    protected function insertNode($left, $right, $depth = 0, $forward = true, $parent = null)
677
    {
678 91
        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\nestedintervals\NestedIntervalsBehavior.

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...
679 12
            throw new Exception('Can not create a node when the target node is new record.');
680
        }
681
682 79
        if ($depth === 0 && $this->getNodeBehavior()->isRoot()) {
683 3
            throw new Exception('Can not insert a node before/after root.');
684
        }
685 76
        $this->owner->setAttribute($this->depthAttribute, $this->node->getAttribute($this->depthAttribute) + $depth);
0 ignored issues
show
Bug introduced by
The method getAttribute does only exist in yii\db\ActiveRecord, but not in paulzi\nestedintervals\NestedIntervalsBehavior.

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...
686 76
        if ($this->treeAttribute !== null) {
687 76
            $this->owner->setAttribute($this->treeAttribute, $this->node->getAttribute($this->treeAttribute));
688
        }
689 76
        if ($right - $left < 3) {
690 32
            for ($i = $right - $left; $i < 3; $i++) {
691 32
                $unallocated = $this->findUnallocatedAll($left, $right);
692 32
                if ($unallocated < $left) {
693 5
                    $this->shift($unallocated, $left, -1);
0 ignored issues
show
Bug introduced by
It seems like $unallocated defined by $this->findUnallocatedAll($left, $right) on line 691 can also be of type double or false; however, paulzi\nestedintervals\N...ervalsBehavior::shift() does only seem to accept integer, 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...
694 5
                    $left--;
695
                } else {
696 30
                    $this->shift($right, $unallocated, 1);
0 ignored issues
show
Bug introduced by
It seems like $unallocated defined by $this->findUnallocatedAll($left, $right) on line 691 can also be of type double or false; however, paulzi\nestedintervals\N...ervalsBehavior::shift() does only seem to accept integer, 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...
697 30
                    $right++;
698
                }
699
            }
700 32
            $this->owner->setAttribute($this->leftAttribute,  $left  + 1);
701 32
            $this->owner->setAttribute($this->rightAttribute, $right - 1);
702
        } else {
703 76
            $left++;
704 76
            $right--;
705
706 76
            $isPadding = false;
707 76
            if ($depth === 1 || $parent !== null) {
708
                // prepending/appending
709 66
                if (is_array($this->amountOptimize)) {
710
                    if (isset($this->amountOptimize[$depth - 1])) {
711
                        $amountOptimize = $this->amountOptimize[$depth - 1];
712
                    } else {
713
                        $amountOptimize = $this->amountOptimize[count($this->amountOptimize) - 1];
714
                    }
715
                } else {
716 66
                    $amountOptimize = $this->amountOptimize;
717
                }
718 66
                $pLeft     = $parent !== null ? (int)$parent[$this->leftAttribute]  : $this->node->getAttribute($this->leftAttribute);
719 66
                $pRight    = $parent !== null ? (int)$parent[$this->rightAttribute] : $this->node->getAttribute($this->rightAttribute);
720 66
                $isCenter  = !$this->noAppend && !$this->noPrepend;
721 66
                $isFirst   = $left === $pLeft + 1 && $right === $pRight - 1;
722 66
                $isPadding = !$this->noInsert || ($isFirst && ($forward ? !$this->noPrepend : !$this->noAppend));
723 66
                $step      = $amountOptimize + $this->reserveFactor * ($this->noInsert ? ($isCenter ? 2 : 1) : $amountOptimize + 1);
724 66
                $step      = ($pRight - $pLeft - 1) / $step;
725 66
                $stepGap   = $step * $this->reserveFactor;
726 66
                $padding   = $isPadding ? $stepGap : 0;
727
728 66
                if ($forward) {
729 33
                    $pLeft  = $left + (int)floor($padding);
730 33
                    $pRight = $left + (int)floor($padding + $step) - 1;
731
                } else {
732 33
                    $pLeft  = $right - (int)floor($padding + $step) + 1;
733 33
                    $pRight = $right - (int)floor($padding);
734
                }
735 66
                if ($isFirst && $isCenter) {
736 20
                    $initPosition = (int)floor(($amountOptimize - 1) / 2) * (int)floor($step + ($this->noInsert ? 0 : $stepGap)) + ($this->noInsert ? 1 : 0);
737 20
                    $pLeft  += $forward ? $initPosition : -$initPosition;
738 20
                    $pRight += $forward ? $initPosition : -$initPosition;
739
                }
740 66
                if ($pLeft < $pRight && $pRight <= $right && $left <= $pLeft && ($forward || $left < $pLeft) && (!$forward || $pRight < $right)) {
741 64
                    $this->owner->setAttribute($this->leftAttribute,  $pLeft);
742 64
                    $this->owner->setAttribute($this->rightAttribute, $pRight);
743 64
                    return;
744
                }
745
            }
746
747 52
            $isPadding = $isPadding || !$this->noInsert;
748 52
            $step = (int)floor(($right - $left) / ($isPadding ? 3 : 2));
749 52
            $this->owner->setAttribute($this->leftAttribute,  $left  + ($forward  && !$isPadding ? 0 : $step));
750 52
            $this->owner->setAttribute($this->rightAttribute, $right - (!$forward && !$isPadding ? 0 : $step));
751
        }
752 66
    }
753
754
    /**
755
     * @param int $left
756
     * @param int $right
757
     * @param int $depth
758
     * @throws Exception
759
     */
760 54
    protected function moveNode($left, $right, $depth = 0)
761
    {
762 54
        $targetDepth = $this->node->getAttribute($this->depthAttribute) + $depth;
0 ignored issues
show
Bug introduced by
The method getAttribute does only exist in yii\db\ActiveRecord, but not in paulzi\nestedintervals\NestedIntervalsBehavior.

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 54
        $depthDelta = $this->owner->getAttribute($this->depthAttribute) - $targetDepth;
764 54
        if ($this->treeAttribute === null || $this->owner->getAttribute($this->treeAttribute) === $this->node->getAttribute($this->treeAttribute)) {
765
            // same root
766 38
            $sLeft = $this->owner->getAttribute($this->leftAttribute);
767 38
            $sRight = $this->owner->getAttribute($this->rightAttribute);
768 38
            $this->owner->updateAll(
769 38
                [$this->depthAttribute => new Expression("-[[{$this->depthAttribute}]]" . sprintf('%+d', $depthDelta))],
770 38
                $this->getDescendants(null, true)->where
771
            );
772 38
            $sDelta = $sRight - $sLeft + 1;
773 38
            if ($sLeft >= $right) {
774 22
                $this->shift($right, $sLeft - 1, $sDelta);
775 22
                $delta = $right - $sLeft;
776
            } else {
777 16
                $this->shift($sRight + 1, $left, -$sDelta);
778 16
                $delta = $left - $sRight;
779
            }
780 38
            $this->owner->updateAll(
781
                [
782 38
                    $this->leftAttribute  => new Expression("[[{$this->leftAttribute}]]"  . sprintf('%+d', $delta)),
783 38
                    $this->rightAttribute => new Expression("[[{$this->rightAttribute}]]" . sprintf('%+d', $delta)),
784 38
                    $this->depthAttribute => new Expression("-[[{$this->depthAttribute}]]"),
785
                ],
786
                [
787 38
                    'and',
788 38
                    $this->getDescendants(null, true)->where,
789 38
                    ['<', $this->depthAttribute, 0],
790
                ]
791
            );
792
        } else {
793
            // move from other root (slow!)
794
            /** @var ActiveRecord|self $root */
795 16
            $root      = $this->getNodeBehavior()->getRoot()->one();
796 16
            $countTo   = (int)$root->getDescendants()->orderBy(null)->count();
0 ignored issues
show
Bug introduced by
The method getDescendants does only exist in paulzi\nestedintervals\NestedIntervalsBehavior, 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...
797 16
            $countFrom = (int)$this->getDescendants(null, true)->orderBy(null)->count();
798 16
            $size  = (int)floor(($this->range[1] - $this->range[0]) / (($countFrom + $countTo) * 2 + 1));
799
800 16
            $leftIdx  = $this->optimizeAttribute($root->getDescendants(null, false, false), $this->leftAttribute,  $this->range[0], $size,  0, $left,  $countFrom, $targetDepth);
801 16
            $rightIdx = $this->optimizeAttribute($root->getDescendants(null, false, true),  $this->rightAttribute, $this->range[1], $size, 0,  $right, $countFrom, $targetDepth);
802 16
            if ($leftIdx !== null && $rightIdx !== null) {
803 16
                $this->optimizeAttribute($this->getDescendants(null, true, false), $this->leftAttribute,  $this->range[0], $size, $leftIdx);
804 16
                $this->optimizeAttribute($this->getDescendants(null, true, true), $this->rightAttribute, $this->range[1],  $size, $rightIdx, null, 0,  null, [
805 16
                    $this->treeAttribute  => $this->node->getAttribute($this->treeAttribute),
806 16
                    $this->depthAttribute => new Expression("[[{$this->depthAttribute}]]" . sprintf('%+d', -$depthDelta)),
807
                ]);
808
            } else {
809
                throw new Exception('Error move a node from other tree');
810
            }
811
        }
812 54
    }
813
814
    /**
815
     *
816
     */
817 5
    protected function moveNodeAsRoot()
818
    {
819 5
        $left   = $this->owner->getAttribute($this->leftAttribute);
820 5
        $right  = $this->owner->getAttribute($this->rightAttribute);
821 5
        $depth  = $this->owner->getAttribute($this->depthAttribute);
822 5
        $tree   = $this->treeChange ? $this->treeChange : $this->owner->getPrimaryKey();
823
824 5
        $factor = ($this->range[1] - $this->range[0]) / ($right - $left);
825 5
        $formula = sprintf('ROUND(([[%%s]] * 1.0 %+d) * %d %+d)', -$left, (int)$factor, $this->range[0]);
826 5
        $this->owner->updateAll(
827
            [
828 5
                $this->leftAttribute  => new Expression(sprintf($formula, $this->leftAttribute)),
829 5
                $this->rightAttribute => new Expression(sprintf($formula, $this->rightAttribute)),
830 5
                $this->depthAttribute => new Expression("[[{$this->depthAttribute}]] - {$depth}"),
831 5
                $this->treeAttribute  => $tree,
832
            ],
833 5
            $this->getDescendants()->where
834
        );
835 5
        $this->owner->updateAll(
836
            [
837 5
                $this->leftAttribute  => $this->range[0],
838 5
                $this->rightAttribute => $this->range[1],
839 5
                $this->depthAttribute => 0,
840 5
                $this->treeAttribute  => $tree,
841
            ],
842 5
            [$this->getPrimaryKey() => $this->owner->getPrimaryKey()]
843
        );
844 5
        $this->owner->refresh();
845 5
    }
846
847
    /**
848
     * @throws Exception
849
     */
850 5
    protected function optimizeInternal()
851
    {
852 5
        $left  = $this->owner->getAttribute($this->leftAttribute);
853 5
        $right = $this->owner->getAttribute($this->rightAttribute);
854
855 5
        $count = $this->getDescendants()->orderBy(null)->count() * 2 + 1;
856 5
        $size  = (int)floor(($right - $left) / $count);
857
858 5
        $this->optimizeAttribute($this->getDescendants(null, false, false), $this->leftAttribute,  $left,  $size);
859 5
        $this->optimizeAttribute($this->getDescendants(null, false, true),  $this->rightAttribute, $right, $size);
860 5
    }
861
862
    /**
863
     * @param \yii\db\ActiveQuery $query
864
     * @param string $attribute
865
     * @param int $from
866
     * @param int $size
867
     * @param int $offset
868
     * @param int|null $freeFrom
869
     * @param int $freeSize
870
     * @param int|null $targetDepth
871
     * @param array $additional
872
     * @return int|null
873
     * @throws Exception
874
     * @throws \yii\db\Exception
875
     */
876 21
    protected function optimizeAttribute($query, $attribute, $from, $size, $offset = 0, $freeFrom = null, $freeSize = 0, $targetDepth = null, $additional = [])
877
    {
878 21
        $primaryKey = $this->getPrimaryKey();
879 21
        $result     = null;
880 21
        $isForward  = $attribute === $this->leftAttribute;
881
882
        // @todo: pgsql and mssql optimization
883 21
        if (in_array($this->owner->getDb()->driverName, ['mysql', 'mysqli'])) {
884
            // mysql optimization
885 8
            $tableName = $this->owner->tableName();
886 8
            $additionalString = null;
887 8
            $additionalParams = [];
888 8
            foreach ($additional as $name => $value) {
889 6
                $additionalString .= ", [[{$name}]] = ";
890 6
                if ($value instanceof Expression) {
891 6
                    $additionalString .= $value->expression;
892 6
                    foreach ($value->params as $n => $v) {
893 6
                        $additionalParams[$n] = $v;
894
                    }
895
                } else {
896 6
                    $paramName = ':nestedIntervals' . count($additionalParams);
897 6
                    $additionalString .= $paramName;
898 6
                    $additionalParams[$paramName] = $value;
899
                }
900
            }
901
902
            $command = $query
903 8
                ->select([$primaryKey, $attribute, $this->depthAttribute])
904 8
                ->orderBy([$attribute => $isForward ? SORT_ASC : SORT_DESC])
905 8
                ->createCommand();
906 8
            $this->owner->getDb()->createCommand("
907
                UPDATE
908 8
                    {$tableName} u,
909
                    (SELECT
910 8
                        [[{$primaryKey}]],
911
                        IF (@i := @i + 1, 0, 0)
912 8
                        + IF ([[{$attribute}]] " . ($isForward ? '>' : '<') . " @freeFrom,
913
                            IF (
914
                                (@result := @i)
915
                                + IF (@depth - :targetDepth > 0, @result := @result + @depth - :targetDepth, 0)
916
                                + (@i := @i + :freeSize * 2)
917
                                + (@freeFrom := NULL), 0, 0),
918
                            0)
919 8
                        + IF (@depth - [[{$this->depthAttribute}]] >= 0,
920 8
                            IF (@i := @i + @depth - [[{$this->depthAttribute}]] + 1, 0, 0),
921
                            0)
922 8
                        + (:from " . ($isForward ? '+' : '-') . " (CAST(@i AS UNSIGNED INTEGER) + :offset) * :size)
923 8
                        + IF ([[{$attribute}]] = @freeFrom,
924
                            IF ((@result := @i) + (@i := @i + :freeSize * 2) + (@freeFrom := NULL), 0, 0),
925
                            0)
926 8
                        + IF (@depth := [[{$this->depthAttribute}]], 0, 0)
927
                        as 'new'
928
                    FROM
929
                        (SELECT @i := 0, @depth := -1, @freeFrom := :freeFrom, @result := NULL) v,
930 8
                        (" . $command->sql . ") t
931
                    ) tmp
932 8
                SET u.[[{$attribute}]]=tmp.[[new]] {$additionalString}
933 8
                WHERE tmp.[[{$primaryKey}]]=u.[[{$primaryKey}]]")
934 8
                ->bindValues($additionalParams)
935 8
                ->bindValues($command->params)
936 8
                ->bindValues([
937 8
                    ':from'        => $from,
938 8
                    ':size'        => $size,
939 8
                    ':offset'      => $offset,
940 8
                    ':freeFrom'    => $freeFrom,
941 8
                    ':freeSize'    => $freeSize,
942 8
                    ':targetDepth' => $targetDepth,
943
                ])
944 8
                ->execute();
945 8
            if ($freeFrom !== null) {
946 6
                $result = $this->owner->getDb()->createCommand("SELECT IFNULL(@result, @i + 1 + IF (@depth - :targetDepth > 0, @depth - :targetDepth, 0))")
947 6
                    ->bindValue(':targetDepth', $targetDepth)
948 6
                    ->queryScalar();
949 6
                $result = $result === null ? null : (int)$result;
950
            }
951 8
            return $result;
952
        } else {
953
            // generic algorithm (very slow!)
954
            $query
955 13
                ->select([$primaryKey, $attribute, $this->depthAttribute])
956 13
                ->asArray()
957 13
                ->orderBy([$attribute => $isForward ? SORT_ASC : SORT_DESC]);
958
959 13
            $prevDepth = -1;
960 13
            $i = 0;
961 13
            foreach ($query->each() as $data) {
962 13
                $i++;
963 13
                if ($freeFrom !== null && $freeFrom !== (int)$data[$attribute] && ($freeFrom > (int)$data[$attribute] xor $isForward)) {
964 7
                    $result = $i;
965 7
                    $depthDiff = $prevDepth - $targetDepth;
966 7
                    if ($depthDiff > 0) {
967
                        $result += $depthDiff;
968
                    }
969 7
                    $i += $freeSize * 2;
970 7
                    $freeFrom = null;
971
                }
972 13
                $depthDiff = $prevDepth - $data[$this->depthAttribute];
973 13
                if ($depthDiff >= 0) {
974 13
                    $i += $depthDiff + 1;
975
                }
976 13
                $this->owner->updateAll(
977 13
                    array_merge($additional, [$attribute  => $isForward ? $from + ($i + $offset) * $size : $from - ($i + $offset) * $size]),
978 13
                    [$primaryKey => $data[$primaryKey]]
979
                );
980 13
                if ($freeFrom !== null && $freeFrom === (int)$data[$attribute]) {
981 6
                    $result = $i;
982 6
                    $i += $freeSize * 2;
983 6
                    $freeFrom = null;
984
                }
985 13
                $prevDepth = $data[$this->depthAttribute];
986
            }
987 13
            if ($freeFrom !== null) {
988 3
                $result = $i + 1;
989 3
                $depthDiff = $prevDepth - $targetDepth;
990 3
                if ($depthDiff > 0) {
991 3
                    $result += $depthDiff;
992
                }
993
            }
994 13
            return $result;
995
        }
996
    }
997
998
    /**
999
     * @param int $left
1000
     * @param int $right
1001
     */
1002 52 View Code Duplication
    protected function findPrependRange(&$left, &$right)
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...
1003
    {
1004 52
        $left  = $this->node->getAttribute($this->leftAttribute);
0 ignored issues
show
Bug introduced by
The method getAttribute does only exist in yii\db\ActiveRecord, but not in paulzi\nestedintervals\NestedIntervalsBehavior.

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...
1005 52
        $right = $this->node->getAttribute($this->rightAttribute);
1006 52
        $child = $this->getNodeBehavior()->getChildren()
1007 52
            ->select($this->leftAttribute)
1008 52
            ->orderBy([$this->leftAttribute => SORT_ASC])
1009 52
            ->limit(1)
1010 52
            ->scalar();
1011 52
        if ($child !== false) {
1012 26
            $right = (int)$child;
1013
        }
1014 52
    }
1015
1016
    /**
1017
     * @param int $left
1018
     * @param int $right
1019
     */
1020 52 View Code Duplication
    protected function findAppendRange(&$left, &$right)
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...
1021
    {
1022 52
        $left  = $this->node->getAttribute($this->leftAttribute);
0 ignored issues
show
Bug introduced by
The method getAttribute does only exist in yii\db\ActiveRecord, but not in paulzi\nestedintervals\NestedIntervalsBehavior.

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...
1023 52
        $right = $this->node->getAttribute($this->rightAttribute);
1024 52
        $child = $this->getNodeBehavior()->getChildren()
1025 52
            ->select($this->rightAttribute)
1026 52
            ->orderBy([$this->rightAttribute => SORT_DESC])
1027 52
            ->limit(1)
1028 52
            ->scalar();
1029 52
        if ($child !== false) {
1030 26
            $left = (int)$child;
1031
        }
1032 52
    }
1033
1034
    /**
1035
     * @param int $left
1036
     * @param int $right
1037
     * @return array|null
1038
     */
1039 25 View Code Duplication
    protected function findInsertBeforeRange(&$left, &$right)
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...
1040
    {
1041 25
        $result = null;
1042 25
        $right = $this->node->getAttribute($this->leftAttribute);
0 ignored issues
show
Bug introduced by
The method getAttribute does only exist in yii\db\ActiveRecord, but not in paulzi\nestedintervals\NestedIntervalsBehavior.

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...
1043 25
        $left  = $this->getNodeBehavior()->getPrev()
1044 25
            ->select($this->rightAttribute)
1045 25
            ->scalar();
1046 25
        if ($left === false) {
1047 9
            $result = $this->getNodeBehavior()->getParent()
1048 9
                ->select([$this->leftAttribute, $this->rightAttribute])
1049 9
                ->createCommand()
1050 9
                ->queryOne();
1051 9
            $left = $result[$this->leftAttribute];
1052
        }
1053 25
        $left = (int)$left;
1054 25
        return $result;
1055
    }
1056
1057
    /**
1058
     * @param int $left
1059
     * @param int $right
1060
     * @return array|null
1061
     */
1062 28 View Code Duplication
    protected function findInsertAfterRange(&$left, &$right)
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...
1063
    {
1064 28
        $result = null;
1065 28
        $left  = $this->node->getAttribute($this->rightAttribute);
0 ignored issues
show
Bug introduced by
The method getAttribute does only exist in yii\db\ActiveRecord, but not in paulzi\nestedintervals\NestedIntervalsBehavior.

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...
1066 28
        $right = $this->getNodeBehavior()->getNext()
1067 28
            ->select($this->leftAttribute)
1068 28
            ->scalar();
1069 28
        if ($right === false) {
1070 15
            $result = $this->getNodeBehavior()->getParent()
1071 15
                ->select([$this->leftAttribute, $this->rightAttribute])
1072 15
                ->createCommand()
1073 15
                ->queryOne();
1074 15
            $right = $result[$this->rightAttribute];
1075
        }
1076 28
        $right = (int)$right;
1077 28
        return $result;
1078
    }
1079
1080
    /**
1081
     * @param int $value
1082
     * @param string $attribute
1083
     * @param bool $forward
1084
     * @return bool|int
1085
     */
1086 32
    protected function findUnallocated($value, $attribute, $forward = true)
1087
    {
1088 32
        $tableName = $this->owner->tableName();
1089 32
        $leftCondition  = "l.[[{$this->leftAttribute}]]  = {$tableName}.[[{$attribute}]] " . ($forward ? '+' : '-') . " 1";
1090 32
        $rightCondition = "r.[[{$this->rightAttribute}]] = {$tableName}.[[{$attribute}]] " . ($forward ? '+' : '-') . " 1";
1091 32
        if ($this->treeAttribute !== null) {
1092 26
            $leftCondition  = ['and', $leftCondition,  "l.[[{$this->treeAttribute}]] = {$tableName}.[[{$this->treeAttribute}]]"];
1093 26
            $rightCondition = ['and', $rightCondition, "r.[[{$this->treeAttribute}]] = {$tableName}.[[{$this->treeAttribute}]]"];
1094
        }
1095 32
        $result = (new Query())
1096 32
            ->select("{$tableName}.[[{$attribute}]]")
1097 32
            ->from("{$tableName}")
1098 32
            ->leftJoin("{$tableName} l", $leftCondition)
1099 32
            ->leftJoin("{$tableName} r", $rightCondition)
1100 32
            ->where([
1101 32
                'and',
1102 32
                [$forward ? '>=' : '<=', "{$tableName}.[[{$attribute}]]", $value],
1103 32
                [$forward ? '<'  : '>',  "{$tableName}.[[{$attribute}]]", $this->range[$forward ? 1 : 0]],
1104
                [
1105 32
                    "l.[[{$this->leftAttribute}]]"  => null,
1106 32
                    "r.[[{$this->rightAttribute}]]" => null,
1107
                ],
1108
            ])
1109 32
            ->andWhere($this->treeCondition())
1110 32
            ->orderBy(["{$tableName}.[[{$attribute}]]" => $forward ? SORT_ASC : SORT_DESC])
1111 32
            ->limit(1)
1112 32
            ->scalar($this->owner->getDb());
1113 32
        if ($result !== false) {
1114 32
            $result += $forward ? 1 : -1;
1115
        }
1116 32
        return $result;
1117
    }
1118
1119
    /**
1120
     * @param int $left
1121
     * @param int $right
1122
     * @return bool|int
1123
     */
1124 32
    protected function findUnallocatedAll($left, $right)
1125
    {
1126 32
        $unallocated = false;
1127 32
        if ($right < (($this->range[1] - $this->range[0])>>1)) {
1128 30
            $unallocated = $unallocated ?: $this->findUnallocated($right, $this->rightAttribute, true);
1129 30
            $unallocated = $unallocated ?: $this->findUnallocated($right, $this->leftAttribute,  true);
1130 30
            $unallocated = $unallocated ?: $this->findUnallocated($left,  $this->leftAttribute,  false);
1131 30
            $unallocated = $unallocated ?: $this->findUnallocated($left,  $this->rightAttribute, false);
1132
        } else {
1133 5
            $unallocated = $unallocated ?: $this->findUnallocated($left,  $this->leftAttribute,  false);
1134 5
            $unallocated = $unallocated ?: $this->findUnallocated($left,  $this->rightAttribute, false);
1135 5
            $unallocated = $unallocated ?: $this->findUnallocated($right, $this->rightAttribute, true);
1136 5
            $unallocated = $unallocated ?: $this->findUnallocated($right, $this->leftAttribute,  true);
1137
        }
1138 32
        return $unallocated;
1139
    }
1140
1141
    /**
1142
     * @param int $from
1143
     * @param int $to
1144
     * @param int $delta
1145
     */
1146 70
    protected function shift($from, $to, $delta)
1147
    {
1148 70
        if ($to >= $from && $delta !== 0) {
1149 70
            foreach ([$this->leftAttribute, $this->rightAttribute] as $i => $attribute) {
1150 70
                $this->owner->updateAll(
1151 70
                    [$attribute => new Expression("[[{$attribute}]]" . sprintf('%+d', $delta))],
1152
                    [
1153 70
                        'and',
1154 70
                        ['between', $attribute, $from, $to],
1155 70
                        $this->treeCondition()
1156
                    ]
1157
                );
1158
            }
1159
        }
1160 70
    }
1161
1162
    /**
1163
     * @return array
1164
     */
1165 238
    protected function treeCondition()
1166
    {
1167 238
        $tableName = $this->owner->tableName();
1168 238
        if ($this->treeAttribute === null) {
1169 153
            return [];
1170
        } else {
1171 220
            return ["{$tableName}.[[{$this->treeAttribute}]]" => $this->owner->getAttribute($this->treeAttribute)];
1172
        }
1173
    }
1174
}
1175