| 1 |
<?php |
|---|
| 2 |
|
|---|
| 3 |
|
|---|
| 4 |
|
|---|
| 5 |
|
|---|
| 6 |
|
|---|
| 7 |
|
|---|
| 8 |
|
|---|
| 9 |
|
|---|
| 10 |
|
|---|
| 11 |
|
|---|
| 12 |
|
|---|
| 13 |
|
|---|
| 14 |
|
|---|
| 15 |
|
|---|
| 16 |
|
|---|
| 17 |
|
|---|
| 18 |
|
|---|
| 19 |
|
|---|
| 20 |
require_once(AK_LIB_DIR.DS.'AkActiveRecord'.DS.'AkObserver.php'); |
|---|
| 21 |
|
|---|
| 22 |
class AkActsAsNestedSet extends AkObserver |
|---|
| 23 |
{ |
|---|
| 24 |
|
|---|
| 25 |
|
|---|
| 26 |
* This acts provides Nested Set functionality. Nested Set is similiar to Tree, but with |
|---|
| 27 |
* the added feature that you can select the children and all of it's descendants with |
|---|
| 28 |
* a single query. A good use case for this is a threaded post system, where you want |
|---|
| 29 |
* to display every reply to a comment without multiple selects. |
|---|
| 30 |
* |
|---|
| 31 |
* A google search for "Nested Set" should point you in the direction to explain the |
|---|
| 32 |
* data base theory. I figured a bunch of this from |
|---|
| 33 |
* http://threebit.net/tutorials/nestedset/tutorial1.html |
|---|
| 34 |
* |
|---|
| 35 |
* Instead of picturing a leaf node structure with child pointing back to their parent, |
|---|
| 36 |
* the best way to imagine how this works is to think of the parent entity surrounding all |
|---|
| 37 |
* of it's children, and it's parent surrounding it, etc. Assuming that they are lined up |
|---|
| 38 |
* horizontally, we store the left and right boundaries in the database. |
|---|
| 39 |
* |
|---|
| 40 |
* Imagine: |
|---|
| 41 |
* root |
|---|
| 42 |
* |_ Child 1 |
|---|
| 43 |
* |_ Child 1.1 |
|---|
| 44 |
* |_ Child 1.2 |
|---|
| 45 |
* |_ Child 2 |
|---|
| 46 |
* |_ Child 2.1 |
|---|
| 47 |
* |_ Child 2.2 |
|---|
| 48 |
* |
|---|
| 49 |
* If my circles in circles description didn't make sense, check out this sweet |
|---|
| 50 |
* ASCII art: |
|---|
| 51 |
* |
|---|
| 52 |
* ___________________________________________________________________ |
|---|
| 53 |
* | Root | |
|---|
| 54 |
* | ____________________________ ____________________________ | |
|---|
| 55 |
* | | Child 1 | | Child 2 | | |
|---|
| 56 |
* | | __________ _________ | | __________ _________ | | |
|---|
| 57 |
* | | | C 1.1 | | C 1.2 | | | | C 2.1 | | C 2.2 | | | |
|---|
| 58 |
* 1 2 3_________4 5________6 7 8 9_________10 11_______12 13 14 |
|---|
| 59 |
* | |___________________________| |___________________________| | |
|---|
| 60 |
* |___________________________________________________________________| |
|---|
| 61 |
* |
|---|
| 62 |
* The numbers represent the left and right boundaries. The table them might |
|---|
| 63 |
* look like this: |
|---|
| 64 |
* ID | PARENT | LEFT | RIGHT | DATA |
|---|
| 65 |
* 1 | 0 | 1 | 14 | root |
|---|
| 66 |
* 2 | 1 | 2 | 7 | Child 1 |
|---|
| 67 |
* 3 | 2 | 3 | 4 | Child 1.1 |
|---|
| 68 |
* 4 | 2 | 5 | 6 | Child 1.2 |
|---|
| 69 |
* 5 | 1 | 8 | 13 | Child 2 |
|---|
| 70 |
* 6 | 5 | 9 | 10 | Child 2.1 |
|---|
| 71 |
* 7 | 5 | 11 | 12 | Child 2.2 |
|---|
| 72 |
* |
|---|
| 73 |
* So, to get all children of an entry, you |
|---|
| 74 |
* SELECT * WHERE CHILD.LEFT IS BETWEEN PARENT.LEFT AND PARENT.RIGHT |
|---|
| 75 |
* |
|---|
| 76 |
* To get the count, it's (LEFT - RIGHT + 1)/2, etc. |
|---|
| 77 |
* |
|---|
| 78 |
* To get the direct parent, it falls back to using the PARENT_ID field. |
|---|
| 79 |
* |
|---|
| 80 |
* There are instance methods for all of these. |
|---|
| 81 |
* |
|---|
| 82 |
* The structure is good if you need to group things together; the downside is that |
|---|
| 83 |
* keeping data integrity is a pain, and both adding and removing and entry |
|---|
| 84 |
* require a full table write. |
|---|
| 85 |
* |
|---|
| 86 |
* This sets up a beforeDestroy() trigger to prune the tree correctly if one of it's |
|---|
| 87 |
* elements gets deleted. |
|---|
| 88 |
* |
|---|
| 89 |
*/ |
|---|
| 90 |
|
|---|
| 91 |
/** |
|---|
| 92 |
* Configuration options are: |
|---|
| 93 |
* |
|---|
| 94 |
* * +parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id) |
|---|
| 95 |
* * +left_column+ - column name for left boundary data, default "lft" |
|---|
| 96 |
* * +right_column+ - column name for right boundary data, default "rgt" |
|---|
| 97 |
* * +scope+ - restricts what is to be considered a list. |
|---|
| 98 |
* Example: <tt>actsAsList(array('scope' => array('todo_list_id = ? AND completed = 0',$todo_list_id)));</tt> |
|---|
| 99 |
*/ |
|---|
| 100 |
|
|---|
| 101 |
public $_scope_condition; |
|---|
| 102 |
public $_left_column_name = 'lft'; |
|---|
| 103 |
public $_right_column_name = 'rgt'; |
|---|
| 104 |
public $_parent_column_name = 'parent_id'; |
|---|
| 105 |
|
|---|
| 106 |
public $_ActiveRecordInstance; |
|---|
| 107 |
|
|---|
| 108 |
public function AkActsAsNestedSet(&$ActiveRecordInstance) |
|---|
| 109 |
{ |
|---|
| 110 |
$this->_ActiveRecordInstance =& $ActiveRecordInstance; |
|---|
| 111 |
} |
|---|
| 112 |
|
|---|
| 113 |
public function init($options = array()) |
|---|
| 114 |
{ |
|---|
| 115 |
empty($options['parent_column']) ? null : ($this->_parent_column_name = $options['parent_column']); |
|---|
| 116 |
empty($options['left_column']) ? null : ($this->_left_column_name = $options['left_column']); |
|---|
| 117 |
empty($options['right_column']) ? null : ($this->_right_column_name = $options['right_column']); |
|---|
| 118 |
empty($options['scope']) ? null : $this->setScopeCondition($options['scope']); |
|---|
| 119 |
return $this->_ensureIsActiveRecordInstance($this->_ActiveRecordInstance); |
|---|
| 120 |
} |
|---|
| 121 |
|
|---|
| 122 |
|
|---|
| 123 |
public function _ensureIsActiveRecordInstance(&$ActiveRecordInstance) |
|---|
| 124 |
{ |
|---|
| 125 |
if(is_object($ActiveRecordInstance) && method_exists($ActiveRecordInstance,'actsLike')){ |
|---|
| 126 |
$this->_ActiveRecordInstance =& $ActiveRecordInstance; |
|---|
| 127 |
if(!$this->_ActiveRecordInstance->hasColumn($this->_parent_column_name) || !$this->_ActiveRecordInstance->hasColumn($this->_left_column_name) || !$this->_ActiveRecordInstance->hasColumn($this->_right_column_name)){ |
|---|
| 128 |
trigger_error(Ak::t( |
|---|
| 129 |
'The following columns are required in the table "%table" for the model "%model" to act as a Nested Set: "%columns".',array( |
|---|
| 130 |
'%columns'=>$this->getParentColumnName().', '.$this->getLeftColumnName().', '.$this->getRightColumnName(),'%table'=>$this->_ActiveRecordInstance->getTableName(),'%model'=>$this->_ActiveRecordInstance->getModelName())),E_USER_ERROR); |
|---|
| 131 |
unset($this->_ActiveRecordInstance->nested_set); |
|---|
| 132 |
return false; |
|---|
| 133 |
}else{ |
|---|
| 134 |
$this->observe(&$ActiveRecordInstance); |
|---|
| 135 |
} |
|---|
| 136 |
}else{ |
|---|
| 137 |
trigger_error(Ak::t('You are trying to set an object that is not an active record.'), E_USER_ERROR); |
|---|
| 138 |
return false; |
|---|
| 139 |
} |
|---|
| 140 |
return true; |
|---|
| 141 |
} |
|---|
| 142 |
|
|---|
| 143 |
public function getType() |
|---|
| 144 |
{ |
|---|
| 145 |
return 'nested set'; |
|---|
| 146 |
} |
|---|
| 147 |
|
|---|
| 148 |
public function getScopeCondition() |
|---|
| 149 |
{ |
|---|
| 150 |
if (!empty($this->variable_scope_condition)){ |
|---|
| 151 |
return $this->_ActiveRecordInstance->_getVariableSqlCondition($this->variable_scope_condition); |
|---|
| 152 |
|
|---|
| 153 |
|
|---|
| 154 |
}elseif(empty($this->scope_condition) && empty($this->scope)){ |
|---|
| 155 |
$this->scope_condition = ($this->_ActiveRecordInstance->_db->type() == 'postgre') ? 'true' : '1'; |
|---|
| 156 |
}elseif (!empty($this->scope)){ |
|---|
| 157 |
$this->setScopeCondition(join(' AND ',array_map(array(&$this,'getScopedColumn'),(array)$this->scope))); |
|---|
| 158 |
} |
|---|
| 159 |
return $this->scope_condition; |
|---|
| 160 |
} |
|---|
| 161 |
|
|---|
| 162 |
|
|---|
| 163 |
public function setScopeCondition($scope_condition) |
|---|
| 164 |
{ |
|---|
| 165 |
if(!is_array($scope_condition) && strstr($scope_condition, '?')){ |
|---|
| 166 |
$this->variable_scope_condition = $scope_condition; |
|---|
| 167 |
}else{ |
|---|
| 168 |
$this->scope_condition = $scope_condition; |
|---|
| 169 |
} |
|---|
| 170 |
} |
|---|
| 171 |
|
|---|
| 172 |
public function getScopedColumn($column) |
|---|
| 173 |
{ |
|---|
| 174 |
if($this->_ActiveRecordInstance->hasColumn($column)){ |
|---|
| 175 |
$value = $this->_ActiveRecordInstance->get($column); |
|---|
| 176 |
$condition = $this->_ActiveRecordInstance->getAttributeCondition($value); |
|---|
| 177 |
$value = $this->_ActiveRecordInstance->castAttributeForDatabase($column, $value); |
|---|
| 178 |
return $column.' '.str_replace('?', $value, $condition); |
|---|
| 179 |
}else{ |
|---|
| 180 |
return $column; |
|---|
| 181 |
} |
|---|
| 182 |
} |
|---|
| 183 |
|
|---|
| 184 |
public function getLeftColumnName() |
|---|
| 185 |
{ |
|---|
| 186 |
return $this->_left_column_name; |
|---|
| 187 |
} |
|---|
| 188 |
public function setLeftColumnName($left_column_name) |
|---|
| 189 |
{ |
|---|
| 190 |
$this->_left_column_name = $left_column_name; |
|---|
| 191 |
} |
|---|
| 192 |
|
|---|
| 193 |
public function getRightColumnName() |
|---|
| 194 |
{ |
|---|
| 195 |
return $this->_right_column_name; |
|---|
| 196 |
} |
|---|
| 197 |
public function setRightColumnName($right_column_name) |
|---|
| 198 |
{ |
|---|
| 199 |
$this->_right_column_name = $right_column_name; |
|---|
| 200 |
} |
|---|
| 201 |
|
|---|
| 202 |
|
|---|
| 203 |
public function getParentColumnName() |
|---|
| 204 |
{ |
|---|
| 205 |
return $this->_parent_column_name; |
|---|
| 206 |
} |
|---|
| 207 |
public function setParentColumnName($parent_column_name) |
|---|
| 208 |
{ |
|---|
| 209 |
$this->_parent_column_name = $parent_column_name; |
|---|
| 210 |
} |
|---|
| 211 |
|
|---|
| 212 |
|
|---|
| 213 |
* Returns true is this is a root node. |
|---|
| 214 |
*/ |
|---|
| 215 |
public function isRoot() |
|---|
| 216 |
{ |
|---|
| 217 |
$left_id = $this->_ActiveRecordInstance->get($this->getLeftColumnName()); |
|---|
| 218 |
return ($this->_ActiveRecordInstance->get($this->getParentColumnName()) == null) && ($left_id == 1) && ($this->_ActiveRecordInstance->get($this->getRightColumnName()) > $left_id); |
|---|
| 219 |
} |
|---|
| 220 |
|
|---|
| 221 |
|
|---|
| 222 |
* Returns true is this is a child node |
|---|
| 223 |
*/ |
|---|
| 224 |
public function isChild() |
|---|
| 225 |
{ |
|---|
| 226 |
$parent_id = $this->_ActiveRecordInstance->get($this->getParentColumnName()); |
|---|
| 227 |
$left_id = $this->_ActiveRecordInstance->get($this->getLeftColumnName()); |
|---|
| 228 |
return !($parent_id == 0 || is_null($parent_id)) && ($left_id > 1) && ($this->_ActiveRecordInstance->get($this->getRightColumnName()) > $left_id); |
|---|
| 229 |
} |
|---|
| 230 |
|
|---|
| 231 |
|
|---|
| 232 |
* Returns true if we have no idea what this is |
|---|
| 233 |
*/ |
|---|
| 234 |
public function isUnknown() |
|---|
| 235 |
{ |
|---|
| 236 |
return !$this->isRoot() && !$this->isChild(); |
|---|
| 237 |
} |
|---|
| 238 |
|
|---|
| 239 |
|
|---|
| 240 |
* Added a child to this object in the tree. If this object hasn't been initialized, |
|---|
| 241 |
* it gets set up as a root node. Otherwise, this method will update all of the |
|---|
| 242 |
* other elements in the tree and shift them to the right. Keeping everything |
|---|
| 243 |
* balanced. |
|---|
| 244 |
*/ |
|---|
| 245 |
public function addChild( &$child ) |
|---|
| 246 |
{ |
|---|
| 247 |
$self =& $this->_ActiveRecordInstance; |
|---|
| 248 |
$self->reload(); |
|---|
| 249 |
$child->reload(); |
|---|
| 250 |
$left_column = $this->getLeftColumnName(); |
|---|
| 251 |
$right_column = $this->getRightColumnName(); |
|---|
| 252 |
$parent_column = $this->getParentColumnName(); |
|---|
| 253 |
|
|---|
| 254 |
if ($child->nested_set->isRoot()){ |
|---|
| 255 |
trigger_error(Ak::t("Adding sub-tree isn't currently supported"),E_USER_ERROR); |
|---|
| 256 |
}elseif ( (is_null($self->get($left_column))) || (is_null($self->get($right_column))) ){ |
|---|
| 257 |
|
|---|
| 258 |
$self->set($left_column, 1); |
|---|
| 259 |
$self->set($right_column, 4); |
|---|
| 260 |
|
|---|
| 261 |
$self->transactionStart(); |
|---|
| 262 |
|
|---|
| 263 |
if(!$self->save()){ |
|---|
| 264 |
$self->transactionFail(); |
|---|
| 265 |
$self->transactionComplete(); |
|---|
| 266 |
return false; |
|---|
| 267 |
} |
|---|
| 268 |
|
|---|
| 269 |
$child->set($parent_column, $self->getId()); |
|---|
| 270 |
$child->set($left_column, 2); |
|---|
| 271 |
$child->set($right_column, 3); |
|---|
| 272 |
|
|---|
| 273 |
if(!$child->save()){ |
|---|
| 274 |
$self->transactionFail(); |
|---|
| 275 |
$self->transactionComplete(); |
|---|
| 276 |
return false; |
|---|
| 277 |
} |
|---|
| 278 |
$self->transactionComplete(); |
|---|
| 279 |
return $child; |
|---|
| 280 |
}else{ |
|---|
| 281 |
|
|---|
| 282 |
$child->set($parent_column, $self->getId()); |
|---|
| 283 |
$right_bound = $self->get($right_column); |
|---|
| 284 |
$child->set($left_column, $right_bound); |
|---|
| 285 |
$child->set($right_column, $right_bound +1); |
|---|
| 286 |
$self->set($right_column, $self->get($right_column) + 2); |
|---|
| 287 |
|
|---|
| 288 |
$self->transactionStart(); |
|---|
| 289 |
$self->updateAll( "$left_column = ($left_column + 2)", $this->getScopeCondition()." AND $left_column >= $right_bound" ); |
|---|
| 290 |
$self->updateAll( "$right_column = ($right_column + 2)", $this->getScopeCondition()." AND $right_column >= $right_bound" ); |
|---|
| 291 |
$self->save(); |
|---|
| 292 |
$child->save(); |
|---|
| 293 |
$self->transactionComplete(); |
|---|
| 294 |
return $child; |
|---|
| 295 |
} |
|---|
| 296 |
} |
|---|
| 297 |
|
|---|
| 298 |
|
|---|
| 299 |
|
|---|
| 300 |
* Returns the parent Object |
|---|
| 301 |
*/ |
|---|
| 302 |
public function &getParent() |
|---|
| 303 |
{ |
|---|
| 304 |
if(!$this->isChild()){ |
|---|
| 305 |
$result = false; |
|---|
| 306 |
}else{ |
|---|
| 307 |
$result =& $this->_ActiveRecordInstance->find( |
|---|
| 308 |
|
|---|
| 309 |
'first', array('conditions' => " ".$this->getScopeCondition()." AND ".$this->_ActiveRecordInstance->getPrimaryKey()." = ".$this->_ActiveRecordInstance->{$this->getParentColumnName()}) |
|---|
| 310 |
); |
|---|
| 311 |
} |
|---|
| 312 |
return $result; |
|---|
| 313 |
} |
|---|
| 314 |
|
|---|
| 315 |
|
|---|
| 316 |
* Returns an array of parent Objects this is usefull to make breadcrum like stuctures |
|---|
| 317 |
*/ |
|---|
| 318 |
public function &getParents() |
|---|
| 319 |
{ |
|---|
| 320 |
$Ancestors =& $this->getAncestors(); |
|---|
| 321 |
return $Ancestors; |
|---|
| 322 |
} |
|---|
| 323 |
|
|---|
| 324 |
|
|---|
| 325 |
|
|---|
| 326 |
* Prunes a branch off of the tree, shifting all of the elements on the right |
|---|
| 327 |
* back to the left so the counts still work. |
|---|
| 328 |
*/ |
|---|
| 329 |
public function beforeDestroy(&$object) |
|---|
| 330 |
{ |
|---|
| 331 |
if(!empty($object->__avoid_nested_set_before_destroy_recursion)){ |
|---|
| 332 |
return true; |
|---|
| 333 |
} |
|---|
| 334 |
if((empty($object->{$this->getRightColumnName()}) || empty($object->{$this->getLeftColumnName()})) || $object->nested_set->isUnknown()){ |
|---|
| 335 |
return true; |
|---|
| 336 |
} |
|---|
| 337 |
$dif = $object->{$this->getRightColumnName()} - $object->{$this->getLeftColumnName()} + 1; |
|---|
| 338 |
|
|---|
| 339 |
$ObjectsToDelete = $object->nested_set->getAllChildren(); |
|---|
| 340 |
|
|---|
| 341 |
$object->transactionStart(); |
|---|
| 342 |
|
|---|
| 343 |
if(!empty($ObjectsToDelete)){ |
|---|
| 344 |
foreach (array_keys($ObjectsToDelete) as $k){ |
|---|
| 345 |
$Child =& $ObjectsToDelete[$k]; |
|---|
| 346 |
$Child->__avoid_nested_set_before_destroy_recursion = true; |
|---|
| 347 |
if($Child->beforeDestroy()){ |
|---|
| 348 |
if($Child->notifyObservers('beforeDestroy') === false){ |
|---|
| 349 |
$Child->transactionFail(); |
|---|
| 350 |
} |
|---|
| 351 |
}else{ |
|---|
| 352 |
$Child->transactionFail(); |
|---|
| 353 |
} |
|---|
| 354 |
} |
|---|
| 355 |
} |
|---|
| 356 |
|
|---|
| 357 |
$object->deleteAll($this->getScopeCondition(). |
|---|
| 358 |
" AND ".$this->getLeftColumnName()." > ".$object->{$this->getLeftColumnName()}. |
|---|
| 359 |
" AND ".$this->getRightColumnName()." < ".$object->{$this->getRightColumnName()}); |
|---|
| 360 |
|
|---|
| 361 |
$object->updateAll($this->getLeftColumnName()." = (".$this->getLeftColumnName()." - $dif)", |
|---|
| 362 |
$this->getScopeCondition()." AND ".$this->getLeftColumnName()." >= ".$object->{$this->getRightColumnName()} ); |
|---|
| 363 |
|
|---|
| 364 |
$object->updateAll($this->getRightColumnName()." = (".$this->getRightColumnName()." - $dif )", |
|---|
| 365 |
$this->getScopeCondition()." AND ".$this->getRightColumnName()." >= ".$object->{$this->getRightColumnName()}); |
|---|
| 366 |
|
|---|
| 367 |
|
|---|
| 368 |
if(!empty($ObjectsToDelete)){ |
|---|
| 369 |
foreach (array_keys($ObjectsToDelete) as $k){ |
|---|
| 370 |
$Child =& $ObjectsToDelete[$k]; |
|---|
| 371 |
$Child->__avoid_nested_set_before_destroy_recursion = true; |
|---|
| 372 |
if(!$Child->afterDestroy() || $Child->notifyObservers('afterDestroy') === false){ |
|---|
| 373 |
$Child->transactionFail(); |
|---|
| 374 |
} |
|---|
| 375 |
} |
|---|
| 376 |
} |
|---|
| 377 |
|
|---|
| 378 |
if($object->transactionHasFailed()){ |
|---|
| 379 |
$object->transactionComplete(); |
|---|
| 380 |
return false; |
|---|
| 381 |
} |
|---|
| 382 |
$object->transactionComplete(); |
|---|
| 383 |
|
|---|
| 384 |
return true; |
|---|
| 385 |
} |
|---|
| 386 |
|
|---|
| 387 |
|
|---|
| 388 |
* on creation, set automatically lft and rgt to the end of the tree |
|---|
| 389 |
*/ |
|---|
| 390 |
public function beforeCreate(&$object) |
|---|
| 391 |
{ |
|---|
| 392 |
$object->nested_set->_setLeftAndRightToTheEndOfTheTree(); |
|---|
| 393 |
return true; |
|---|
| 394 |
} |
|---|
| 395 |
|
|---|
| 396 |
public function _setLeftAndRightToTheEndOfTheTree() |
|---|
| 397 |
{ |
|---|
| 398 |
$left = $this->getLeftColumnName(); |
|---|
| 399 |
$right = $this->getRightColumnName(); |
|---|
| 400 |
|
|---|
| 401 |
$maxright = $this->_ActiveRecordInstance->maximum($right, array('conditions'=>$this->getScopeCondition())); |
|---|
| 402 |
$maxright = empty($maxright) ? 0 : $maxright; |
|---|
| 403 |
|
|---|
| 404 |
$this->_ActiveRecordInstance->set($left, $maxright+1); |
|---|
| 405 |
$this->_ActiveRecordInstance->set($right, $maxright+2); |
|---|
| 406 |
} |
|---|
| 407 |
|
|---|
| 408 |
|
|---|
| 409 |
* Returns the single root |
|---|
| 410 |
*/ |
|---|
| 411 |
public function getRoot() |
|---|
| 412 |
{ |
|---|
| 413 |
return $this->_ActiveRecordInstance->find('first', array('conditions' => " ".$this->getScopeCondition()." AND ".$this->getParentColumnName()." IS NULL ")); |
|---|
| 414 |
} |
|---|
| 415 |
|
|---|
| 416 |
|
|---|
| 417 |
* Returns roots when multiple roots (or virtual root, which is the same) |
|---|
| 418 |
*/ |
|---|
| 419 |
public function getRoots() |
|---|
| 420 |
{ |
|---|
| 421 |
return $this->_ActiveRecordInstance->find('all', array('conditions' => " ".$this->getScopeCondition()." AND ".$this->getParentColumnName()." IS NULL ",'order' => $this->getLeftColumnName())); |
|---|
| 422 |
} |
|---|
| 423 |
|
|---|
| 424 |
|
|---|
| 425 |
|
|---|
| 426 |
* Returns an array of all parents |
|---|
| 427 |
*/ |
|---|
| 428 |
public function &getAncestors() |
|---|
| 429 |
{ |
|---|
| 430 |
$Ancestors =& $this->_ActiveRecordInstance->find('all', array('conditions' => ' '.$this->getScopeCondition().' AND '. |
|---|
| 431 |
$this->getLeftColumnName().' < '.$this->_ActiveRecordInstance->get($this->getLeftColumnName()).' AND '. |
|---|
| 432 |
$this->getRightColumnName().' > '.$this->_ActiveRecordInstance->get($this->getRightColumnName()) |
|---|
| 433 |
,'order' => $this->getLeftColumnName())); |
|---|
| 434 |
return $Ancestors; |
|---|
| 435 |
} |
|---|
| 436 |
|
|---|
| 437 |
|
|---|
| 438 |
* Returns the array of all parents and self |
|---|
| 439 |
*/ |
|---|
| 440 |
public function &getSelfAndAncestors() |
|---|
| 441 |
{ |
|---|
| 442 |
if($result =& $this->getAncestors()){ |
|---|
| 443 |
array_push($result, $this->_ActiveRecordInstance); |
|---|
| 444 |
}else{ |
|---|
| 445 |
$result = array(&$this->_ActiveRecordInstance); |
|---|
| 446 |
} |
|---|
| 447 |
return $result; |
|---|
| 448 |
} |
|---|
| 449 |
|
|---|
| 450 |
|
|---|
| 451 |
|
|---|
| 452 |
* Returns the array of all children of the parent, except self |
|---|
| 453 |
*/ |
|---|
| 454 |
public function getSiblings($search_for_self = false) |
|---|
| 455 |
{ |
|---|
| 456 |
return $this->_ActiveRecordInstance->find('all', array('conditions' => ' (('.$this->getScopeCondition().' AND '. |
|---|
| 457 |
$this->getParentColumnName().' = '.$this->_ActiveRecordInstance->get($this->getParentColumnName()).' AND '. |
|---|
| 458 |
$this->_ActiveRecordInstance->getPrimaryKey().' <> '.$this->_ActiveRecordInstance->getId(). |
|---|
| 459 |
($search_for_self&&!$this->_ActiveRecordInstance->isNewRecord()?') OR ('.$this->_ActiveRecordInstance->getPrimaryKey().' = '.$this->_ActiveRecordInstance->quotedId().'))':'))') |
|---|
| 460 |
,'order' => $this->getLeftColumnName())); |
|---|
| 461 |
} |
|---|
| 462 |
|
|---|
| 463 |
|
|---|
| 464 |
* Returns the array of all children of the parent, included self |
|---|
| 465 |
*/ |
|---|
| 466 |
public function getSelfAndSiblings() |
|---|
| 467 |
{ |
|---|
| 468 |
$parent_id = $this->_ActiveRecordInstance->get($this->getParentColumnName()); |
|---|
| 469 |
if(empty($parent_id) || !$result = $this->getSiblings(true)){ |
|---|
| 470 |
$result = array($this->_ActiveRecordInstance); |
|---|
| 471 |
} |
|---|
| 472 |
return $result; |
|---|
| 473 |
} |
|---|
| 474 |
|
|---|
| 475 |
|
|---|
| 476 |
|
|---|
| 477 |
* Returns the level of this object in the tree |
|---|
| 478 |
* root level is 0 |
|---|
| 479 |
*/ |
|---|
| 480 |
public function getLevel() |
|---|
| 481 |
{ |
|---|
| 482 |
$parent_id = $this->_ActiveRecordInstance->get($this->getParentColumnName()); |
|---|
| 483 |
if(empty($parent_id)){ |
|---|
| 484 |
return 0; |
|---|
| 485 |
} |
|---|
| 486 |
return $this->_ActiveRecordInstance->count(' '.$this->getScopeCondition().' AND '. |
|---|
| 487 |
$this->getLeftColumnName().' < '.$this->_ActiveRecordInstance->get($this->getLeftColumnName()).' AND '. |
|---|
| 488 |
$this->getRightColumnName().' > '.$this->_ActiveRecordInstance->get($this->getRightColumnName())); |
|---|
| 489 |
} |
|---|
| 490 |
|
|---|
| 491 |
|
|---|
| 492 |
|
|---|
| 493 |
* Returns the number of all nested children of this object. |
|---|
| 494 |
*/ |
|---|
| 495 |
public function countChildren() |
|---|
| 496 |
{ |
|---|
| 497 |
$children_count = ($this->_ActiveRecordInstance->get($this->getRightColumnName()) - $this->_ActiveRecordInstance->get($this->getLeftColumnName()) - 1)/2; |
|---|
| 498 |
return $children_count > 0 ? $children_count : 0; |
|---|
| 499 |
} |
|---|
| 500 |
|
|---|
| 501 |
|
|---|
| 502 |
|
|---|
| 503 |
* Returns a set of only this entry's immediate children |
|---|
| 504 |
*/ |
|---|
| 505 |
public function getChildren() |
|---|
| 506 |
{ |
|---|
| 507 |
return $this->_ActiveRecordInstance->find('all', array('conditions' => ' '.$this->getScopeCondition().' AND '. |
|---|
| 508 |
$this->getParentColumnName().' = '.$this->_ActiveRecordInstance->getId() |
|---|
| 509 |
,'order' => $this->getLeftColumnName())); |
|---|
| 510 |
} |
|---|
| 511 |
|
|---|
| 512 |
|
|---|
| 513 |
* Returns a set of all of its children and nested children |
|---|
| 514 |
*/ |
|---|
| 515 |
public function getAllChildren() |
|---|
| 516 |
{ |
|---|
| 517 |
$args = func_get_args(); |
|---|
| 518 |
$excluded_ids = array(); |
|---|
| 519 |
if(!empty($args)){ |
|---|
| 520 |
$exclude = count($args) > 1 ? $args : (is_array($args[0]) ? $args[0] : (empty($args[0]) ? false : array($args[0]))); |
|---|
| 521 |
if(!empty($exclude)){ |
|---|
| 522 |
$parent_class_name = get_class($this->_ActiveRecordInstance); |
|---|
| 523 |
foreach (array_keys($exclude) as $k){ |
|---|
| 524 |
$Item =& $exclude[$k]; |
|---|
| 525 |
if(is_a($Item,$parent_class_name)){ |
|---|
| 526 |
$ItemToExclude =& $Item; |
|---|
| 527 |
}else{ |
|---|
| 528 |
$ItemToExclude =& $this->_ActiveRecordInstance->find($Item); |
|---|
| 529 |
} |
|---|
| 530 |
if($ItemSet = $ItemToExclude->nested_set->getFullSet()){ |
|---|
| 531 |
foreach (array_keys($ItemSet) as $l){ |
|---|
| 532 |
$excluded_ids[] = $ItemSet[$l]->getId(); |
|---|
| 533 |
} |
|---|
| 534 |
} |
|---|
| 535 |
} |
|---|
| 536 |
$excluded_ids = array_unique(array_diff($excluded_ids,array(''))); |
|---|
| 537 |
} |
|---|
| 538 |
} |
|---|
| 539 |
return $this->_ActiveRecordInstance->find('all', array('conditions' => ' '.$this->getScopeCondition().' AND '. |
|---|
| 540 |
(empty($excluded_ids) ? '' : ' id NOT IN ('.join(',',$excluded_ids).') AND '). |
|---|
| 541 |
$this->getLeftColumnName().' > '.$this->_ActiveRecordInstance->get($this->getLeftColumnName()).' AND '. |
|---|
| 542 |
$this->getRightColumnName().' < '.$this->_ActiveRecordInstance->get($this->getRightColumnName()) |
|---|
| 543 |
,'order' => $this->getLeftColumnName())); |
|---|
| 544 |
} |
|---|
| 545 |
|
|---|
| 546 |
|
|---|
| 547 |
* Returns a set of itself and all of its nested children |
|---|
| 548 |
*/ |
|---|
| 549 |
public function getFullSet($exclude = null) |
|---|
| 550 |
{ |
|---|
| 551 |
if($this->_ActiveRecordInstance->isNewRecord() || $this->_ActiveRecordInstance->get($this->getRightColumnName()) - $this->_ActiveRecordInstance->get($this->getLeftColumnName()) == 1 ){ |
|---|
| 552 |
$result = array($this->_ActiveRecordInstance); |
|---|
| 553 |
}else{ |
|---|
| 554 |
(array)$result = $this->getAllChildren($exclude); |
|---|
| 555 |
array_unshift($result, $this->_ActiveRecordInstance); |
|---|
| 556 |
} |
|---|
| 557 |
return $result; |
|---|
| 558 |
} |
|---|
| 559 |
|
|---|
| 560 |
|
|---|
| 561 |
|
|---|
| 562 |
* Move the node to the left of another node |
|---|
| 563 |
*/ |
|---|
| 564 |
public function moveToLeftOf($node) |
|---|
| 565 |
{ |
|---|
| 566 |
return $this->moveTo($node, 'left'); |
|---|
| 567 |
} |
|---|
| 568 |
|
|---|
| 569 |
|
|---|
| 570 |
* Move the node to the left of another node |
|---|
| 571 |
*/ |
|---|
| 572 |
public function moveToRightOf($node) |
|---|
| 573 |
{ |
|---|
| 574 |
return $this->moveTo($node, 'right'); |
|---|
| 575 |
} |
|---|
| 576 |
|
|---|
| 577 |
|
|---|
| 578 |
* Move the node to the child of another node |
|---|
| 579 |
*/ |
|---|
| 580 |
public function moveToChildOf($node) |
|---|
| 581 |
{ |
|---|
| 582 |
return $this->moveTo($node, 'child'); |
|---|
| 583 |
} |
|---|
| 584 |
|
|---|
| 585 |
public function moveTo($target, $position) |
|---|
| 586 |
{ |
|---|
| 587 |
if($this->_ActiveRecordInstance->isNewRecord()){ |
|---|
| 588 |
trigger_error(Ak::t('You cannot move a new node'), E_USER_ERROR); |
|---|
| 589 |
} |
|---|
| 590 |
$current_left = $this->_ActiveRecordInstance->get($this->getLeftColumnName()); |
|---|
| 591 |
$current_right = $this->_ActiveRecordInstance->get($this->getRightColumnName()); |
|---|
| 592 |
|
|---|
| 593 |
$extent = $current_right - $current_left + 1; |
|---|
| 594 |
|
|---|
| 595 |
|
|---|
| 596 |
if (is_numeric($target)){ |
|---|
| 597 |
$target =& $this->_ActiveRecordInstance->find($target); |
|---|
| 598 |
} |
|---|
| 599 |
if(!$target || !is_a($target, get_class($this->_ActiveRecordInstance))){ |
|---|
| 600 |
trigger_error(Ak::t('Invalid target'), E_USER_NOTICE); |
|---|
| 601 |
return false; |
|---|
| 602 |
} |
|---|
| 603 |
|
|---|
| 604 |
$target_left = $target->get($this->getLeftColumnName()); |
|---|
| 605 |
$target_right = $target->get($this->getRightColumnName()); |
|---|
| 606 |
|
|---|
| 607 |
|
|---|
| 608 |
|
|---|
| 609 |
if ((($current_left <= $target_left) && ($target_left <= $current_right)) || (($current_left <= $target_right) && ($target_right <= $current_right))){ |
|---|
| 610 |
trigger_error(Ak::t('Impossible move, target node cannot be inside moved tree.'), E_USER_ERROR); |
|---|
| 611 |
} |
|---|
| 612 |
|
|---|
| 613 |
|
|---|
| 614 |
if ($position == 'child'){ |
|---|
| 615 |
if ($target_left < $current_left){ |
|---|
| 616 |
$new_left = $target_left + 1; |
|---|
| 617 |
$new_right = $target_left + $extent; |
|---|
| 618 |
}else{ |
|---|
| 619 |
$new_left = $target_left - $extent + 1; |
|---|
| 620 |
$new_right = $target_left; |
|---|
| 621 |
} |
|---|
| 622 |
}elseif($position == 'left'){ |
|---|
| 623 |
if ($target_left < $current_left){ |
|---|
| 624 |
$new_left = $target_left; |
|---|
| 625 |
$new_right = $target_left + $extent - 1; |
|---|
| 626 |
}else{ |
|---|
| 627 |
$new_left = $target_left - $extent; |
|---|
| 628 |
$new_right = $target_left - 1; |
|---|
| 629 |
} |
|---|
| 630 |
}elseif($position == 'right'){ |
|---|
| 631 |
if ($target_right < $current_right){ |
|---|
| 632 |
$new_left = $target_right + 1; |
|---|
| 633 |
$new_right = $target_right + $extent; |
|---|
| 634 |
}else{ |
|---|
| 635 |
$new_left = $target_right - $extent + 1; |
|---|
| 636 |
$new_right = $target_right; |
|---|
| 637 |
} |
|---|
| 638 |
}else{ |
|---|
| 639 |
trigger_error(Ak::t("Position should be either left or right ('%position' received).",array('%position'=>$position)), E_USER_ERROR); |
|---|
| 640 |
} |
|---|
| 641 |
|
|---|
| 642 |
|
|---|
| 643 |
$left_boundary = min($current_left, $new_left); |
|---|
| 644 |
$right_boundary = max($current_right, $new_right); |
|---|
| 645 |
|
|---|
| 646 |
|
|---|
| 647 |
$shift = $new_left - $current_left; |
|---|
| 648 |
|
|---|
| 649 |
|
|---|
| 650 |
$updown = ($shift > 0) ? -$extent : $extent; |
|---|
| 651 |
|
|---|
| 652 |
|
|---|
| 653 |
if($position == 'child'){ |
|---|
| 654 |
$new_parent = $target->getId(); |
|---|
| 655 |
}else{ |
|---|
| 656 |
$target_parent = $target->get($this->getParentColumnName()); |
|---|
| 657 |
$new_parent = empty($target_parent) ? 'NULL' : $target_parent; |
|---|
| 658 |
} |
|---|
| 659 |
|
|---|
| 660 |
$this->_ActiveRecordInstance->updateAll( |
|---|
| 661 |
|
|---|
| 662 |
$this->getLeftColumnName().' = CASE '. |
|---|
| 663 |
'WHEN '.$this->getLeftColumnName().' BETWEEN '.$current_left.' AND '.$current_right.' '. |
|---|
| 664 |
'THEN '.$this->getLeftColumnName().' + '.$shift.' '. |
|---|
| 665 |
'WHEN '.$this->getLeftColumnName().' BETWEEN '.$left_boundary.' AND '.$right_boundary.' '. |
|---|
| 666 |
'THEN '.$this->getLeftColumnName().' + '.$updown.' '. |
|---|
| 667 |
'ELSE '.$this->getLeftColumnName().' END, '. |
|---|
| 668 |
|
|---|
| 669 |
$this->getRightColumnName().' = CASE '. |
|---|
| 670 |
'WHEN '.$this->getRightColumnName().' BETWEEN '.$current_left.' AND '.$current_right.' '. |
|---|
| 671 |
'THEN '.$this->getRightColumnName().' + '.$shift.' '. |
|---|
| 672 |
'WHEN '.$this->getRightColumnName().' BETWEEN '.$left_boundary.' AND '.$right_boundary.' '. |
|---|
| 673 |
'THEN '.$this->getRightColumnName().' + '.$updown.' '. |
|---|
| 674 |
'ELSE '.$this->getRightColumnName().' END, '. |
|---|
| 675 |
|
|---|
| 676 |
$this->getParentColumnName().' = CASE '. |
|---|
| 677 |
'WHEN '.$this->_ActiveRecordInstance->getPrimaryKey().' = '.$this->_ActiveRecordInstance->getId().' '. |
|---|
| 678 |
'THEN '.$new_parent.' '. |
|---|
| 679 |
'ELSE '.$this->getParentColumnName().' END', |
|---|
| 680 |
|
|---|
| 681 |
$this->getScopeCondition() ); |
|---|
| 682 |
$this->_ActiveRecordInstance->reload(); |
|---|
| 683 |
|
|---|
| 684 |
return true; |
|---|
| 685 |
} |
|---|
| 686 |
|
|---|
| 687 |
} |
|---|
| 688 |
|
|---|
| 689 |
|
|---|
| 690 |
?> |
|---|