Completed
Push — master ( 5a2aa2...c4910f )
by Pavel
04:23
created

NestedIntervalsBehavior::getPrimaryKey()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2.0185

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 5
cts 6
cp 0.8333
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 0
crap 2.0185
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[$key] = [];
0 ignored issues
show
Coding Style Comprehensibility introduced by
$relates was never initialized. Although not strictly required by PHP, it is generally a good practice to add $relates = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

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