NestedSetBehavior move root nodes – sorting[Solved]

NestedSetBehavior is a very good extension for manipulating tree with multiple roots. It only supports moveAsRoot and the new root is place as the last root, as

- 1. Mobile phones
    - 2. iPhone
    - 3. Samsung
        - 4. X100
        - 5. C200
        - 6. Motorola
- 7. Cars
    - 8. Audi
    - 9. Ford
    - 10. Mercedes

The node 9 can be moveAsRoot, but how to move Node 9 as root before 1. Mobile phones?

I have added some code based on the latest version https://gist.github.com/max107/10723289. This version
1. Disable single root mode
2. Root is not initiated same as PrimaryKey and set to Max(root)
3. The insertBefore, insertAfter, moveBefore, moveAfter does not support the case we mentioned before.

NestedSetBehavior Download here

insertBefore

public function insertBefore($target, $runValidation = true, $attributes = null) {
    if ($target->isRoot()) {
        return $this->addRootNode($target, $runValidation, $attributes, true);
    } else {
        return $this->addNode($target, $target->{$this->leftAttribute}, 0, $runValidation, $attributes);
    }
}

insertAfter

public function insertAfter($target, $runValidation = true, $attributes = null) {
    if ($target->isRoot()) {
        return $this->addRootNode($target, $runValidation, $attributes);
    } else {
        return $this->addNode($target, $target->{$this->rightAttribute} + 1, 0, $runValidation, $attributes);
    }
}

moveBefore

public function moveBefore($target) {
    if ($target->isRoot()) {
        return $this->moveAsRootSortable($target, true);
    } else {
        return $this->moveNode($target, $target->{$this->leftAttribute}, 0);
    }
}

moveAfter

public function moveAfter($target) {
    if ($target->isRoot()) {
        return $this->moveAsRootSortable($target);
    } else {
        return $this->moveNode($target, $target->{$this->rightAttribute} + 1, 0);
    }
}

Private method 1 (added)

private function moveAsRootSortable($target, $isBeforeMove=false) {
    $owner = $this->getOwner();
    if ($owner->getIsNewRecord())
        throw new CException(Yii::t('yiiext', 'The node should not be new record.'));

    if ($this->getIsDeletedRecord())
        throw new CDbException(Yii::t('yiiext', 'The node should not be deleted.'));

    if (!$target->isRoot())
        throw new CException(Yii::t('yiiext', 'the target node is not root'));

    $db = $owner->getDbConnection();

    if ($db->getCurrentTransaction() === null)
        $transaction = $db->beginTransaction();

    try {
        $source_tree_root = $owner->{$this->rootAttribute};
        $owner_db_temp_root = $owner->{$this->rootAttribute} * -1;
        $owner_left = $owner->{$this->leftAttribute};
        $owner_right = $owner->{$this->rightAttribute};
        $owner_level = $owner->{$this->levelAttribute};

        // update owner with temperary root, avoiding to be overwrited
        $owner->updateAll(
            array($this->rootAttribute => new CDbExpression($db->quoteColumnName($this->rootAttribute) . '* -1')),
                $db->quoteColumnName($this->leftAttribute) . '>=' . $owner->{$this->leftAttribute} . ' AND ' .
                $db->quoteColumnName($this->rightAttribute) . '<=' . $owner->{$this->rightAttribute} . ' AND ' .
                $db->quoteColumnName($this->rootAttribute) . '=' . CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount,
                array(CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount++ => $owner->{$this->rootAttribute}));

        // root sorting
        if ($owner->isRoot()) {
            // just shiftRoot for target node
            $owner_new_root = $this->shiftRoot($target->{$this->rootAttribute}, 1, $isBeforeMove);

            // update owner root
            $owner->updateAll(
                array($this->rootAttribute => $owner_new_root),
                $db->quoteColumnName($this->rootAttribute) . '=' . CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount,
                    array(CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount++ => $owner->{$this->rootAttribute} * -1));
        } else { // subtree to root
            // first step: source tree - shift left and right
            foreach (array($this->leftAttribute, $this->rightAttribute) as $attribute) {
                $condition = $db->quoteColumnName($attribute) . '>=' . $owner_left;
                $params = array();
                $condition .= ' AND ' . $db->quoteColumnName($this->rootAttribute) . '=' . CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount;
                $params[CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount++] = $source_tree_root;

                $owner->updateAll(array($attribute => new CDbExpression($db->quoteColumnName($attribute) . " - $owner_right + $owner_left - 1")), $condition, $params);
            }

            // second steps - just shiftRoot for target node, this can NOT be please before step 1, because this step will change root
            $owner_new_root = $this->shiftRoot($target->{$this->rootAttribute}, 1, $isBeforeMove);

            // new root
            $owner->updateAll(
                array(
                    $this->leftAttribute => new CDbExpression($db->quoteColumnName($this->leftAttribute) . " + 1 - ". $owner_level),
                    $this->rightAttribute => new CDbExpression($db->quoteColumnName($this->rightAttribute) . " + 1 - ". $owner_level),
                    $this->levelAttribute => new CDbExpression($db->quoteColumnName($this->levelAttribute) . " + 1 - ". $owner_level),
                    $this->rootAttribute => $owner_new_root,
                ), $db->quoteColumnName($this->leftAttribute) . '>=' . $owner_left . ' AND ' .
                $db->quoteColumnName($this->rightAttribute) . '<=' . $owner_right . ' AND ' .
                $db->quoteColumnName($this->rootAttribute) . '=' . CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount, array(CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount++ => $owner_db_temp_root));
        }

        if (isset($transaction))
            $transaction->commit();
    } catch (Exception $e) {
        if (isset($transaction))
            $transaction->rollback();

        throw $e;
    }

    return true;
}

Private method 2 (added)

private function addRootNode($target, $runValidation, $attributes, $isBeforeInsert=false) {
    $owner = $this->getOwner();
    if (!(!$levelUp && $target->isRoot())) {
        throw new CException(Yii::t('yiiext', 'The target node should be root.'));
    }

    if ($runValidation && !$owner->validate())
        return false;
    $db = $owner->getDbConnection();

    if ($db->getCurrentTransaction() === null)
        $transaction = $db->beginTransaction();

    try {
        $owner->{$this->rootAttribute} = $this->shiftRoot($target->{$this->rootAttribute}, 1, $isBeforeInsert);
        $owner->{$this->leftAttribute} = 1;
        $owner->{$this->rightAttribute} = 2;
        $owner->{$this->levelAttribute} = 1;

        $this->_ignoreEvent = true;
        $result = $owner->insert($attributes);
        $this->_ignoreEvent = false;

        if (!$result) {
            if (isset($transaction))
                $transaction->rollback();

            return false;
        }

        if (isset($transaction))
            $transaction->commit();
    } catch (Exception $e) {
        if (isset($transaction))
            $transaction->rollback();

        throw $e;
    }

    return true;
}

Private Method 3 (Added)

private function shiftRoot ($key, $delta, $isBeforeInsert=false) {
    $owner = $this->getOwner();
    $db = $owner->getDbConnection();
    $op = $isBeforeInsert ? ">=": ">";
    $condition = $db->quoteColumnName($this->rootAttribute) . $op . $key;
    $params = array();
    
    $owner->updateAll(array($this->rootAttribute => new CDbExpression($db->quoteColumnName($this->rootAttribute) . sprintf('%+d', $delta))), $condition, $params);
    
    return $isBeforeInsert ? $key : ($key + 1);
}