Completed
Push — master ( c4910f...c003bb )
by Pavel
04:27
created

NestedIntervalsBehavior::populateTree()   D

Complexity

Conditions 9
Paths 40

Size

Total Lines 43
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 33
CRAP Score 9

Importance

Changes 0
Metric Value
dl 0
loc 43
ccs 33
cts 33
cp 1
rs 4.909
c 0
b 0
f 0
cc 9
eloc 28
nc 40
nop 1
crap 9
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 299
            ActiveRecord::EVENT_AFTER_INSERT    => 'afterInsert',
108 299
            ActiveRecord::EVENT_BEFORE_UPDATE   => 'beforeUpdate',
109 299
            ActiveRecord::EVENT_AFTER_UPDATE    => 'afterUpdate',
110 299
            ActiveRecord::EVENT_BEFORE_DELETE   => 'beforeDelete',
111 299
            ActiveRecord::EVENT_AFTER_DELETE    => 'afterDelete',
112 299
        ];
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 73
        ];
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 73
        }
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 174
        ];
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 129
        }
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 1
    }
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 5
        ];
216
217 5
        if ($this->treeAttribute !== null) {
218 5
            $condition[] = ["leaves.[[{$this->treeAttribute}]]" => new Expression("{$tableName}.[[{$this->treeAttribute}]]")];
219 5
        }
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 30
            ])
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 33
            ])
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
     * @return static
270
     */
271 5
    public function populateTree($depth = null)
272
    {
273
        /** @var ActiveRecord[]|static[] $nodes */
274 5
        if ($depth === null) {
275 5
            $nodes = $this->owner->descendants;
276 5
        } else {
277 5
            $nodes = $this->getDescendants($depth)->all();
278
        }
279
280 5
        $key = $this->owner->getAttribute($this->leftAttribute);
281 5
        $relates = [];
282 5
        $parents = [$key];
283 5
        $prev = $this->owner->getAttribute($this->depthAttribute);
284 5
        foreach($nodes as $node)
285
        {
286 5
            $level = $node->getAttribute($this->depthAttribute);
287 5
            if ($level <= $prev) {
288 5
                $parents = array_slice($parents, 0, $level - $prev - 1);
289 5
            }
290
291 5
            $key = end($parents);
292 5
            if (!isset($relates[$key])) {
293 5
                $relates[$key] = [];
294 5
            }
295 5
            $relates[$key][] = $node;
296
297 5
            $parents[] = $node->getAttribute($this->leftAttribute);
298 5
            $prev = $level;
299 5
        }
300
301 5
        $ownerDepth = $this->owner->getAttribute($this->depthAttribute);
302 5
        $nodes[] = $this->owner;
303 5
        foreach ($nodes as $node) {
304 5
            $key = $node->getAttribute($this->leftAttribute);
305 5
            if (isset($relates[$key])) {
306 5
                $node->populateRelation('children', $relates[$key]);
307 5
            } elseif ($depth === null || $ownerDepth + $depth > $node->getAttribute($this->depthAttribute)) {
308 5
                $node->populateRelation('children', []);
309 5
            }
310 5
        }
311
312 5
        return $this->owner;
313
    }
314
315
    /**
316
     * @return bool
317
     */
318 80
    public function isRoot()
319
    {
320 80
        return $this->owner->getAttribute($this->leftAttribute) == $this->range[0];
321
    }
322
323
    /**
324
     * @param ActiveRecord $node
325
     * @return bool
326
     */
327 83
    public function isChildOf($node)
328
    {
329 83
        $result = $this->owner->getAttribute($this->leftAttribute) > $node->getAttribute($this->leftAttribute)
330 83
            && $this->owner->getAttribute($this->rightAttribute) < $node->getAttribute($this->rightAttribute);
331
332 83
        if ($result && $this->treeAttribute !== null) {
333 11
            $result = $this->owner->getAttribute($this->treeAttribute) === $node->getAttribute($this->treeAttribute);
334 11
        }
335
336 83
        return $result;
337
    }
338
339
    /**
340
     * @return bool
341
     */
342 5
    public function isLeaf()
343
    {
344 5
        return count($this->owner->children) === 0;
345
    }
346
347
    /**
348
     * @return ActiveRecord
349
     */
350 13
    public function makeRoot()
351
    {
352 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...
353 13
        return $this->owner;
354
    }
355
356
    /**
357
     * @param ActiveRecord $node
358
     * @return ActiveRecord
359
     */
360 61
    public function prependTo($node)
361
    {
362 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...
363 61
        $this->node = $node;
364 61
        return $this->owner;
365
    }
366
367
    /**
368
     * @param ActiveRecord $node
369
     * @return ActiveRecord
370
     */
371 61
    public function appendTo($node)
372
    {
373 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...
374 61
        $this->node = $node;
375 61
        return $this->owner;
376
    }
377
378
    /**
379
     * @param ActiveRecord $node
380
     * @return ActiveRecord
381
     */
382 31
    public function insertBefore($node)
383
    {
384 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...
385 31
        $this->node = $node;
386 31
        return $this->owner;
387
    }
388
389
    /**
390
     * @param ActiveRecord $node
391
     * @return ActiveRecord
392
     */
393 34
    public function insertAfter($node)
394
    {
395 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...
396 34
        $this->node = $node;
397 34
        return $this->owner;
398
    }
399
400
    /**
401
     * Need for paulzi/auto-tree
402
     */
403
    public function preDeleteWithChildren()
404
    {
405
        $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...
406
    }
407
408
    /**
409
     * @return bool|int
410
     * @throws \Exception
411
     * @throws \yii\db\Exception
412
     */
413 11
    public function deleteWithChildren()
414
    {
415 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...
416 11
        if (!$this->owner->isTransactional(ActiveRecord::OP_DELETE)) {
417
            $transaction = $this->owner->getDb()->beginTransaction();
418
            try {
419
                $result = $this->deleteWithChildrenInternal();
420
                if ($result === false) {
421
                    $transaction->rollBack();
422
                } else {
423
                    $transaction->commit();
424
                }
425
                return $result;
426
            } catch (\Exception $e) {
427
                $transaction->rollBack();
428
                throw $e;
429
            }
430
        } else {
431 11
            $result = $this->deleteWithChildrenInternal();
432
        }
433 8
        return $result;
434
    }
435
436
    /**
437
     * @return ActiveRecord
438
     * @throws \Exception
439
     * @throws \yii\db\Exception
440
     */
441 5
    public function optimize()
442
    {
443 5
        if (!$this->owner->isTransactional(ActiveRecord::OP_UPDATE)) {
444
            $transaction = $this->owner->getDb()->beginTransaction();
445
            try {
446
                $this->optimizeInternal();
447
                $transaction->commit();
448
            } catch (\Exception $e) {
449
                $transaction->rollBack();
450
                throw $e;
451
            }
452
        } else {
453 5
            $this->optimizeInternal();
454
        }
455 5
        return $this->owner;
456
    }
457
458
    /**
459
     * @throws Exception
460
     * @throws NotSupportedException
461
     */
462 102
    public function beforeInsert()
463
    {
464 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...
465 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...
466 79
        }
467 102
        switch ($this->operation) {
468 102
            case self::OPERATION_MAKE_ROOT:
469 8
                $condition = array_merge([$this->leftAttribute => $this->range[0]], $this->treeCondition());
470 8
                if ($this->owner->findOne($condition) !== null) {
471 3
                    throw new Exception('Can not create more than one root.');
472
                }
473 5
                $this->owner->setAttribute($this->leftAttribute,  $this->range[0]);
474 5
                $this->owner->setAttribute($this->rightAttribute, $this->range[1]);
475 5
                $this->owner->setAttribute($this->depthAttribute, 0);
476 5
                break;
477
478 94
            case self::OPERATION_PREPEND_TO:
479 33
                $this->findPrependRange($left, $right);
480 33
                $this->insertNode($left, $right, 1, false);
481 30
                break;
482
483 61
            case self::OPERATION_APPEND_TO:
484 33
                $this->findAppendRange($left, $right);
485 33
                $this->insertNode($left, $right, 1, true);
486 30
                break;
487
488 28
            case self::OPERATION_INSERT_BEFORE:
489 11
                $parent = $this->findInsertBeforeRange($left, $right);
490 11
                $this->insertNode($left, $right, 0, false, $parent);
491 8
                break;
492
493 17
            case self::OPERATION_INSERT_AFTER:
494 14
                $parent = $this->findInsertAfterRange($left, $right);
495 14
                $this->insertNode($left, $right, 0, true, $parent);
496 8
                break;
497
498 3
            default:
499 3
                throw new NotSupportedException('Method "'. $this->owner->className() . '::insert" is not supported for inserting new nodes.');
500 84
        }
501 81
    }
502
503
    /**
504
     * @throws Exception
505
     */
506 81
    public function afterInsert()
507
    {
508 81
        if ($this->operation === self::OPERATION_MAKE_ROOT && $this->treeAttribute !== null && $this->owner->getAttribute($this->treeAttribute) === null) {
509 5
            $id = $this->owner->getPrimaryKey();
510 5
            $this->owner->setAttribute($this->treeAttribute, $id);
511 5
            $this->owner->updateAll([$this->treeAttribute => $id], [$this->getPrimaryKey() => $id]);
512 5
        }
513 81
        $this->operation = null;
514 81
        $this->node      = null;
515 81
    }
516
517
    /**
518
     * @throws Exception
519
     */
520 104
    public function beforeUpdate()
521
    {
522 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...
523 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...
524 90
        }
525
526 104
        switch ($this->operation) {
527 104
            case self::OPERATION_MAKE_ROOT:
528 5
                if ($this->treeAttribute === null) {
529
                    throw new Exception('Can not move a node as the root when "treeAttribute" is not set.');
530
                }
531 5
                if ($this->owner->getOldAttribute($this->treeAttribute) !== $this->owner->getAttribute($this->treeAttribute)) {
532 5
                    $this->treeChange = $this->owner->getAttribute($this->treeAttribute);
533 5
                    $this->owner->setAttribute($this->treeAttribute, $this->owner->getOldAttribute($this->treeAttribute));
534 5
                }
535 5
                break;
536
537 99
            case self::OPERATION_INSERT_BEFORE:
538 99
            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...
539 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...
540
                    throw new Exception('Can not move a node before/after root.');
541
                }
542
543 99
            case self::OPERATION_PREPEND_TO:
544 99
            case self::OPERATION_APPEND_TO:
545 96
                if ($this->node->getIsNewRecord()) {
546 6
                    throw new Exception('Can not move a node when the target node is new record.');
547
                }
548
549 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...
550 12
                    throw new Exception('Can not move a node when the target node is same.');
551
                }
552
553 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...
554 12
                    throw new Exception('Can not move a node when the target node is child.');
555
                }
556 74
        }
557 74
    }
558
559
    /**
560
     *
561
     */
562 74
    public function afterUpdate()
563
    {
564 74
        switch ($this->operation) {
565 74
            case self::OPERATION_MAKE_ROOT:
566 5
                $this->moveNodeAsRoot();
567 5
                break;
568
569 69
            case self::OPERATION_PREPEND_TO:
570 19
                $this->findPrependRange($left, $right);
571 19
                if ($right !== $this->owner->getAttribute($this->leftAttribute)) {
572 16
                    $this->moveNode($left, $right, 1);
573 16
                }
574 19
                break;
575
576 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...
577 19
                $this->findAppendRange($left, $right);
578 19
                if ($left !== $this->owner->getAttribute($this->rightAttribute)) {
579 16
                    $this->moveNode($left, $right, 1);
580 16
                }
581 19
                break;
582
583 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...
584 14
                $this->findInsertBeforeRange($left, $right);
585 14
                if ($left !== $this->owner->getAttribute($this->rightAttribute)) {
586 11
                    $this->moveNode($left, $right);
587 11
                }
588 14
                break;
589
590 17
            case self::OPERATION_INSERT_AFTER:
591 14
                $this->findInsertAfterRange($left, $right);
592 14
                if ($right !== $this->owner->getAttribute($this->leftAttribute)) {
593 11
                    $this->moveNode($left, $right);
594 11
                }
595 14
                break;
596 74
        }
597 74
        $this->operation = null;
598 74
        $this->node      = null;
599 74
    }
600
601
    /**
602
     * @throws Exception
603
     */
604 22
    public function beforeDelete()
605
    {
606 22
        if ($this->owner->getIsNewRecord()) {
607 6
            throw new Exception('Can not delete a node when it is new record.');
608
        }
609 16
        if ($this->isRoot() && $this->operation !== self::OPERATION_DELETE_ALL) {
610 3
            throw new Exception('Method "'. $this->owner->className() . '::delete" is not supported for deleting root nodes.');
611
        }
612 13
        $this->owner->refresh();
613 13
    }
614
615
    /**
616
     *
617
     */
618 13
    public function afterDelete()
619
    {
620 13
        if ($this->operation !== self::OPERATION_DELETE_ALL) {
621 5
            $this->owner->updateAll([$this->depthAttribute => new Expression("[[{$this->depthAttribute}]] - 1")], $this->getDescendants()->where);
622 5
        }
623 13
        $this->operation = null;
624 13
        $this->node      = null;
625 13
    }
626
627
    /**
628
     * @return mixed
629
     * @throws Exception
630
     */
631 31
    protected function getPrimaryKey()
632
    {
633 31
        $primaryKey = $this->owner->primaryKey();
634 31
        if (!isset($primaryKey[0])) {
635
            throw new Exception('"' . $this->owner->className() . '" must have a primary key.');
636
        }
637 31
        return $primaryKey[0];
638
    }
639
640
    /**
641
     * @return self
642
     */
643 157
    public function getNodeBehavior()
644
    {
645 157
        foreach ($this->node->behaviors as $behavior) {
646 157
            if ($behavior instanceof NestedIntervalsBehavior) {
647 157
                return $behavior;
648
            }
649
        }
650
        return null;
651
    }
652
653
    /**
654
     * @return int
655
     */
656 11
    protected function deleteWithChildrenInternal()
657
    {
658 11
        if (!$this->owner->beforeDelete()) {
659
            return false;
660
        }
661 8
        $result = $this->owner->deleteAll($this->getDescendants(null, true)->where);
662 8
        $this->owner->setOldAttributes(null);
663 8
        $this->owner->afterDelete();
664 8
        return $result;
665
    }
666
667
    /**
668
     * @param int $left
669
     * @param int $right
670
     * @param int $depth
671
     * @param bool $forward
672
     * @param array|null $parent
673
     * @throws Exception
674
     */
675 91
    protected function insertNode($left, $right, $depth = 0, $forward = true, $parent = null)
676
    {
677 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...
678 12
            throw new Exception('Can not create a node when the target node is new record.');
679
        }
680
681 79
        if ($depth === 0 && $this->getNodeBehavior()->isRoot()) {
682 3
            throw new Exception('Can not insert a node before/after root.');
683
        }
684 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...
685 76
        if ($this->treeAttribute !== null) {
686 76
            $this->owner->setAttribute($this->treeAttribute, $this->node->getAttribute($this->treeAttribute));
687 76
        }
688 76
        if ($right - $left < 3) {
689 32
            for ($i = $right - $left; $i < 3; $i++) {
690 32
                $unallocated = $this->findUnallocatedAll($left, $right);
691 32
                if ($unallocated < $left) {
692 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 690 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...
693 5
                    $left--;
694 5
                } else {
695 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 690 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...
696 30
                    $right++;
697
                }
698 32
            }
699 32
            $this->owner->setAttribute($this->leftAttribute,  $left  + 1);
700 32
            $this->owner->setAttribute($this->rightAttribute, $right - 1);
701 32
        } else {
702 76
            $left++;
703 76
            $right--;
704
705 76
            $isPadding = false;
706 76
            if ($depth === 1 || $parent !== null) {
707
                // prepending/appending
708 66
                if (is_array($this->amountOptimize)) {
709
                    if (isset($this->amountOptimize[$depth - 1])) {
710
                        $amountOptimize = $this->amountOptimize[$depth - 1];
711
                    } else {
712
                        $amountOptimize = $this->amountOptimize[count($this->amountOptimize) - 1];
713
                    }
714
                } else {
715 66
                    $amountOptimize = $this->amountOptimize;
716
                }
717 66
                $pLeft     = $parent !== null ? (int)$parent[$this->leftAttribute]  : $this->node->getAttribute($this->leftAttribute);
718 66
                $pRight    = $parent !== null ? (int)$parent[$this->rightAttribute] : $this->node->getAttribute($this->rightAttribute);
719 66
                $isCenter  = !$this->noAppend && !$this->noPrepend;
720 66
                $isFirst   = $left === $pLeft + 1 && $right === $pRight - 1;
721 66
                $isPadding = !$this->noInsert || ($isFirst && ($forward ? !$this->noPrepend : !$this->noAppend));
722 66
                $step      = $amountOptimize + $this->reserveFactor * ($this->noInsert ? ($isCenter ? 2 : 1) : $amountOptimize + 1);
723 66
                $step      = ($pRight - $pLeft - 1) / $step;
724 66
                $stepGap   = $step * $this->reserveFactor;
725 66
                $padding   = $isPadding ? $stepGap : 0;
726
727 66
                if ($forward) {
728 33
                    $pLeft  = $left + (int)floor($padding);
729 33
                    $pRight = $left + (int)floor($padding + $step) - 1;
730 33
                } else {
731 33
                    $pLeft  = $right - (int)floor($padding + $step) + 1;
732 33
                    $pRight = $right - (int)floor($padding);
733
                }
734 66
                if ($isFirst && $isCenter) {
735 20
                    $initPosition = (int)floor(($amountOptimize - 1) / 2) * (int)floor($step + ($this->noInsert ? 0 : $stepGap)) + ($this->noInsert ? 1 : 0);
736 20
                    $pLeft  += $forward ? $initPosition : -$initPosition;
737 20
                    $pRight += $forward ? $initPosition : -$initPosition;
738 20
                }
739 66
                if ($pLeft < $pRight && $pRight <= $right && $left <= $pLeft && ($forward || $left < $pLeft) && (!$forward || $pRight < $right)) {
740 64
                    $this->owner->setAttribute($this->leftAttribute,  $pLeft);
741 64
                    $this->owner->setAttribute($this->rightAttribute, $pRight);
742 64
                    return;
743
                }
744 42
            }
745
746 52
            $isPadding = $isPadding || !$this->noInsert;
747 52
            $step = (int)floor(($right - $left) / ($isPadding ? 3 : 2));
748 52
            $this->owner->setAttribute($this->leftAttribute,  $left  + ($forward  && !$isPadding ? 0 : $step));
749 52
            $this->owner->setAttribute($this->rightAttribute, $right - (!$forward && !$isPadding ? 0 : $step));
750
        }
751 66
    }
752
753
    /**
754
     * @param int $left
755
     * @param int $right
756
     * @param int $depth
757
     * @throws Exception
758
     */
759 54
    protected function moveNode($left, $right, $depth = 0)
760
    {
761 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...
762 54
        $depthDelta = $this->owner->getAttribute($this->depthAttribute) - $targetDepth;
763 54
        if ($this->treeAttribute === null || $this->owner->getAttribute($this->treeAttribute) === $this->node->getAttribute($this->treeAttribute)) {
764
            // same root
765 38
            $sLeft = $this->owner->getAttribute($this->leftAttribute);
766 38
            $sRight = $this->owner->getAttribute($this->rightAttribute);
767 38
            $this->owner->updateAll(
768 38
                [$this->depthAttribute => new Expression("-[[{$this->depthAttribute}]]" . sprintf('%+d', $depthDelta))],
769 38
                $this->getDescendants(null, true)->where
770 38
            );
771 38
            $sDelta = $sRight - $sLeft + 1;
772 38
            if ($sLeft >= $right) {
773 22
                $this->shift($right, $sLeft - 1, $sDelta);
774 22
                $delta = $right - $sLeft;
775 22
            } else {
776 16
                $this->shift($sRight + 1, $left, -$sDelta);
777 16
                $delta = $left - $sRight;
778
            }
779 38
            $this->owner->updateAll(
780
                [
781 38
                    $this->leftAttribute  => new Expression("[[{$this->leftAttribute}]]"  . sprintf('%+d', $delta)),
782 38
                    $this->rightAttribute => new Expression("[[{$this->rightAttribute}]]" . sprintf('%+d', $delta)),
783 38
                    $this->depthAttribute => new Expression("-[[{$this->depthAttribute}]]"),
784 38
                ],
785
                [
786 38
                    'and',
787 38
                    $this->getDescendants(null, true)->where,
788 38
                    ['<', $this->depthAttribute, 0],
789
                ]
790 38
            );
791 38
        } else {
792
            // move from other root (slow!)
793
            /** @var ActiveRecord|self $root */
794 16
            $root      = $this->getNodeBehavior()->getRoot()->one();
795 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...
796 16
            $countFrom = (int)$this->getDescendants(null, true)->orderBy(null)->count();
797 16
            $size  = (int)floor(($this->range[1] - $this->range[0]) / (($countFrom + $countTo) * 2 + 1));
798
799 16
            $leftIdx  = $this->optimizeAttribute($root->getDescendants(null, false, false), $this->leftAttribute,  $this->range[0], $size,  0, $left,  $countFrom, $targetDepth);
800 16
            $rightIdx = $this->optimizeAttribute($root->getDescendants(null, false, true),  $this->rightAttribute, $this->range[1], $size, 0,  $right, $countFrom, $targetDepth);
801 16
            if ($leftIdx !== null && $rightIdx !== null) {
802 16
                $this->optimizeAttribute($this->getDescendants(null, true, false), $this->leftAttribute,  $this->range[0], $size, $leftIdx);
803 16
                $this->optimizeAttribute($this->getDescendants(null, true, true), $this->rightAttribute, $this->range[1],  $size, $rightIdx, null, 0,  null, [
804 16
                    $this->treeAttribute  => $this->node->getAttribute($this->treeAttribute),
805 16
                    $this->depthAttribute => new Expression("[[{$this->depthAttribute}]]" . sprintf('%+d', -$depthDelta)),
806 16
                ]);
807 16
            } else {
808
                throw new Exception('Error move a node from other tree');
809
            }
810
        }
811 54
    }
812
813
    /**
814
     *
815
     */
816 5
    protected function moveNodeAsRoot()
817
    {
818 5
        $left   = $this->owner->getAttribute($this->leftAttribute);
819 5
        $right  = $this->owner->getAttribute($this->rightAttribute);
820 5
        $depth  = $this->owner->getAttribute($this->depthAttribute);
821 5
        $tree   = $this->treeChange ? $this->treeChange : $this->owner->getPrimaryKey();
822
823 5
        $factor = ($this->range[1] - $this->range[0]) / ($right - $left);
824 5
        $formula = sprintf('ROUND(([[%%s]] * 1.0 %+d) * %d %+d)', -$left, (int)$factor, $this->range[0]);
825 5
        $this->owner->updateAll(
826
            [
827 5
                $this->leftAttribute  => new Expression(sprintf($formula, $this->leftAttribute)),
828 5
                $this->rightAttribute => new Expression(sprintf($formula, $this->rightAttribute)),
829 5
                $this->depthAttribute => new Expression("[[{$this->depthAttribute}]] - {$depth}"),
830 5
                $this->treeAttribute  => $tree,
831 5
            ],
832 5
            $this->getDescendants()->where
833 5
        );
834 5
        $this->owner->updateAll(
835
            [
836 5
                $this->leftAttribute  => $this->range[0],
837 5
                $this->rightAttribute => $this->range[1],
838 5
                $this->depthAttribute => 0,
839 5
                $this->treeAttribute  => $tree,
840 5
            ],
841 5
            [$this->getPrimaryKey() => $this->owner->getPrimaryKey()]
842 5
        );
843 5
        $this->owner->refresh();
844 5
    }
845
846
    /**
847
     * @throws Exception
848
     */
849 5
    protected function optimizeInternal()
850
    {
851 5
        $left  = $this->owner->getAttribute($this->leftAttribute);
852 5
        $right = $this->owner->getAttribute($this->rightAttribute);
853
854 5
        $count = $this->getDescendants()->orderBy(null)->count() * 2 + 1;
855 5
        $size  = (int)floor(($right - $left) / $count);
856
857 5
        $this->optimizeAttribute($this->getDescendants(null, false, false), $this->leftAttribute,  $left,  $size);
858 5
        $this->optimizeAttribute($this->getDescendants(null, false, true),  $this->rightAttribute, $right, $size);
859 5
    }
860
861
    /**
862
     * @param \yii\db\ActiveQuery $query
863
     * @param string $attribute
864
     * @param int $from
865
     * @param int $size
866
     * @param int $offset
867
     * @param int|null $freeFrom
868
     * @param int $freeSize
869
     * @param int|null $targetDepth
870
     * @param array $additional
871
     * @return int|null
872
     * @throws Exception
873
     * @throws \yii\db\Exception
874
     */
875 21
    protected function optimizeAttribute($query, $attribute, $from, $size, $offset = 0, $freeFrom = null, $freeSize = 0, $targetDepth = null, $additional = [])
876
    {
877 21
        $primaryKey = $this->getPrimaryKey();
878 21
        $result     = null;
879 21
        $isForward  = $attribute === $this->leftAttribute;
880
881
        // @todo: pgsql and mssql optimization
882 21
        if (in_array($this->owner->getDb()->driverName, ['mysql', 'mysqli'])) {
883
            // mysql optimization
884 8
            $tableName = $this->owner->tableName();
885 8
            $additionalString = null;
886 8
            $additionalParams = [];
887 8
            foreach ($additional as $name => $value) {
888 6
                $additionalString .= ", [[{$name}]] = ";
889 6
                if ($value instanceof Expression) {
890 6
                    $additionalString .= $value->expression;
891 6
                    foreach ($value->params as $n => $v) {
892
                        $additionalParams[$n] = $v;
893 6
                    }
894 6
                } else {
895 6
                    $paramName = ':nestedIntervals' . count($additionalParams);
896 6
                    $additionalString .= $paramName;
897 6
                    $additionalParams[$paramName] = $value;
898
                }
899 8
            }
900
901
            $command = $query
902 8
                ->select([$primaryKey, $attribute, $this->depthAttribute])
903 8
                ->orderBy([$attribute => $isForward ? SORT_ASC : SORT_DESC])
904 8
                ->createCommand();
905 8
            $this->owner->getDb()->createCommand("
906
                UPDATE
907 8
                    {$tableName} u,
908
                    (SELECT
909 8
                        [[{$primaryKey}]],
910
                        IF (@i := @i + 1, 0, 0)
911 8
                        + IF ([[{$attribute}]] " . ($isForward ? '>' : '<') . " @freeFrom,
912
                            IF (
913
                                (@result := @i)
914
                                + IF (@depth - :targetDepth > 0, @result := @result + @depth - :targetDepth, 0)
915
                                + (@i := @i + :freeSize * 2)
916
                                + (@freeFrom := NULL), 0, 0),
917
                            0)
918 8
                        + IF (@depth - [[{$this->depthAttribute}]] >= 0,
919 8
                            IF (@i := @i + @depth - [[{$this->depthAttribute}]] + 1, 0, 0),
920
                            0)
921 8
                        + (:from " . ($isForward ? '+' : '-') . " (CAST(@i AS UNSIGNED INTEGER) + :offset) * :size)
922 8
                        + IF ([[{$attribute}]] = @freeFrom,
923
                            IF ((@result := @i) + (@i := @i + :freeSize * 2) + (@freeFrom := NULL), 0, 0),
924
                            0)
925 8
                        + IF (@depth := [[{$this->depthAttribute}]], 0, 0)
926
                        as 'new'
927
                    FROM
928
                        (SELECT @i := 0, @depth := -1, @freeFrom := :freeFrom, @result := NULL) v,
929 8
                        (" . $command->sql . ") t
930
                    ) tmp
931 8
                SET u.[[{$attribute}]]=tmp.[[new]] {$additionalString}
932 8
                WHERE tmp.[[{$primaryKey}]]=u.[[{$primaryKey}]]")
933 8
                ->bindValues($additionalParams)
934 8
                ->bindValues($command->params)
935 8
                ->bindValues([
936 8
                    ':from'        => $from,
937 8
                    ':size'        => $size,
938 8
                    ':offset'      => $offset,
939 8
                    ':freeFrom'    => $freeFrom,
940 8
                    ':freeSize'    => $freeSize,
941 8
                    ':targetDepth' => $targetDepth,
942 8
                ])
943 8
                ->execute();
944 8
            if ($freeFrom !== null) {
945 6
                $result = $this->owner->getDb()->createCommand("SELECT IFNULL(@result, @i + 1 + IF (@depth - :targetDepth > 0, @depth - :targetDepth, 0))")
946 6
                    ->bindValue(':targetDepth', $targetDepth)
947 6
                    ->queryScalar();
948 6
                $result = $result === null ? null : (int)$result;
949 6
            }
950 8
            return $result;
951
        } else {
952
            // generic algorithm (very slow!)
953
            $query
954 13
                ->select([$primaryKey, $attribute, $this->depthAttribute])
955 13
                ->asArray()
956 13
                ->orderBy([$attribute => $isForward ? SORT_ASC : SORT_DESC]);
957
958 13
            $prevDepth = -1;
959 13
            $i = 0;
960 13
            foreach ($query->each() as $data) {
961 13
                $i++;
962 13
                if ($freeFrom !== null && $freeFrom !== (int)$data[$attribute] && ($freeFrom > (int)$data[$attribute] xor $isForward)) {
963 7
                    $result = $i;
964 7
                    $depthDiff = $prevDepth - $targetDepth;
965 7
                    if ($depthDiff > 0) {
966
                        $result += $depthDiff;
967
                    }
968 7
                    $i += $freeSize * 2;
969 7
                    $freeFrom = null;
970 7
                }
971 13
                $depthDiff = $prevDepth - $data[$this->depthAttribute];
972 13
                if ($depthDiff >= 0) {
973 13
                    $i += $depthDiff + 1;
974 13
                }
975 13
                $this->owner->updateAll(
976 13
                    array_merge($additional, [$attribute  => $isForward ? $from + ($i + $offset) * $size : $from - ($i + $offset) * $size]),
977 13
                    [$primaryKey => $data[$primaryKey]]
978 13
                );
979 13
                if ($freeFrom !== null && $freeFrom === (int)$data[$attribute]) {
980 6
                    $result = $i;
981 6
                    $i += $freeSize * 2;
982 6
                    $freeFrom = null;
983 6
                }
984 13
                $prevDepth = $data[$this->depthAttribute];
985 13
            }
986 13
            if ($freeFrom !== null) {
987 3
                $result = $i + 1;
988 3
                $depthDiff = $prevDepth - $targetDepth;
989 3
                if ($depthDiff > 0) {
990 3
                    $result += $depthDiff;
991 3
                }
992 3
            }
993 13
            return $result;
994
        }
995
    }
996
997
    /**
998
     * @param int $left
999
     * @param int $right
1000
     */
1001 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...
1002
    {
1003 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...
1004 52
        $right = $this->node->getAttribute($this->rightAttribute);
1005 52
        $child = $this->getNodeBehavior()->getChildren()
1006 52
            ->select($this->leftAttribute)
1007 52
            ->orderBy([$this->leftAttribute => SORT_ASC])
1008 52
            ->limit(1)
1009 52
            ->scalar();
1010 52
        if ($child !== false) {
1011 26
            $right = (int)$child;
1012 26
        }
1013 52
    }
1014
1015
    /**
1016
     * @param int $left
1017
     * @param int $right
1018
     */
1019 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...
1020
    {
1021 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...
1022 52
        $right = $this->node->getAttribute($this->rightAttribute);
1023 52
        $child = $this->getNodeBehavior()->getChildren()
1024 52
            ->select($this->rightAttribute)
1025 52
            ->orderBy([$this->rightAttribute => SORT_DESC])
1026 52
            ->limit(1)
1027 52
            ->scalar();
1028 52
        if ($child !== false) {
1029 26
            $left = (int)$child;
1030 26
        }
1031 52
    }
1032
1033
    /**
1034
     * @param int $left
1035
     * @param int $right
1036
     * @return array|null
1037
     */
1038 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...
1039
    {
1040 25
        $result = null;
1041 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...
1042 25
        $left  = $this->getNodeBehavior()->getPrev()
1043 25
            ->select($this->rightAttribute)
1044 25
            ->scalar();
1045 25
        if ($left === false) {
1046 9
            $result = $this->getNodeBehavior()->getParent()
1047 9
                ->select([$this->leftAttribute, $this->rightAttribute])
1048 9
                ->createCommand()
1049 9
                ->queryOne();
1050 9
            $left = $result[$this->leftAttribute];
1051 9
        }
1052 25
        $left = (int)$left;
1053 25
        return $result;
1054
    }
1055
1056
    /**
1057
     * @param int $left
1058
     * @param int $right
1059
     * @return array|null
1060
     */
1061 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...
1062
    {
1063 28
        $result = null;
1064 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...
1065 28
        $right = $this->getNodeBehavior()->getNext()
1066 28
            ->select($this->leftAttribute)
1067 28
            ->scalar();
1068 28
        if ($right === false) {
1069 15
            $result = $this->getNodeBehavior()->getParent()
1070 15
                ->select([$this->leftAttribute, $this->rightAttribute])
1071 15
                ->createCommand()
1072 15
                ->queryOne();
1073 15
            $right = $result[$this->rightAttribute];
1074 15
        }
1075 28
        $right = (int)$right;
1076 28
        return $result;
1077
    }
1078
1079
    /**
1080
     * @param int $value
1081
     * @param string $attribute
1082
     * @param bool $forward
1083
     * @return bool|int
1084
     */
1085 32
    protected function findUnallocated($value, $attribute, $forward = true)
1086
    {
1087 32
        $tableName = $this->owner->tableName();
1088 32
        $leftCondition  = "l.[[{$this->leftAttribute}]]  = {$tableName}.[[{$attribute}]] " . ($forward ? '+' : '-') . " 1";
1089 32
        $rightCondition = "r.[[{$this->rightAttribute}]] = {$tableName}.[[{$attribute}]] " . ($forward ? '+' : '-') . " 1";
1090 32
        if ($this->treeAttribute !== null) {
1091 26
            $leftCondition  = ['and', $leftCondition,  "l.[[{$this->treeAttribute}]] = {$tableName}.[[{$this->treeAttribute}]]"];
1092 26
            $rightCondition = ['and', $rightCondition, "r.[[{$this->treeAttribute}]] = {$tableName}.[[{$this->treeAttribute}]]"];
1093 26
        }
1094 32
        $result = (new Query())
1095 32
            ->select("{$tableName}.[[{$attribute}]]")
1096 32
            ->from("{$tableName}")
1097 32
            ->leftJoin("{$tableName} l", $leftCondition)
1098 32
            ->leftJoin("{$tableName} r", $rightCondition)
1099 32
            ->where([
1100 32
                'and',
1101 32
                [$forward ? '>=' : '<=', "{$tableName}.[[{$attribute}]]", $value],
1102 32
                [$forward ? '<'  : '>',  "{$tableName}.[[{$attribute}]]", $this->range[$forward ? 1 : 0]],
1103
                [
1104 32
                    "l.[[{$this->leftAttribute}]]"  => null,
1105 32
                    "r.[[{$this->rightAttribute}]]" => null,
1106 32
                ],
1107 32
            ])
1108 32
            ->andWhere($this->treeCondition())
1109 32
            ->orderBy(["{$tableName}.[[{$attribute}]]" => $forward ? SORT_ASC : SORT_DESC])
1110 32
            ->limit(1)
1111 32
            ->scalar($this->owner->getDb());
1112 32
        if ($result !== false) {
1113 32
            $result += $forward ? 1 : -1;
1114 32
        }
1115 32
        return $result;
1116
    }
1117
1118
    /**
1119
     * @param int $left
1120
     * @param int $right
1121
     * @return bool|int
1122
     */
1123 32
    protected function findUnallocatedAll($left, $right)
1124
    {
1125 32
        $unallocated = false;
1126 32
        if ($right < (($this->range[1] - $this->range[0])>>1)) {
1127 30
            $unallocated = $unallocated ?: $this->findUnallocated($right, $this->rightAttribute, true);
1128 30
            $unallocated = $unallocated ?: $this->findUnallocated($right, $this->leftAttribute,  true);
1129 30
            $unallocated = $unallocated ?: $this->findUnallocated($left,  $this->leftAttribute,  false);
1130 30
            $unallocated = $unallocated ?: $this->findUnallocated($left,  $this->rightAttribute, false);
1131 30
        } else {
1132 5
            $unallocated = $unallocated ?: $this->findUnallocated($left,  $this->leftAttribute,  false);
1133 5
            $unallocated = $unallocated ?: $this->findUnallocated($left,  $this->rightAttribute, false);
1134 5
            $unallocated = $unallocated ?: $this->findUnallocated($right, $this->rightAttribute, true);
1135 5
            $unallocated = $unallocated ?: $this->findUnallocated($right, $this->leftAttribute,  true);
1136
        }
1137 32
        return $unallocated;
1138
    }
1139
1140
    /**
1141
     * @param int $from
1142
     * @param int $to
1143
     * @param int $delta
1144
     */
1145 70
    protected function shift($from, $to, $delta)
1146
    {
1147 70
        if ($to >= $from && $delta !== 0) {
1148 70
            foreach ([$this->leftAttribute, $this->rightAttribute] as $i => $attribute) {
1149 70
                $this->owner->updateAll(
1150 70
                    [$attribute => new Expression("[[{$attribute}]]" . sprintf('%+d', $delta))],
1151
                    [
1152 70
                        'and',
1153 70
                        ['between', $attribute, $from, $to],
1154 70
                        $this->treeCondition()
1155 70
                    ]
1156 70
                );
1157 70
            }
1158 70
        }
1159 70
    }
1160
1161
    /**
1162
     * @return array
1163
     */
1164 238
    protected function treeCondition()
1165
    {
1166 238
        $tableName = $this->owner->tableName();
1167 238
        if ($this->treeAttribute === null) {
1168 153
            return [];
1169
        } else {
1170 220
            return ["{$tableName}.[[{$this->treeAttribute}]]" => $this->owner->getAttribute($this->treeAttribute)];
1171
        }
1172
    }
1173
}
1174