Created
April 15, 2014 11:06
-
-
Save max107/10723289 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* NestedSetBehavior class file. | |
* | |
* @author Alexander Kochetov <creocoder@gmail.com> | |
* @link https://github.com/yiiext/nested-set-behavior | |
*/ | |
/** | |
* Provides nested set functionality for a model. | |
* | |
* @version 1.06 | |
* @package yiiext.behaviors.model.trees | |
*/ | |
class NestedSetBehavior extends CActiveRecordBehavior | |
{ | |
public $rootAttribute = 'root'; | |
public $leftAttribute = 'lft'; | |
public $rightAttribute = 'rgt'; | |
public $levelAttribute = 'level'; | |
private $_ignoreEvent = false; | |
private $_deleted = false; | |
private $_id; | |
private static $_cached; | |
private static $_c = 0; | |
/** | |
* Named scope. Gets descendants for node. | |
* @param int $depth the depth. | |
* @return CActiveRecord the owner. | |
*/ | |
public function descendants($depth = null) | |
{ | |
$owner = $this->getOwner(); | |
$db = $owner->getDbConnection(); | |
$criteria = $owner->getDbCriteria(); | |
$alias = $db->quoteColumnName($owner->getTableAlias()); | |
$criteria->mergeWith(array( | |
'condition' => $alias . '.' . $db->quoteColumnName($this->leftAttribute) . '>' . $owner->{$this->leftAttribute} . | |
' AND ' . $alias . '.' . $db->quoteColumnName($this->rightAttribute) . '<' . $owner->{$this->rightAttribute}, | |
'order' => $alias . '.' . $db->quoteColumnName($this->leftAttribute), | |
)); | |
if ($depth !== null) | |
$criteria->addCondition($alias . '.' . $db->quoteColumnName($this->levelAttribute) . '<=' . ($owner->{$this->levelAttribute} + $depth)); | |
$criteria->addCondition($alias . '.' . $db->quoteColumnName($this->rootAttribute) . '=' . CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount); | |
$criteria->params[CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount++] = $owner->{$this->rootAttribute}; | |
return $owner; | |
} | |
/** | |
* Named scope. Gets children for node (direct descendants only). | |
* @return CActiveRecord the owner. | |
*/ | |
public function children() | |
{ | |
return $this->descendants(1); | |
} | |
/** | |
* Named scope. Gets ancestors for node. | |
* @param int $depth the depth. | |
* @return CActiveRecord the owner. | |
*/ | |
public function ancestors($depth = null) | |
{ | |
$owner = $this->getOwner(); | |
$db = $owner->getDbConnection(); | |
$criteria = $owner->getDbCriteria(); | |
$alias = $db->quoteColumnName($owner->getTableAlias()); | |
$criteria->mergeWith(array( | |
'condition' => $alias . '.' . $db->quoteColumnName($this->leftAttribute) . '<' . $owner->{$this->leftAttribute} . | |
' AND ' . $alias . '.' . $db->quoteColumnName($this->rightAttribute) . '>' . $owner->{$this->rightAttribute}, | |
'order' => $alias . '.' . $db->quoteColumnName($this->leftAttribute), | |
)); | |
if ($depth !== null) | |
$criteria->addCondition($alias . '.' . $db->quoteColumnName($this->levelAttribute) . '>=' . ($owner->{$this->levelAttribute} - $depth)); | |
$criteria->addCondition($alias . '.' . $db->quoteColumnName($this->rootAttribute) . '=' . CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount); | |
$criteria->params[CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount++] = $owner->{$this->rootAttribute}; | |
return $owner; | |
} | |
/** | |
* Named scope. Gets root node(s). | |
* @return CActiveRecord the owner. | |
*/ | |
public function roots() | |
{ | |
$owner = $this->getOwner(); | |
$db = $owner->getDbConnection(); | |
$owner->getDbCriteria()->addCondition($db->quoteColumnName($owner->getTableAlias()) . '.' . $db->quoteColumnName($this->leftAttribute) . '=1'); | |
return $owner; | |
} | |
/** | |
* Named scope. Gets parent of node. | |
* @return CActiveRecord the owner. | |
*/ | |
public function parent() | |
{ | |
$owner = $this->getOwner(); | |
$db = $owner->getDbConnection(); | |
$criteria = $owner->getDbCriteria(); | |
$alias = $db->quoteColumnName($owner->getTableAlias()); | |
$criteria->mergeWith(array( | |
'condition' => $alias . '.' . $db->quoteColumnName($this->leftAttribute) . '<' . $owner->{$this->leftAttribute} . | |
' AND ' . $alias . '.' . $db->quoteColumnName($this->rightAttribute) . '>' . $owner->{$this->rightAttribute}, | |
'order' => $alias . '.' . $db->quoteColumnName($this->rightAttribute), | |
)); | |
$criteria->addCondition($alias . '.' . $db->quoteColumnName($this->rootAttribute) . '=' . CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount); | |
$criteria->params[CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount++] = $owner->{$this->rootAttribute}; | |
return $owner; | |
} | |
/** | |
* Named scope. Gets previous sibling of node. | |
* @return CActiveRecord the owner. | |
*/ | |
public function prev() | |
{ | |
$owner = $this->getOwner(); | |
$db = $owner->getDbConnection(); | |
$criteria = $owner->getDbCriteria(); | |
$alias = $db->quoteColumnName($owner->getTableAlias()); | |
$criteria->addCondition($alias . '.' . $db->quoteColumnName($this->rightAttribute) . '=' . ($owner->{$this->leftAttribute} - 1)); | |
$criteria->addCondition($alias . '.' . $db->quoteColumnName($this->rootAttribute) . '=' . CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount); | |
$criteria->params[CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount++] = $owner->{$this->rootAttribute}; | |
return $owner; | |
} | |
/** | |
* Named scope. Gets next sibling of node. | |
* @return CActiveRecord the owner. | |
*/ | |
public function next() | |
{ | |
$owner = $this->getOwner(); | |
$db = $owner->getDbConnection(); | |
$criteria = $owner->getDbCriteria(); | |
$alias = $db->quoteColumnName($owner->getTableAlias()); | |
$criteria->addCondition($alias . '.' . $db->quoteColumnName($this->leftAttribute) . '=' . ($owner->{$this->rightAttribute} + 1)); | |
$criteria->addCondition($alias . '.' . $db->quoteColumnName($this->rootAttribute) . '=' . CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount); | |
$criteria->params[CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount++] = $owner->{$this->rootAttribute}; | |
return $owner; | |
} | |
/** | |
* Create root node if multiple-root tree mode. Update node if it's not new. | |
* @param boolean $runValidation whether to perform validation. | |
* @param boolean $attributes list of attributes. | |
* @return boolean whether the saving succeeds. | |
*/ | |
public function save($runValidation = true, $attributes = null) | |
{ | |
$owner = $this->getOwner(); | |
if ($runValidation && !$owner->validate($attributes)) | |
return false; | |
if ($owner->getIsNewRecord()) | |
return $this->makeRoot($attributes); | |
$this->_ignoreEvent = true; | |
$result = $owner->update($attributes); | |
$this->_ignoreEvent = false; | |
return $result; | |
} | |
/** | |
* Create root node if multiple-root tree mode. Update node if it's not new. | |
* @param boolean $runValidation whether to perform validation. | |
* @param boolean $attributes list of attributes. | |
* @return boolean whether the saving succeeds. | |
*/ | |
public function saveNode($runValidation = true, $attributes = null) | |
{ | |
return $this->save($runValidation, $attributes); | |
} | |
/** | |
* Deletes node and it's descendants. | |
* @return boolean whether the deletion is successful. | |
* @throws CDbException | |
* @throws Exception | |
*/ | |
public function delete() | |
{ | |
$owner = $this->getOwner(); | |
if ($owner->getIsNewRecord()) | |
throw new CDbException(Yii::t('yiiext', 'The node cannot be deleted because it is new.')); | |
if ($this->getIsDeletedRecord()) | |
throw new CDbException(Yii::t('yiiext', 'The node cannot be deleted because it is already deleted.')); | |
$db = $owner->getDbConnection(); | |
if ($db->getCurrentTransaction() === null) | |
$transaction = $db->beginTransaction(); | |
try { | |
if ($owner->isLeaf()) { | |
$this->_ignoreEvent = true; | |
$result = $owner->delete(); | |
$this->_ignoreEvent = false; | |
} else { | |
$condition = $db->quoteColumnName($this->leftAttribute) . '>=' . $owner->{$this->leftAttribute} . ' AND ' . | |
$db->quoteColumnName($this->rightAttribute) . '<=' . $owner->{$this->rightAttribute}; | |
$params = array(); | |
$condition .= ' AND ' . $db->quoteColumnName($this->rootAttribute) . '=' . CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount; | |
$params[CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount++] = $owner->{$this->rootAttribute}; | |
$result = $owner->deleteAll($condition, $params) > 0; | |
} | |
if (!$result) { | |
if (isset($transaction)) | |
$transaction->rollback(); | |
return false; | |
} | |
$this->shiftLeftRight($owner->{$this->rightAttribute} + 1, $owner->{$this->leftAttribute} - $owner->{$this->rightAttribute} - 1); | |
if (isset($transaction)) | |
$transaction->commit(); | |
$this->correctCachedOnDelete(); | |
} catch (Exception $e) { | |
if (isset($transaction)) | |
$transaction->rollback(); | |
throw $e; | |
} | |
return true; | |
} | |
/** | |
* Deletes node and it's descendants. | |
* @return boolean whether the deletion is successful. | |
*/ | |
public function deleteNode() | |
{ | |
return $this->delete(); | |
} | |
/** | |
* Prepends node to target as first child. | |
* @param CActiveRecord $target the target. | |
* @param boolean $runValidation whether to perform validation. | |
* @param array $attributes list of attributes. | |
* @return boolean whether the prepending succeeds. | |
*/ | |
public function prependTo($target, $runValidation = true, $attributes = null) | |
{ | |
return $this->addNode($target, $target->{$this->leftAttribute} + 1, 1, $runValidation, $attributes); | |
} | |
/** | |
* Prepends target to node as first child. | |
* @param CActiveRecord $target the target. | |
* @param boolean $runValidation whether to perform validation. | |
* @param array $attributes list of attributes. | |
* @return boolean whether the prepending succeeds. | |
*/ | |
public function prepend($target, $runValidation = true, $attributes = null) | |
{ | |
return $target->prependTo($this->getOwner(), $runValidation, $attributes); | |
} | |
/** | |
* Appends node to target as last child. | |
* @param CActiveRecord $target the target. | |
* @param boolean $runValidation whether to perform validation. | |
* @param array $attributes list of attributes. | |
* @return boolean whether the appending succeeds. | |
*/ | |
public function appendTo($target, $runValidation = true, $attributes = null) | |
{ | |
return $this->addNode($target, $target->{$this->rightAttribute}, 1, $runValidation, $attributes); | |
} | |
/** | |
* Appends target to node as last child. | |
* @param CActiveRecord $target the target. | |
* @param boolean $runValidation whether to perform validation. | |
* @param array $attributes list of attributes. | |
* @return boolean whether the appending succeeds. | |
*/ | |
public function append($target, $runValidation = true, $attributes = null) | |
{ | |
return $target->appendTo($this->getOwner(), $runValidation, $attributes); | |
} | |
/** | |
* Inserts node as previous sibling of target. | |
* @param CActiveRecord $target the target. | |
* @param boolean $runValidation whether to perform validation. | |
* @param array $attributes list of attributes. | |
* @return boolean whether the inserting succeeds. | |
*/ | |
public function insertBefore($target, $runValidation = true, $attributes = null) | |
{ | |
return $this->addNode($target, $target->{$this->leftAttribute}, 0, $runValidation, $attributes); | |
} | |
/** | |
* Inserts node as next sibling of target. | |
* @param CActiveRecord $target the target. | |
* @param boolean $runValidation whether to perform validation. | |
* @param array $attributes list of attributes. | |
* @return boolean whether the inserting succeeds. | |
*/ | |
public function insertAfter($target, $runValidation = true, $attributes = null) | |
{ | |
return $this->addNode($target, $target->{$this->rightAttribute} + 1, 0, $runValidation, $attributes); | |
} | |
/** | |
* Move node as previous sibling of target. | |
* @param CActiveRecord $target the target. | |
* @return boolean whether the moving succeeds. | |
*/ | |
public function moveBefore($target) | |
{ | |
return $this->moveNode($target, $target->{$this->leftAttribute}, 0); | |
} | |
/** | |
* Move node as next sibling of target. | |
* @param CActiveRecord $target the target. | |
* @return boolean whether the moving succeeds. | |
*/ | |
public function moveAfter($target) | |
{ | |
return $this->moveNode($target, $target->{$this->rightAttribute} + 1, 0); | |
} | |
/** | |
* Move node as first child of target. | |
* @param CActiveRecord $target the target. | |
* @return boolean whether the moving succeeds. | |
*/ | |
public function moveAsFirst($target) | |
{ | |
return $this->moveNode($target, $target->{$this->leftAttribute} + 1, 1); | |
} | |
/** | |
* Move node as last child of target. | |
* @param CActiveRecord $target the target. | |
* @return boolean whether the moving succeeds. | |
*/ | |
public function moveAsLast($target) | |
{ | |
return $this->moveNode($target, $target->{$this->rightAttribute}, 1); | |
} | |
/** | |
* Move node as new root. | |
* @return boolean whether the moving succeeds. | |
* @throws CDbException | |
* @throws CException | |
* @throws Exception | |
*/ | |
public function moveAsRoot() | |
{ | |
$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 ($owner->isRoot()) | |
throw new CException(Yii::t('yiiext', 'The node already is root node.')); | |
$db = $owner->getDbConnection(); | |
if ($db->getCurrentTransaction() === null) | |
$transaction = $db->beginTransaction(); | |
try { | |
$left = $owner->{$this->leftAttribute}; | |
$right = $owner->{$this->rightAttribute}; | |
$levelDelta = 1 - $owner->{$this->levelAttribute}; | |
$delta = 1 - $left; | |
$lastRoot = $this->getLastRoot($owner); | |
$owner->updateAll( | |
array( | |
$this->leftAttribute => new CDbExpression($db->quoteColumnName($this->leftAttribute) . sprintf('%+d', $delta)), | |
$this->rightAttribute => new CDbExpression($db->quoteColumnName($this->rightAttribute) . sprintf('%+d', $delta)), | |
$this->levelAttribute => new CDbExpression($db->quoteColumnName($this->levelAttribute) . sprintf('%+d', $levelDelta)), | |
$this->rootAttribute => $lastRoot, | |
), | |
$db->quoteColumnName($this->leftAttribute) . '>=' . $left . ' AND ' . | |
$db->quoteColumnName($this->rightAttribute) . '<=' . $right . ' AND ' . | |
$db->quoteColumnName($this->rootAttribute) . '=' . CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount, | |
array(CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount++ => $owner->{$this->rootAttribute})); | |
$this->shiftLeftRight($right + 1, $left - $right - 1); | |
if (isset($transaction)) | |
$transaction->commit(); | |
$this->correctCachedOnMoveBetweenTrees(1, $levelDelta, $owner->getPrimaryKey()); | |
} catch (Exception $e) { | |
if (isset($transaction)) | |
$transaction->rollback(); | |
throw $e; | |
} | |
return true; | |
} | |
/** | |
* Determines if node is descendant of subject node. | |
* @param CActiveRecord $subj the subject node. | |
* @return boolean whether the node is descendant of subject node. | |
*/ | |
public function isDescendantOf($subj) | |
{ | |
$owner = $this->getOwner(); | |
$result = ($owner->{$this->leftAttribute} > $subj->{$this->leftAttribute}) | |
&& ($owner->{$this->rightAttribute} < $subj->{$this->rightAttribute}); | |
$result = $result && ($owner->{$this->rootAttribute} === $subj->{$this->rootAttribute}); | |
return $result; | |
} | |
/** | |
* Determines if node is leaf. | |
* @return boolean whether the node is leaf. | |
*/ | |
public function isLeaf() | |
{ | |
$owner = $this->getOwner(); | |
return $owner->{$this->rightAttribute} - $owner->{$this->leftAttribute} === 1; | |
} | |
/** | |
* Determines if node is root. | |
* @return boolean whether the node is root. | |
*/ | |
public function isRoot() | |
{ | |
return $this->getOwner()->{$this->leftAttribute} == 1; | |
} | |
/** | |
* Returns if the current node is deleted. | |
* @return boolean whether the node is deleted. | |
*/ | |
public function getIsDeletedRecord() | |
{ | |
return $this->_deleted; | |
} | |
/** | |
* Sets if the current node is deleted. | |
* @param boolean $value whether the node is deleted. | |
*/ | |
public function setIsDeletedRecord($value) | |
{ | |
$this->_deleted = $value; | |
} | |
/** | |
* Handle 'afterConstruct' event of the owner. | |
* @param CEvent $event event parameter. | |
*/ | |
public function afterConstruct($event) | |
{ | |
$owner = $this->getOwner(); | |
self::$_cached[get_class($owner)][$this->_id = self::$_c++] = $owner; | |
} | |
/** | |
* Handle 'afterFind' event of the owner. | |
* @param CEvent $event event parameter. | |
*/ | |
public function afterFind($event) | |
{ | |
$owner = $this->getOwner(); | |
self::$_cached[get_class($owner)][$this->_id = self::$_c++] = $owner; | |
} | |
/** | |
* Handle 'beforeSave' event of the owner. | |
* @param CEvent $event event parameter. | |
* @return boolean. | |
* @throws CDbException | |
*/ | |
public function beforeSave($event) | |
{ | |
if ($this->_ignoreEvent) | |
return true; | |
else | |
throw new CDbException(Yii::t('yiiext', 'You should not use CActiveRecord::save() method when NestedSetBehavior attached.')); | |
} | |
/** | |
* Handle 'beforeDelete' event of the owner. | |
* @param CEvent $event event parameter. | |
* @return boolean. | |
* @throws CDbException | |
*/ | |
public function beforeDelete($event) | |
{ | |
if ($this->_ignoreEvent) | |
return true; | |
else | |
throw new CDbException(Yii::t('yiiext', 'You should not use CActiveRecord::delete() method when NestedSetBehavior attached.')); | |
} | |
/** | |
* @param int $key . | |
* @param int $delta . | |
*/ | |
private function shiftLeftRight($key, $delta) | |
{ | |
$owner = $this->getOwner(); | |
$db = $owner->getDbConnection(); | |
foreach (array($this->leftAttribute, $this->rightAttribute) as $attribute) { | |
$condition = $db->quoteColumnName($attribute) . '>=' . $key; | |
$params = array(); | |
$condition .= ' AND ' . $db->quoteColumnName($this->rootAttribute) . '=' . CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount; | |
$params[CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount++] = $owner->{$this->rootAttribute}; | |
$owner->updateAll(array($attribute => new CDbExpression($db->quoteColumnName($attribute) . sprintf('%+d', $delta))), $condition, $params); | |
} | |
} | |
/** | |
* @param CActiveRecord $target . | |
* @param int $key . | |
* @param int $levelUp . | |
* @param boolean $runValidation . | |
* @param array $attributes . | |
* @return boolean. | |
* @throws CDbException | |
* @throws CException | |
* @throws Exception | |
*/ | |
private function addNode($target, $key, $levelUp, $runValidation, $attributes) | |
{ | |
$owner = $this->getOwner(); | |
if (!$owner->getIsNewRecord()) | |
throw new CDbException(Yii::t('yiiext', 'The node cannot be inserted because it is not new.')); | |
if ($this->getIsDeletedRecord()) | |
throw new CDbException(Yii::t('yiiext', 'The node cannot be inserted because it is deleted.')); | |
if ($target->getIsDeletedRecord()) | |
throw new CDbException(Yii::t('yiiext', 'The node cannot be inserted because target node is deleted.')); | |
if ($owner->equals($target)) | |
throw new CException(Yii::t('yiiext', 'The target node should not be self.')); | |
if (!$levelUp && $target->isRoot()) | |
throw new CException(Yii::t('yiiext', 'The target node should not be root.')); | |
if ($runValidation && !$owner->validate()) | |
return false; | |
$owner->{$this->rootAttribute} = $target->{$this->rootAttribute}; | |
$db = $owner->getDbConnection(); | |
if ($db->getCurrentTransaction() === null) | |
$transaction = $db->beginTransaction(); | |
try { | |
$this->shiftLeftRight($key, 2); | |
$owner->{$this->leftAttribute} = $key; | |
$owner->{$this->rightAttribute} = $key + 1; | |
$owner->{$this->levelAttribute} = $target->{$this->levelAttribute} + $levelUp; | |
$this->_ignoreEvent = true; | |
$result = $owner->insert($attributes); | |
$this->_ignoreEvent = false; | |
if (!$result) { | |
if (isset($transaction)) | |
$transaction->rollback(); | |
return false; | |
} | |
if (isset($transaction)) | |
$transaction->commit(); | |
$this->correctCachedOnAddNode($key); | |
} catch (Exception $e) { | |
if (isset($transaction)) | |
$transaction->rollback(); | |
throw $e; | |
} | |
return true; | |
} | |
/** | |
* @param array $attributes . | |
* @return boolean. | |
* @throws CException | |
* @throws Exception | |
*/ | |
private function makeRoot($attributes) | |
{ | |
$owner = $this->getOwner(); | |
$owner->{$this->leftAttribute} = 1; | |
$owner->{$this->rightAttribute} = 2; | |
$owner->{$this->levelAttribute} = 1; | |
$db = $owner->getDbConnection(); | |
if ($db->getCurrentTransaction() === null) | |
$transaction = $db->beginTransaction(); | |
try { | |
$this->_ignoreEvent = true; | |
$result = $owner->insert($attributes); | |
$this->_ignoreEvent = false; | |
if (!$result) { | |
if (isset($transaction)) | |
$transaction->rollback(); | |
return false; | |
} | |
$lastRoot = $this->getLastRoot($owner); | |
$owner->{$this->rootAttribute} = $lastRoot; | |
$owner->updateByPk($owner->getPrimaryKey(), array( | |
$this->rootAttribute => $lastRoot | |
)); | |
if (isset($transaction)) | |
$transaction->commit(); | |
} catch (Exception $e) { | |
if (isset($transaction)) | |
$transaction->rollback(); | |
throw $e; | |
} | |
return true; | |
} | |
/** | |
* @param $owner CActiveRecord | |
* @return CActiveRecord | |
*/ | |
protected function getLastRoot(CActiveRecord $owner) | |
{ | |
$db = $owner->getDbConnection(); | |
$criteria=new CDbCriteria(); | |
$criteria->select = 'MAX(' . $db->quoteColumnName($this->rootAttribute) . ')+1 AS ' . $this->rootAttribute; | |
$lastRoot = $owner->model()->find($criteria); | |
return $lastRoot->{$this->rootAttribute} ? $lastRoot->{$this->rootAttribute} : 1; | |
} | |
/** | |
* @param CActiveRecord $target . | |
* @param int $key . | |
* @param int $levelUp . | |
* @return boolean. | |
* @throws CDbException | |
* @throws CException | |
* @throws Exception | |
*/ | |
private function moveNode($target, $key, $levelUp) | |
{ | |
$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->getIsDeletedRecord()) | |
throw new CDbException(Yii::t('yiiext', 'The target node should not be deleted.')); | |
if ($owner->equals($target)) | |
throw new CException(Yii::t('yiiext', 'The target node should not be self.')); | |
if ($target->isDescendantOf($owner)) | |
throw new CException(Yii::t('yiiext', 'The target node should not be descendant.')); | |
if (!$levelUp && $target->isRoot()) | |
throw new CException(Yii::t('yiiext', 'The target node should not be root.')); | |
$db = $owner->getDbConnection(); | |
if ($db->getCurrentTransaction() === null) | |
$transaction = $db->beginTransaction(); | |
try { | |
$left = $owner->{$this->leftAttribute}; | |
$right = $owner->{$this->rightAttribute}; | |
$levelDelta = $target->{$this->levelAttribute} - $owner->{$this->levelAttribute} + $levelUp; | |
if ($owner->{$this->rootAttribute} !== $target->{$this->rootAttribute}) { | |
foreach (array($this->leftAttribute, $this->rightAttribute) as $attribute) { | |
$owner->updateAll(array($attribute => new CDbExpression($db->quoteColumnName($attribute) . sprintf('%+d', $right - $left + 1))), | |
$db->quoteColumnName($attribute) . '>=' . $key . ' AND ' . $db->quoteColumnName($this->rootAttribute) . '=' . CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount, | |
array(CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount++ => $target->{$this->rootAttribute})); | |
} | |
$delta = $key - $left; | |
$owner->updateAll( | |
array( | |
$this->leftAttribute => new CDbExpression($db->quoteColumnName($this->leftAttribute) . sprintf('%+d', $delta)), | |
$this->rightAttribute => new CDbExpression($db->quoteColumnName($this->rightAttribute) . sprintf('%+d', $delta)), | |
$this->levelAttribute => new CDbExpression($db->quoteColumnName($this->levelAttribute) . sprintf('%+d', $levelDelta)), | |
$this->rootAttribute => $target->{$this->rootAttribute}, | |
), | |
$db->quoteColumnName($this->leftAttribute) . '>=' . $left . ' AND ' . | |
$db->quoteColumnName($this->rightAttribute) . '<=' . $right . ' AND ' . | |
$db->quoteColumnName($this->rootAttribute) . '=' . CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount, | |
array(CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount++ => $owner->{$this->rootAttribute})); | |
$this->shiftLeftRight($right + 1, $left - $right - 1); | |
if (isset($transaction)) | |
$transaction->commit(); | |
$this->correctCachedOnMoveBetweenTrees($key, $levelDelta, $target->{$this->rootAttribute}); | |
} else { | |
$delta = $right - $left + 1; | |
$this->shiftLeftRight($key, $delta); | |
if ($left >= $key) { | |
$left += $delta; | |
$right += $delta; | |
} | |
$condition = $db->quoteColumnName($this->leftAttribute) . '>=' . $left . ' AND ' . $db->quoteColumnName($this->rightAttribute) . '<=' . $right; | |
$params = array(); | |
$condition .= ' AND ' . $db->quoteColumnName($this->rootAttribute) . '=' . CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount; | |
$params[CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount++] = $owner->{$this->rootAttribute}; | |
$owner->updateAll(array($this->levelAttribute => new CDbExpression($db->quoteColumnName($this->levelAttribute) . sprintf('%+d', $levelDelta))), $condition, $params); | |
foreach (array($this->leftAttribute, $this->rightAttribute) as $attribute) { | |
$condition = $db->quoteColumnName($attribute) . '>=' . $left . ' AND ' . $db->quoteColumnName($attribute) . '<=' . $right; | |
$params = array(); | |
$condition .= ' AND ' . $db->quoteColumnName($this->rootAttribute) . '=' . CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount; | |
$params[CDbCriteria::PARAM_PREFIX . CDbCriteria::$paramCount++] = $owner->{$this->rootAttribute}; | |
$owner->updateAll(array($attribute => new CDbExpression($db->quoteColumnName($attribute) . sprintf('%+d', $key - $left))), $condition, $params); | |
} | |
$this->shiftLeftRight($right + 1, -$delta); | |
if (isset($transaction)) | |
$transaction->commit(); | |
$this->correctCachedOnMoveNode($key, $levelDelta); | |
} | |
} catch (Exception $e) { | |
if (isset($transaction)) | |
$transaction->rollback(); | |
throw $e; | |
} | |
return true; | |
} | |
/** | |
* Correct cache for {@link NestedSetBehavior::delete()} and {@link NestedSetBehavior::deleteNode()}. | |
*/ | |
private function correctCachedOnDelete() | |
{ | |
$owner = $this->getOwner(); | |
$left = $owner->{$this->leftAttribute}; | |
$right = $owner->{$this->rightAttribute}; | |
$key = $right + 1; | |
$delta = $left - $right - 1; | |
foreach (self::$_cached[get_class($owner)] as $node) { | |
if ($node->getIsNewRecord() || $node->getIsDeletedRecord()) | |
continue; | |
if ($owner->{$this->rootAttribute} !== $node->{$this->rootAttribute}) | |
continue; | |
if ($node->{$this->leftAttribute} >= $left && $node->{$this->rightAttribute} <= $right) | |
$node->setIsDeletedRecord(true); | |
else { | |
if ($node->{$this->leftAttribute} >= $key) | |
$node->{$this->leftAttribute} += $delta; | |
if ($node->{$this->rightAttribute} >= $key) | |
$node->{$this->rightAttribute} += $delta; | |
} | |
} | |
} | |
/** | |
* Correct cache for {@link NestedSetBehavior::addNode()}. | |
* @param int $key . | |
*/ | |
private function correctCachedOnAddNode($key) | |
{ | |
$owner = $this->getOwner(); | |
foreach (self::$_cached[get_class($owner)] as $node) { | |
if ($node->getIsNewRecord() || $node->getIsDeletedRecord()) | |
continue; | |
if ($owner->{$this->rootAttribute} !== $node->{$this->rootAttribute}) | |
continue; | |
if ($owner === $node) | |
continue; | |
if ($node->{$this->leftAttribute} >= $key) | |
$node->{$this->leftAttribute} += 2; | |
if ($node->{$this->rightAttribute} >= $key) | |
$node->{$this->rightAttribute} += 2; | |
} | |
} | |
/** | |
* Correct cache for {@link NestedSetBehavior::moveNode()}. | |
* @param int $key . | |
* @param int $levelDelta . | |
*/ | |
private function correctCachedOnMoveNode($key, $levelDelta) | |
{ | |
$owner = $this->getOwner(); | |
$left = $owner->{$this->leftAttribute}; | |
$right = $owner->{$this->rightAttribute}; | |
$delta = $right - $left + 1; | |
if ($left >= $key) { | |
$left += $delta; | |
$right += $delta; | |
} | |
$delta2 = $key - $left; | |
foreach (self::$_cached[get_class($owner)] as $node) { | |
if ($node->getIsNewRecord() || $node->getIsDeletedRecord()) | |
continue; | |
if ($owner->{$this->rootAttribute} !== $node->{$this->rootAttribute}) | |
continue; | |
if ($node->{$this->leftAttribute} >= $key) | |
$node->{$this->leftAttribute} += $delta; | |
if ($node->{$this->rightAttribute} >= $key) | |
$node->{$this->rightAttribute} += $delta; | |
if ($node->{$this->leftAttribute} >= $left && $node->{$this->rightAttribute} <= $right) | |
$node->{$this->levelAttribute} += $levelDelta; | |
if ($node->{$this->leftAttribute} >= $left && $node->{$this->leftAttribute} <= $right) | |
$node->{$this->leftAttribute} += $delta2; | |
if ($node->{$this->rightAttribute} >= $left && $node->{$this->rightAttribute} <= $right) | |
$node->{$this->rightAttribute} += $delta2; | |
if ($node->{$this->leftAttribute} >= $right + 1) | |
$node->{$this->leftAttribute} -= $delta; | |
if ($node->{$this->rightAttribute} >= $right + 1) | |
$node->{$this->rightAttribute} -= $delta; | |
} | |
} | |
/** | |
* Correct cache for {@link NestedSetBehavior::moveNode()}. | |
* @param int $key . | |
* @param int $levelDelta . | |
* @param int $root . | |
*/ | |
private function correctCachedOnMoveBetweenTrees($key, $levelDelta, $root) | |
{ | |
$owner = $this->getOwner(); | |
$left = $owner->{$this->leftAttribute}; | |
$right = $owner->{$this->rightAttribute}; | |
$delta = $right - $left + 1; | |
$delta2 = $key - $left; | |
$delta3 = $left - $right - 1; | |
foreach (self::$_cached[get_class($owner)] as $node) { | |
if ($node->getIsNewRecord() || $node->getIsDeletedRecord()) | |
continue; | |
if ($node->{$this->rootAttribute} === $root) { | |
if ($node->{$this->leftAttribute} >= $key) | |
$node->{$this->leftAttribute} += $delta; | |
if ($node->{$this->rightAttribute} >= $key) | |
$node->{$this->rightAttribute} += $delta; | |
} else if ($node->{$this->rootAttribute} === $owner->{$this->rootAttribute}) { | |
if ($node->{$this->leftAttribute} >= $left && $node->{$this->rightAttribute} <= $right) { | |
$node->{$this->leftAttribute} += $delta2; | |
$node->{$this->rightAttribute} += $delta2; | |
$node->{$this->levelAttribute} += $levelDelta; | |
$node->{$this->rootAttribute} = $root; | |
} else { | |
if ($node->{$this->leftAttribute} >= $right + 1) | |
$node->{$this->leftAttribute} += $delta3; | |
if ($node->{$this->rightAttribute} >= $right + 1) | |
$node->{$this->rightAttribute} += $delta3; | |
} | |
} | |
} | |
} | |
/** | |
* Destructor. | |
*/ | |
public function __destruct() | |
{ | |
unset(self::$_cached[get_class($this->getOwner())][$this->_id]); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment