<?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