Code

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