├── README.markdown └── models └── behaviors └── multi_tree.php /README.markdown: -------------------------------------------------------------------------------- 1 | # CakePHP MultiTree Behavior 2 | 3 | [MultiTree][1] is a drop-in behaviour to CakePHP's Core [Tree Behavior][2] allowing for more advanced operations and better performance on large data sets 4 | 5 | ## Advantages 6 | * Support for root_id (This will vastly increase speed for write operations on large data sets - this is because not the whole tree has to be rewritten when updating a node but only those rows with the same root id) 7 | * Support for level caching 8 | * Easier moving of nodes (MultiTree supports full move() to any id as opposed to Core Tree's moveUp and moveDown) 9 | * More getter functions (easily retrieve siblings, children, parents etc.) 10 | 11 | ## Caution 12 | Use __InnoDB__ (or a different engine that supports transactions, otherwise you have to LOCK tables manually during operations to prevent corrupted data in multi user environments) 13 | 14 | ## Configuration 15 | ### Example 1 16 | The following config is meant for large trees that are often updated as well a retrieved. It keeps track of a tree that has root_id's and level caching enabled. It is ideal for e.g. Comment Trees 17 | 18 | class Comment extends AppModel { 19 | var $name = 'Comment'; 20 | var $actsAs = array( 21 | 'MultiTree' => array( 22 | 'root' =>'root_id', 23 | 'level' =>'level' 24 | ) 25 | ); 26 | } 27 | 28 | #### Schema: 29 | 30 | CREATE TABLE `comments` ( 31 | `id` int(10) unsigned NOT NULL auto_increment, 32 | `title` varchar(128) NOT NULL default '', 33 | `body` text NOT NULL, 34 | `created` datetime default NULL, 35 | `modified` datetime default NULL, 36 | `parent_id` int(10) unsigned default NULL, 37 | `root_id` int(10) unsigned default NULL, 38 | `lft` mediumint(8) unsigned default NULL, 39 | `rght` mediumint(8) unsigned default NULL, 40 | `level` mediumint(8) unsigned default NULL, 41 | PRIMARY KEY (`id`), 42 | KEY `rght` USING BTREE (`root_id`,`rght`,`lft`), 43 | KEY `lft` USING BTREE (`root_id`,`lft`,`rght`), 44 | KEY `parent_id` USING BTREE (`parent_id`,`created`) 45 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 46 | 47 | ### Example 2 48 | This following config is meant for small trees that are mainly retrieved and not often updated. It keeps track of a tree without root_id's and level caching disabled. It is ideal for e.g. Category Trees 49 | _Note: This would also be the config for drop in's from the core Tree Behaviour_ 50 | 51 | class Category extends AppModel { 52 | var $name = 'Comment'; 53 | var $actsAs = array( 54 | 'MultiTree' => array( 55 | 'root' => false, 56 | 'level' => false 57 | ) 58 | ); 59 | } 60 | 61 | #### Schema: 62 | 63 | CREATE TABLE `categories` ( 64 | `id` int(10) unsigned NOT NULL auto_increment, 65 | `name` varchar(128) NOT NULL default '', 66 | `parent_id` int(10) unsigned default NULL, 67 | `lft` mediumint(6) unsigned default NULL, 68 | `rght` mediumint(6) unsigned default NULL, 69 | PRIMARY KEY (`id`), 70 | KEY `lft` USING BTREE (`lft`), 71 | KEY `parent_id` USING BTREE (`parent_id`), 72 | KEY `rght` USING BTREE (`rght`) 73 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 74 | 75 | 76 | ### Config defaults 77 | parent: parent_id 78 | left: lft 79 | right: rght 80 | root: root_id 81 | level: level 82 | 83 | 84 | ## Traversing the tree 85 | ### Get parent 86 | Get parent based on Parent 87 | debug($this->Category->getParent(32)); 88 | 89 | Get parent based on Left/Right values 90 | debug($this->Category->getParentFromTree(32)); 91 | 92 | ### Get path 93 | debug($this->Category->getPath(32)); 94 | 95 | ### Get level 96 | debug($this->Category->getLevel(32)); 97 | 98 | ### Get children 99 | debug($this->Category->getChildren(32)); 100 | 101 | Get direct children only: 102 | debug($this->Category->getChildren(32, true)); 103 | 104 | ### Get child count 105 | debug($this->Category->getChildCount(32)); 106 | 107 | ### Get siblings 108 | debug($this->Category->getSiblings(32)); 109 | debug($this->Category->getSiblings(32, true)); // Get siblings including the node itself 110 | 111 | ### Get previous siblings 112 | debug($this->Category->getPrevSiblings(32)); 113 | debug($this->Category->getPrevSiblings(32, true)); // Get previous siblings including the node itself 114 | 115 | ### Get next siblings 116 | debug($this->Category->getNextSiblings(32)); 117 | debug($this->Category->getNextSiblings(32, true)); // Get next siblings including the node itself 118 | 119 | ### Get previous sibling 120 | debug($this->Category->getPrevSibling(32)); 121 | 122 | ### Get next sibling 123 | debug($this->Category->getNextSibling(32)); 124 | 125 | ## Insert 126 | 127 | Insert new node as the last child of node 1 128 | $format = array( 129 | 'name' => 'Cat', 130 | 'parent_id' => 1 131 | ); 132 | $this->Category->save($format); 133 | 134 | Insert new node as the next sibling of node 4 135 | 136 | $format = array( 137 | 'name' => 'Lion', 138 | 'parent_id' => array('destination' => 4, 'position' => 'nextSibling') 139 | ); 140 | $this->Category->save($format); 141 | 142 | Not setting a parent_id or nulling it out will insert the node as a top level (root) node 143 | 144 | $format = array( 145 | 'name' => 'Animal', 146 | 'parent_id' => null 147 | ); 148 | $this->Category->save($format); 149 | 150 | 151 | ## Move 152 | 153 | $this->Category->move(6, 12, 'firstChild'); // Move node 6 to be the first child of node 12 154 | $this->Category->move(6, 12, 'lastChild'); // Move node 6 to be the last child of node 12 155 | $this->Category->move(6, 12, 'prevSibling'); // You get the idea.. 156 | $this->Category->move(6, 12, 'nextSibling'); 157 | 158 | Move node 9 up by 2 (if possible, otherwise move as high up as possible) 159 | 160 | $this->Category->moveUp(9, 2); 161 | 162 | Move node 9 down by 3 (if possible, otherwise move as low down as possible) 163 | 164 | $this->Category->moveDown(9, 3); 165 | 166 | Will make node 6 a new top level (root) node 167 | 168 | $this->Category->move(6, null); 169 | 170 | ## Delete 171 | $this->Category->delete(25); // Same as removeFromTree(25) 172 | 173 | This will delete node 25 and all its children 174 | 175 | $this->Category->removeFromTree(25); 176 | 177 | This will delete node 25 itself but if it has any children shift them one level up 178 | 179 | $this->Category->removeFromTree(25, false); 180 | 181 | ## Repair 182 | left and right values are broken but we have valid parent_id's 183 | 184 | $this->Category->repair('tree'); 185 | 186 | parent_id's are broken but we have valid left and right values 187 | 188 | $this->Category->repair('parent'); 189 | 190 | [1]: http://bakery.cakephp.org/articles/view/multitree-behavior 191 | [2]: http://book.cakephp.org/view/228/Basic-Usage -------------------------------------------------------------------------------- /models/behaviors/multi_tree.php: -------------------------------------------------------------------------------- 1 | 'parent_id', 29 | 'left' => 'lft', 30 | 'right' => 'rght', 31 | 'root' => 'root_id', // optional, allow multiple trees per table 32 | 'level' => 'level', // optional, cache levels 33 | 'dependent' => false, 34 | 'callbacks' => true, 35 | 36 | // Other 37 | 'recursive' => -1, 38 | 39 | // Private 40 | '__treeFields' => array(), 41 | '__move' => false, 42 | '__delete' => false 43 | ); 44 | 45 | /** 46 | * undocumented function 47 | * 48 | * @access public 49 | * @return void 50 | **/ 51 | function setup(&$Model, $config = array()) { 52 | // Merge config with defaults 53 | if ( !is_array($config) ) { 54 | $config = array(); 55 | } 56 | $this->settings[$Model->alias] = array_merge($this->_defaults, $config); 57 | // __treeFields 58 | if ( empty($this->settings[$Model->alias]['__treeFields']) ) { 59 | $this->settings[$Model->alias]['__treeFields'] = array( 60 | $this->settings[$Model->alias]['parent'], 61 | $this->settings[$Model->alias]['left'], 62 | $this->settings[$Model->alias]['right'] 63 | ); 64 | if ( !empty($this->settings[$Model->alias]['root']) ) { 65 | // if ( !$Model->hasField($this->settings[$Model->alias]['root']) ) 66 | $this->settings[$Model->alias]['__treeFields'][] = $this->settings[$Model->alias]['root']; 67 | } 68 | if ( !empty($this->settings[$Model->alias]['level']) ) { 69 | // if ( !$Model->hasField($this->settings[$Model->alias]['level']) ) 70 | $this->settings[$Model->alias]['__treeFields'][] = $this->settings[$Model->alias]['level']; 71 | } 72 | } 73 | } 74 | 75 | /** 76 | * undocumented function 77 | * 78 | * @access public 79 | * @return boolean 80 | **/ 81 | function beforeSave(&$Model) { 82 | extract($this->settings[$Model->alias]); 83 | 84 | // Are we about to create or edit? 85 | $creating = empty($Model->id); 86 | 87 | // Check if we need to perform changes to the tree 88 | if ( isset($Model->data[$Model->alias][$parent]) ) { 89 | // Get node 90 | if ( !$creating && ($node = $this->_node($Model, $Model->id)) === false ) { 91 | return false; 92 | } 93 | // Accept array with position information 94 | $position = 'lastChild'; 95 | if ( is_array($Model->data[$Model->alias][$parent]) ) { 96 | if ( array_key_exists('destination', $Model->data[$Model->alias][$parent]) && array_key_exists('position', $Model->data[$Model->alias][$parent]) ) { 97 | $position = $Model->data[$Model->alias][$parent]['position']; 98 | $Model->data[$Model->alias][$parent] = $Model->data[$Model->alias][$parent]['destination']; 99 | } else { 100 | $Model->data[$Model->alias][$parent] = reset($Model->data[$Model->alias][$parent]); 101 | } 102 | } 103 | // Any parent changes? 104 | if ( $creating || $Model->data[$Model->alias][$parent] != $node[$parent] ) { 105 | // Check if parent axists 106 | if ( !empty($Model->data[$Model->alias][$parent]) && ($destNode = $this->_node($Model, $Model->data[$Model->alias][$parent])) === false ) { 107 | $Model->invalidate($parent, 'Parent does not exist'); 108 | return false; 109 | } 110 | // Mark for moving 111 | $this->settings[$Model->alias]['__move'] = array('parent' => $Model->data[$Model->alias][$parent], 'position' => $position); 112 | } 113 | } else if ( !empty($root) && isset($Model->data[$Model->alias][$root]) ) { 114 | // Get node 115 | if ( !$creating && ($node = $this->_node($Model, $Model->id)) === false ) { 116 | return false; 117 | } 118 | // Any root changes? 119 | if ( $creating || $Model->data[$Model->alias][$root] != $node[$root] ) { 120 | // Mark for moving 121 | $this->settings[$Model->alias]['__move'] = array('root' => $Model->data[$Model->alias][$root]); 122 | } 123 | } else if ( $creating ) { 124 | $this->settings[$Model->alias]['__move'] = null; 125 | } 126 | 127 | // Don't allow manually changing left, right etc. 128 | $Model->data[$Model->alias] = array_diff_key($Model->data[$Model->alias], array_flip($__treeFields)); 129 | 130 | return true; 131 | } 132 | 133 | /** 134 | * undocumented function 135 | * 136 | * @access public 137 | * @return boolean 138 | **/ 139 | function afterSave(&$Model, $created) { 140 | if ( $this->settings[$Model->alias]['__move'] !== false ) { 141 | $this->move($Model, $Model->id, $this->settings[$Model->alias]['__move']); 142 | $this->settings[$Model->alias]['__move'] = false; 143 | } 144 | } 145 | 146 | /** 147 | * undocumented function 148 | * 149 | * @return void 150 | **/ 151 | function beforeDelete(&$Model, $cascade) { 152 | $this->settings[$Model->alias]['__delete'] = $this->_node($Model, $Model->id); 153 | return true; 154 | } 155 | 156 | /** 157 | * undocumented function 158 | * 159 | * @access public 160 | * @return boolean 161 | **/ 162 | function afterDelete(&$Model) { 163 | if ( $this->settings[$Model->alias]['__delete'] !== false ) { 164 | $this->_removeFromTree($Model, $this->settings[$Model->alias]['__delete'], $this->settings[$Model->alias]['dependent']); 165 | $this->settings[$Model->alias]['__delete'] = false; 166 | } 167 | } 168 | 169 | /** 170 | * undocumented function 171 | * 172 | * @access public 173 | * @return boolean 174 | **/ 175 | function move(&$Model, $id = null, $dest = null, $position = 'lastChild') { 176 | if (!$id && $Model->id) { 177 | $id = $Model->id; 178 | } 179 | extract($this->settings[$Model->alias]); 180 | if ( !is_array($dest) ) { 181 | $dest = array('parent' => $dest); 182 | } else if ( array_key_exists('position', $dest) ) { 183 | $position = $dest['position']; 184 | } 185 | 186 | // Get node 187 | if ( ($node = $this->_node($Model, $id)) === false ) { 188 | return false; 189 | } 190 | $oldNode = $node; 191 | $invalid = (empty($oldNode[$left]) || empty($oldNode[$right])); 192 | 193 | // Start transaction 194 | $Model->getDataSource()->begin($Model); 195 | 196 | // Fake loop allowing us to jump to the end on failure 197 | while ( $commit = true ) { 198 | 199 | // Get node size 200 | if ( $invalid ) { 201 | $node[$left] = 1; 202 | $node[$right] = 2; 203 | // $node[$parent] = null; 204 | // if ( !empty($root) ) 205 | // $node[$root] = null; 206 | } 207 | $treeSize = $node[$right]-$node[$left]+1; 208 | 209 | // Are we moving to another node? 210 | if ( !empty($dest['parent']) ) { 211 | // Get destination node 212 | if ( ($destNode = $this->_node($Model, $dest['parent'])) === false ) { 213 | // return false; 214 | $Model->invalidate($parent, 'Parent does not exist'); 215 | $commit = false; 216 | break; 217 | } 218 | // Do not allow to move a node to or inside itself 219 | if ( !$invalid && (empty($root) || $node[$root] == $destNode[$root]) && ($destNode[$left] >= $node[$left] && $destNode[$right] <= $node[$right]) ) { 220 | // return false; 221 | $Model->invalidate($parent, 'Destination node is within source tree'); 222 | $commit = false; 223 | break; 224 | } 225 | // Set beginning of shift range 226 | switch ( $position ) { 227 | case 'prevSibling': 228 | case 'before': 229 | $node[$parent] = $destNode[$parent]; 230 | if ( !empty($level) ) 231 | $node[$level] = $destNode[$level]; 232 | $start = $destNode[$left]; 233 | break; 234 | case 'nextSibling': 235 | case 'after': 236 | $node[$parent] = $destNode[$parent]; 237 | if ( !empty($level) ) 238 | $node[$level] = $destNode[$level]; 239 | $start = $destNode[$right]+1; 240 | break; 241 | case 'firstChild': 242 | $node[$parent] = $destNode[$Model->primaryKey]; // Same as $dest['parent'] 243 | if ( !empty($level) ) 244 | $node[$level] = $destNode[$level]+1; 245 | $start = $destNode[$left]+1; 246 | break; 247 | case 'lastChild': 248 | default: 249 | $node[$parent] = $destNode[$Model->primaryKey]; // Same as $dest['parent'] 250 | if ( !empty($level) ) 251 | $node[$level] = $destNode[$level]+1; 252 | $start = $destNode[$right]; 253 | } 254 | 255 | // Create gap for node in target tree 256 | if ( ($commit = $this->_shift($Model, $start, $treeSize, @$destNode[$root])) === false ) 257 | break; 258 | 259 | // Refresh node record (might have been affected by previous shift) 260 | // $node = $this->_node(&$Model, $id); // We can save us this query with the following: 261 | if ( ($affectedLeft = (!$invalid && (empty($root) || $node[$root] == $destNode[$root]) && $node[$left] >= $start)) !== false ) 262 | $node[$left] += $treeSize; 263 | if ( ($affectedRight = (!$invalid && (empty($root) || $node[$root] == $destNode[$root]) && $node[$right] >= $start)) !== false ) 264 | $node[$right] += $treeSize; 265 | 266 | if ( !empty($root) ) 267 | $node[$root] = $destNode[$root]; 268 | 269 | } else if ( empty($root) ) { 270 | // Move to the end of new tree 271 | $node[$parent] = null; 272 | if ( !empty($level) ) 273 | $node[$level] = 0; 274 | $start = $this->_max($Model, $right)+1; 275 | } else if ( !empty($dest['root']) ) { 276 | // Move to the end of tree 277 | $node[$root] = $dest['root']; 278 | $node[$parent] = null; 279 | if ( !empty($level) ) 280 | $node[$level] = 0; 281 | $start = $this->_max($Model, $right, array($Model->escapeField($root) => $dest['root']))+1; 282 | } else if ( isset($dest['root']) && !empty($node[$root]) ) { 283 | // Move to the end of tree 284 | // $node[$root] = $node[$root]; // I know.. 285 | $node[$parent] = null; 286 | if ( !empty($level) ) 287 | $node[$level] = 0; 288 | $start = $this->_max($Model, $right, array($Model->escapeField($root) => $node[$root]))+1; 289 | } else { 290 | // Move to the end of new tree 291 | $node[$root] = $id; 292 | $node[$parent] = null; 293 | if ( !empty($level) ) 294 | $node[$level] = 0; 295 | $start = 1; 296 | } 297 | 298 | if ( !$invalid && $treeSize > 2 ) { 299 | // Move node into that gap (Save new left, right, root and level) 300 | $diff = $start-$node[$left]; 301 | $levelDiff = !empty($level) ? $node[$level]-$oldNode[$level] : 0; 302 | if ( ($commit = $this->_shiftRange($Model, $node[$left], $node[$right], $diff, @$oldNode[$root], @$node[$root], @$levelDiff)) === false ) 303 | break; 304 | // Save new parent 305 | if ( ($commit = ($Model->save($node, array('callbacks' => false, 'validate' => false, 'fieldList' => array($parent))) !== false)) === false ) 306 | break; 307 | } else { 308 | // Move node into that gap (Save new left, right, root, parent and level) 309 | $diff = $start-$node[$left]; 310 | $data = $node; // Create new array, otherwise we affect range of shift() below 311 | $data[$left] += $diff; 312 | $data[$right] += $diff; 313 | if ( ($commit = ($Model->save($data, array('callbacks' => false, 'validate' => false, 'fieldList' => $__treeFields)) !== false)) === false ) 314 | break; 315 | } 316 | 317 | // Remove gap created while removing node from source tree 318 | if ( !$invalid ) { 319 | if ( ($commit = $this->_shift($Model, $node[$left], -$treeSize, @$oldNode[$root])) === false ) 320 | break; 321 | } 322 | 323 | // We don't want this to actually loop 324 | break; 325 | } 326 | 327 | // Commit 328 | if ( $commit ) { 329 | $Model->getDataSource()->commit($Model); 330 | } else { 331 | $Model->getDataSource()->rollback($Model); 332 | } 333 | return $commit; 334 | } 335 | 336 | /** 337 | * Reorder the node without changing the parent. 338 | * 339 | * If the node is the first child, or is a top level node with no previous node this method will return false 340 | * 341 | * @param AppModel $Model Model instance 342 | * @param integer $id The ID of the record to move 343 | * @param mixed $number how many places to move the node, or true to move to first position 344 | * @return boolean true on success, false on failure 345 | * @access public 346 | */ 347 | function moveUp(&$Model, $id = null, $number = 1) { 348 | if (!$id && $Model->id) { 349 | $id = $Model->id; 350 | } 351 | $prevSiblings = array_reverse($this->getPrevSiblings($Model, $id, false)); 352 | if ( empty($prevSiblings) ) 353 | return false; 354 | if ( count($prevSiblings) < $number ) 355 | $number = count($prevSiblings); 356 | return $this->move($Model, $id, $prevSiblings[$number-1][$Model->alias][$Model->primaryKey], 'prevSibling'); 357 | } 358 | 359 | /** 360 | * Reorder the node without changing the parent. 361 | * 362 | * If the node is the last child, or is a top level node with no subsequent node this method will return false 363 | * 364 | * @param AppModel $Model Model instance 365 | * @param integer $id The ID of the record to move 366 | * @param mixed $number how many places to move the node or true to move to last position 367 | * @return boolean true on success, false on failure 368 | * @access public 369 | */ 370 | function moveDown(&$Model, $id = null, $number = 1) { 371 | if (!$id && $Model->id) { 372 | $id = $Model->id; 373 | } 374 | $nextSiblings = $this->getNextSiblings($Model, $id, false); 375 | if ( empty($nextSiblings) ) 376 | return false; 377 | if ( count($nextSiblings) < $number ) 378 | $number = count($nextSiblings); 379 | return $this->move($Model, $id, $nextSiblings[$number-1][$Model->alias][$Model->primaryKey], 'nextSibling'); 380 | } 381 | 382 | /** 383 | * Remove the current node from the tree, and reparent all children up one level. 384 | * 385 | * If the parameter delete is false, the node will become a new top level node. Otherwise the node will be deleted 386 | * after the children are reparented. 387 | * 388 | * @param AppModel $Model Model instance 389 | * @param integer $id The ID of the record to remove 390 | * @param boolean $deleteChildren whether to delete the children while deleting the node (if any) 391 | * @return boolean true on success, false on failure 392 | * @access protected 393 | */ 394 | function _removeFromTree(&$Model, $id = null, $deleteChildren = false) { 395 | if (!$id && $Model->id) { 396 | $id = $Model->id; 397 | } 398 | extract($this->settings[$Model->alias]); 399 | 400 | // Get node (or use id as data) 401 | if ( is_array($id) ) { 402 | $node = $id; 403 | $id = $node[$Model->primaryKey]; 404 | } else { 405 | if ( ($node = $this->_node($Model, $id)) === false ) { 406 | return false; 407 | } 408 | } 409 | $invalid = (empty($node[$left]) || empty($node[$right])); 410 | if ( $invalid ) { 411 | // Delete invalid nodes just like that 412 | return $this->__delete($Model, $id); 413 | } 414 | 415 | // Get node size 416 | $treeSize = $node[$right]-$node[$left]+1; 417 | 418 | // Start transaction 419 | $Model->getDataSource()->begin($Model); 420 | 421 | // Fake loop allowing us to jump to the end on failure 422 | while ( $commit = true ) { 423 | // Either delete node and all its children - or - delete node and shift its children one level up 424 | if ( $deleteChildren ) { 425 | if ( $treeSize > 2 ) { 426 | // Delete node and all its children from tree 427 | if ( ($commit = $this->__deleteRange($Model, $node[$left], $node[$right], @$node[$root])) === false ) 428 | break; 429 | } else { 430 | // Delete node 431 | if ( ($commit = $this->__delete($Model, $id)) === false ) 432 | break; 433 | } 434 | // Remove gap created while removing node from tree 435 | if ( ($commit = $this->_shift($Model, $node[$left], -$treeSize, @$node[$root])) === false ) 436 | break; 437 | } else { 438 | // Delete node 439 | if ( ($commit = $this->__delete($Model, $id)) === false ) 440 | break; 441 | if ( $treeSize > 2 ) { 442 | // Set new parent of direct children 443 | $conditions = array($Model->escapeField($parent) => $id); 444 | if ( !empty($root) ) 445 | $conditions[$Model->escapeField($root)] = $node[$root]; 446 | if ( ($commit = $Model->updateAll(array($Model->escapeField($parent) => $node[$parent]), $conditions)) === false ) 447 | break; 448 | // Shift all children up 449 | if ( ($commit = $this->_shiftRange($Model, $node[$left], $node[$right], -1, @$node[$root], @$node[$root], -1)) === false ) 450 | break; 451 | } 452 | // Shift siblings 453 | if ( ($commit = $this->_shift($Model, $node[$right], -2, @$node[$root])) === false ) 454 | break; 455 | } 456 | 457 | // We don't want this to actually loop 458 | break; 459 | } 460 | 461 | // Commit 462 | if ( $commit ) { 463 | $Model->getDataSource()->commit($Model); 464 | } else { 465 | $Model->getDataSource()->rollback($Model); 466 | } 467 | return $commit; 468 | } 469 | 470 | /** 471 | * Get the child nodes of the current model 472 | * 473 | * If the direct parameter is set to true, only the direct children are returned (based upon the parent_id field (and root_id if set)) 474 | * If false is passed for the id parameter, top level, or all (depending on direct parameter appropriate) are counted. 475 | * 476 | * @param AppModel $Model Model instance 477 | * @param integer $id The ID of the record to read 478 | * @param boolean $direct whether to return only the direct, or all, children 479 | * @param boolean $includeNode Whether or not to include the current node 480 | * @param mixed $fields Either a single string of a field name, or an array of field names 481 | * @param string $order SQL ORDER BY conditions (e.g. "price DESC" or "name ASC") defaults to the tree order 482 | * @param integer $limit SQL LIMIT clause, for calculating items per page. 483 | * @param integer $recursive The number of levels deep to fetch associated records 484 | * @return array Array of child nodes 485 | * @access public 486 | */ 487 | function getChildren(&$Model, $id = null, $options = array()) { 488 | $options = Set::merge( 489 | array( 490 | 'direct' => false, 491 | 'includeNode' => false, 492 | 'fields' => null, 493 | 'order' => null, 494 | 'limit' => null, 495 | 'sort' => 'asc' 496 | ), 497 | $this->settings[$Model->alias], 498 | $options 499 | ); 500 | extract($options); 501 | if (!$id && $Model->id) { 502 | $id = $Model->id; 503 | } 504 | if (!$order) { 505 | $order = array($Model->escapeField($left) => $sort); 506 | } 507 | if ( $direct ) { 508 | $conditions = array($Model->escapeField($parent) => $id); 509 | if ( $includeNode ) { 510 | $conditions[$Model->escapeField()] = $id; 511 | $conditions = array('OR' => $conditions); 512 | } 513 | // Get node's direct children 514 | return $Model->find('all', array( 515 | 'fields' => $fields, 516 | 'conditions' => $conditions, 517 | 'order' => $order, 518 | 'limit' => $limit, 519 | 'recursive' => $recursive, 520 | )); 521 | } 522 | 523 | // Get node 524 | if ( ($node = $this->_node($Model, $id)) === false ) { 525 | return array(); 526 | } 527 | // Conditions 528 | if ( $includeNode ) { 529 | $conditions = array( 530 | $Model->escapeField($left).' >=' => $node[$left], 531 | $Model->escapeField($right).' <=' => $node[$right] 532 | ); 533 | } else { 534 | $conditions = array( 535 | $Model->escapeField($left).' >' => $node[$left], 536 | $Model->escapeField($right).' <' => $node[$right] 537 | ); 538 | } 539 | if ( !empty($root) ) 540 | $conditions[$Model->escapeField($root)] = $node[$root]; 541 | // Get node's children 542 | return $Model->find('all', array( 543 | 'fields' => $fields, 544 | 'conditions' => $conditions, 545 | 'order' => $order, 546 | 'limit' => $limit, 547 | 'recursive' => $recursive, 548 | )); 549 | } 550 | 551 | /** 552 | * Get the number of child nodes 553 | * 554 | * If the direct parameter is set to true, only the direct children are counted (based upon the parent_id field) 555 | * If false is passed for the id parameter, all top level nodes are counted, or all nodes are counted. 556 | * 557 | * @param AppModel $Model Model instance 558 | * @param integer $id The ID of the record to read 559 | * @param boolean $direct whether to count direct, or all, children 560 | * @return integer number of child nodes 561 | * @access public 562 | */ 563 | function getChildCount(&$Model, $id = null, $direct = false) { 564 | if (!$id && $Model->id) { 565 | $id = $Model->id; 566 | } 567 | extract($this->settings[$Model->alias]); 568 | 569 | if ( $direct ) { 570 | return $Model->find('count', array('conditions' => array($Model->escapeField($parent) => $id))); 571 | } else { 572 | // Use cached node if possible 573 | if ( isset($Model->data[$Model->alias][$left]) && isset($Model->data[$Model->alias][$right]) ) { 574 | $node = $Model->data[$Model->alias]; 575 | } else { 576 | // Get node 577 | if ( ($node = $this->_node($Model, $id)) === false ) { 578 | return false; 579 | } 580 | } 581 | return ($node[$right]-$node[$left]-1)/2; 582 | } 583 | } 584 | 585 | /** 586 | * undocumented function 587 | * 588 | * @access public 589 | * @return boolean 590 | **/ 591 | function getSiblings(&$Model, $id = null, $includeNode = false, $fields = null, $recursive = null) { 592 | $overrideRecursive = $recursive; 593 | if (!$id && $Model->id) { 594 | $id = $Model->id; 595 | } 596 | extract($this->settings[$Model->alias]); 597 | if (!is_null($overrideRecursive)) { 598 | $recursive = $overrideRecursive; 599 | } 600 | 601 | // Get node 602 | if ( ($node = $this->_node($Model, $id)) === false ) { 603 | return array(); 604 | } 605 | // Get node's siblings 606 | $conditions = array($Model->escapeField($parent) => $node[$parent]); 607 | if ( !$includeNode ) { 608 | $conditions[$Model->escapeField().' <>'] = $id; 609 | } 610 | return $Model->find('all', array( 611 | 'fields' => $fields, 612 | 'conditions' => $conditions, 613 | 'order' => array($Model->escapeField($left) => 'asc'), 614 | 'recursive' => $recursive, 615 | )); 616 | } 617 | 618 | /** 619 | * undocumented function 620 | * 621 | * @access public 622 | * @return boolean 623 | **/ 624 | function getNextSiblings(&$Model, $id = null, $includeNode = false, $fields = null, $recursive = null) { 625 | $overrideRecursive = $recursive; 626 | if (!$id && $Model->id) { 627 | $id = $Model->id; 628 | } 629 | extract($this->settings[$Model->alias]); 630 | if (!is_null($overrideRecursive)) { 631 | $recursive = $overrideRecursive; 632 | } 633 | 634 | // Get node 635 | if ( ($node = $this->_node($Model, $id)) === false ) { 636 | return array(); 637 | } 638 | // Get node's siblings 639 | $conditions = array($Model->escapeField($parent) => $node[$parent]); 640 | if ( $includeNode ) { 641 | $conditions[$Model->escapeField($left).' >='] = $node[$left]; 642 | } else { 643 | $conditions[$Model->escapeField($left).' >'] = $node[$left]; 644 | } 645 | return $Model->find('all', array( 646 | 'fields' => $fields, 647 | 'conditions' => $conditions, 648 | 'order' => array($Model->escapeField($left) => 'asc'), 649 | 'recursive' => $recursive, 650 | )); 651 | } 652 | 653 | /** 654 | * undocumented function 655 | * 656 | * @access public 657 | * @return boolean 658 | **/ 659 | function getPrevSiblings(&$Model, $id = null, $includeNode = false, $fields = null, $recursive = null) { 660 | $overrideRecursive = $recursive; 661 | if (!$id && $Model->id) { 662 | $id = $Model->id; 663 | } 664 | extract($this->settings[$Model->alias]); 665 | if (!is_null($overrideRecursive)) { 666 | $recursive = $overrideRecursive; 667 | } 668 | 669 | // Get node 670 | if ( ($node = $this->_node($Model, $id)) === false ) { 671 | return array(); 672 | } 673 | // Get node's siblings 674 | $conditions = array($Model->escapeField($parent) => $node[$parent]); 675 | if ( $includeNode ) { 676 | $conditions[$Model->escapeField($left).' <='] = $node[$left]; 677 | } else { 678 | $conditions[$Model->escapeField($left).' <'] = $node[$left]; 679 | } 680 | return $Model->find('all', array( 681 | 'fields' => $fields, 682 | 'conditions' => $conditions, 683 | 'order' => array($Model->escapeField($left) => 'asc'), 684 | 'recursive' => $recursive, 685 | )); 686 | } 687 | 688 | /** 689 | * undocumented function 690 | * 691 | * @access public 692 | * @return boolean 693 | **/ 694 | function getNextSibling(&$Model, $id = null, $fields = null, $recursive = null) { 695 | $overrideRecursive = $recursive; 696 | if (!$id && $Model->id) { 697 | $id = $Model->id; 698 | } 699 | extract($this->settings[$Model->alias]); 700 | if (!is_null($overrideRecursive)) { 701 | $recursive = $overrideRecursive; 702 | } 703 | 704 | // Get node 705 | if ( ($node = $this->_node($Model, $id)) === false ) { 706 | return false; 707 | } 708 | // Conditions 709 | $conditions = array( 710 | // $Model->escapeField($root) => $node[$root], 711 | $Model->escapeField($left) => $node[$right]+1 712 | ); 713 | if ( !empty($root) ) 714 | $conditions[$Model->escapeField($root)] = $node[$root]; 715 | // Get node's parent 716 | return $Model->find('first', array( 717 | 'fields' => $fields, 718 | 'conditions' => $conditions, 719 | 'recursive' => $recursive, 720 | )); 721 | } 722 | 723 | /** 724 | * undocumented function 725 | * 726 | * @access public 727 | * @return boolean 728 | **/ 729 | function getPrevSibling(&$Model, $id = null, $fields = null, $recursive = null) { 730 | $overrideRecursive = $recursive; 731 | if (!$id && $Model->id) { 732 | $id = $Model->id; 733 | } 734 | extract($this->settings[$Model->alias]); 735 | if (!is_null($overrideRecursive)) { 736 | $recursive = $overrideRecursive; 737 | } 738 | 739 | // Get node 740 | if ( ($node = $this->_node($Model, $id)) === false ) { 741 | return false; 742 | } 743 | // Conditions 744 | $conditions = array( 745 | // $Model->escapeField($root) => $node[$root], 746 | $Model->escapeField($right) => $node[$left]-1 747 | ); 748 | if ( !empty($root) ) 749 | $conditions[$Model->escapeField($root)] = $node[$root]; 750 | // Get node's parent 751 | return $Model->find('first', array( 752 | 'fields' => $fields, 753 | 'conditions' => $conditions, 754 | 'recursive' => $recursive, 755 | )); 756 | } 757 | 758 | /** 759 | * Get the parent node 760 | * 761 | * reads the parent id and returns this node 762 | * 763 | * @param AppModel $Model Model instance 764 | * @param integer $id The ID of the record to read 765 | * @param integer $recursive The number of levels deep to fetch associated records 766 | * @return array Array of data for the parent node 767 | * @access public 768 | */ 769 | function getParent(&$Model, $id = null, $fields = null, $recursive = null) { 770 | $overrideRecursive = $recursive; 771 | if (!$id && $Model->id) { 772 | $id = $Model->id; 773 | } 774 | extract($this->settings[$Model->alias]); 775 | if (!is_null($overrideRecursive)) { 776 | $recursive = $overrideRecursive; 777 | } 778 | 779 | // Get node 780 | if ( ($node = $this->_node($Model, $id)) === false ) { 781 | return false; 782 | } 783 | // Get node's parent 784 | return $Model->find('first', array( 785 | 'fields' => $fields, 786 | 'conditions' => array($Model->escapeField() => $node[$parent]), 787 | 'recursive' => $recursive, 788 | )); 789 | } 790 | 791 | /** 792 | * Get the parent node 793 | * 794 | * reads the parent id and returns this node in the current tree defined by root_id (if set) 795 | * 796 | * @param AppModel $Model Model instance 797 | * @param integer $id The ID of the record to read 798 | * @param integer $recursive The number of levels deep to fetch associated records 799 | * @return array Array of data for the parent node 800 | * @access public 801 | */ 802 | function getParentFromTree(&$Model, $id = null, $fields = null, $recursive = null) { 803 | $overrideRecursive = $recursive; 804 | if (!$id && $Model->id) { 805 | $id = $Model->id; 806 | } 807 | extract($this->settings[$Model->alias]); 808 | if (!is_null($overrideRecursive)) { 809 | $recursive = $overrideRecursive; 810 | } 811 | 812 | // Get node 813 | if ( ($node = $this->_node($Model, $id)) === false ) { 814 | return false; 815 | } 816 | // Conditions 817 | $conditions = array( 818 | $Model->escapeField($left).' <' => $node[$left], 819 | $Model->escapeField($right).' >' => $node[$right], 820 | ); 821 | if ( !empty($root) ) 822 | $conditions[$Model->escapeField($root)] = $node[$root]; 823 | // Get path to node 824 | return $Model->find('first', array( 825 | 'fields' => $fields, 826 | 'conditions' => $conditions, 827 | 'order' => array($Model->escapeField($left) => 'desc'), 828 | 'recursive' => $recursive, 829 | )); 830 | } 831 | 832 | /** 833 | * Get the root to the given node 834 | * 835 | * @param AppModel $Model Model instance 836 | * @param integer $id The ID of the record to read 837 | * @param mixed $fields Either a single string of a field name, or an array of field names 838 | * @param integer $recursive The number of levels deep to fetch associated records 839 | * @return array Top most parent node 840 | * @access public 841 | */ 842 | function getRoot(&$Model, $id = null, $fields = null, $recursive = null) { 843 | $overrideRecursive = $recursive; 844 | if (!$id && $Model->id) { 845 | $id = $Model->id; 846 | } 847 | extract($this->settings[$Model->alias]); 848 | if (!is_null($overrideRecursive)) { 849 | $recursive = $overrideRecursive; 850 | } 851 | 852 | // Get node 853 | if ( ($node = $this->_node($Model, $id)) === false ) { 854 | return array(); 855 | } 856 | // Conditions 857 | $conditions = array( 858 | // $Model->escapeField($root) => $node[$root], 859 | $Model->escapeField($left).' <=' => $node[$left], 860 | $Model->escapeField($right).' >=' => $node[$right], 861 | $Model->escapeField('parent_id') => NULL 862 | ); 863 | // Get path to node 864 | return $Model->find('first', array( 865 | 'fields' => $fields, 866 | 'conditions' => $conditions, 867 | 'order' => array($Model->escapeField($left) => 'asc'), 868 | 'recursive' => $recursive, 869 | )); 870 | } 871 | 872 | /** 873 | * Get the path to the given node 874 | * 875 | * @param AppModel $Model Model instance 876 | * @param integer $id The ID of the record to read 877 | * @param mixed $fields Either a single string of a field name, or an array of field names 878 | * @param integer $recursive The number of levels deep to fetch associated records 879 | * @return array Array of nodes from top most parent to current node 880 | * @access public 881 | */ 882 | function getPath(&$Model, $id = null, $fields = null, $recursive = null) { 883 | $overrideRecursive = $recursive; 884 | if (!$id && $Model->id) { 885 | $id = $Model->id; 886 | } 887 | extract($this->settings[$Model->alias]); 888 | if (!is_null($overrideRecursive)) { 889 | $recursive = $overrideRecursive; 890 | } 891 | 892 | // Get node 893 | if ( ($node = $this->_node($Model, $id)) === false ) { 894 | return array(); 895 | } 896 | // Conditions 897 | $conditions = array( 898 | // $Model->escapeField($root) => $node[$root], 899 | $Model->escapeField($left).' <=' => $node[$left], 900 | $Model->escapeField($right).' >=' => $node[$right], 901 | ); 902 | if ( !empty($root) ) 903 | $conditions[$Model->escapeField($root)] = $node[$root]; 904 | // Get path to node 905 | return $Model->find('all', array( 906 | 'fields' => $fields, 907 | 'conditions' => $conditions, 908 | 'order' => array($Model->escapeField($left) => 'asc'), 909 | 'recursive' => $recursive, 910 | )); 911 | } 912 | 913 | /** 914 | * undocumented function 915 | * 916 | * @access public 917 | * @return boolean 918 | **/ 919 | function getLevel(&$Model, $id = null) { 920 | if (!$id && $Model->id) { 921 | $id = $Model->id; 922 | } 923 | extract($this->settings[$Model->alias]); 924 | 925 | // Get node 926 | if ( ($node = $this->_node($Model, $id)) === false ) { 927 | return false; 928 | } 929 | // if ( !empty($level) ) 930 | // return $node[$level]; 931 | // Conditions 932 | $conditions = array( 933 | $Model->escapeField($left).' <' => $node[$left], 934 | $Model->escapeField($right).' >' => $node[$right], 935 | ); 936 | if ( !empty($root) ) 937 | $conditions[$Model->escapeField($root)] = $node[$root]; 938 | return $Model->find('count', array('conditions' => $conditions)); 939 | } 940 | 941 | /** 942 | * A convenience method for returning a hierarchical array used for HTML select boxes 943 | * 944 | * @param AppModel $Model Model instance 945 | * @param mixed $conditions SQL conditions as a string or as an array('field' =>'value',...) 946 | * @param string $keyPath A string path to the key, i.e. "{n}.Post.id" 947 | * @param string $valuePath A string path to the value, i.e. "{n}.Post.title" 948 | * @param string $spacer The character or characters which will be repeated 949 | * @param integer $recursive The number of levels deep to fetch associated records 950 | * @return array An associative array of records, where the id is the key, and the display field is the value 951 | * @access public 952 | */ 953 | function generateTreeList(&$Model, $conditions = null, $keyPath = null, $valuePath = null, $spacer = '_', $recursive = null) { 954 | $overrideRecursive = $recursive; 955 | extract($this->settings[$Model->alias]); 956 | if (!is_null($overrideRecursive)) { 957 | $recursive = $overrideRecursive; 958 | } 959 | 960 | if ( is_numeric($conditions) ) { 961 | $results = $this->getChildren($Model, $conditions); 962 | } else { 963 | $order = array(); 964 | if ( !empty($root) ) 965 | $order[$Model->escapeField($root)] = 'asc'; 966 | $order[$Model->escapeField($left)] = 'asc'; 967 | $results = $Model->find('all', array( 968 | 'conditions' => $conditions, 969 | 'order' => $order, 970 | 'recursive' => $recursive, 971 | )); 972 | } 973 | if ( empty($results) ) { 974 | return array(); 975 | } 976 | 977 | if ($keyPath == null && $valuePath == null && $Model->hasField($Model->displayField)) { 978 | $fields = array($Model->primaryKey, $Model->displayField, $root, $left, $right); 979 | } else { 980 | $fields = null; 981 | } 982 | if ($keyPath == null) { 983 | $keyPath = '{n}.'.$Model->alias.'.'.$Model->primaryKey; 984 | } 985 | if ($valuePath == null) { 986 | $valuePath = array('{0}{1}', '{n}.tree_prefix', '{n}.'.$Model->alias.'.'.$Model->displayField); 987 | } else if (is_string($valuePath)) { 988 | $valuePath = array('{0}{1}', '{n}.tree_prefix', $valuePath); 989 | } else { 990 | $valuePath[0] = '{'.(count($valuePath)-1).'}'.$valuePath[0]; 991 | $valuePath[] = '{n}.tree_prefix'; 992 | } 993 | 994 | if ( !empty($level) ) { 995 | foreach ( $results as $i => $result ) { 996 | $results[$i]['tree_prefix'] = str_repeat($spacer, $result[$Model->alias][$level]); 997 | } 998 | } else { 999 | foreach ($results as $i => $result) { 1000 | $stack_key = @$result[$Model->alias][$root]; 1001 | if ( !@array_key_exists($stack_key, $stack) ) 1002 | $stack[$stack_key] = array(); 1003 | while ($stack[$stack_key] && ($stack[$stack_key][count($stack[$stack_key])-1] < $result[$Model->alias][$right])) { 1004 | array_pop($stack[$stack_key]); 1005 | } 1006 | $results[$i]['tree_prefix'] = str_repeat($spacer,count($stack[$stack_key])); 1007 | $stack[$stack_key][] = $result[$Model->alias][$right]; 1008 | } 1009 | } 1010 | return Set::combine($results, $keyPath, $valuePath); 1011 | 1012 | } 1013 | 1014 | /** 1015 | * Repair a corrupted tree 1016 | * 1017 | * The broken parameter is used to specify the source of info that is invalid/incorrect. The source of data 1018 | * will be populated based upon the opposite source of info. E.g. if the MPTT fields are corrupt or empty, with the $broken 1019 | * 'tree' the values of the parent_id field will be used to populate the left and right fields. 1020 | * 1021 | * @param AppModel $Model Model instance 1022 | * @param string $broken parent or tree 1023 | * @return boolean true on success, false on failure 1024 | * @access public 1025 | */ 1026 | function repair(&$Model, $broken = 'tree') { 1027 | extract($this->settings[$Model->alias]); 1028 | $Model->recursive = $recursive; 1029 | 1030 | switch ( $broken ) { 1031 | case 'parent': 1032 | // Find and set parent of each node using tree structure 1033 | $nodes = $Model->find('all', array( 1034 | 'fields' => array_merge(array($Model->primaryKey), $__treeFields), 1035 | )); 1036 | foreach ( $nodes as $node ) { 1037 | $id = $node[$Model->alias][$Model->primaryKey]; 1038 | if ( ($parentNode = $this->getParentFromTree($Model, $id)) !== false ) { 1039 | $node[$Model->alias][$parent] = $parentNode[$Model->alias][$Model->primaryKey]; 1040 | } else { 1041 | $node[$Model->alias][$parent] = null; 1042 | } 1043 | $Model->save($node, array('callbacks' => false, 'validate' => false, 'fieldList' => array($parent))); 1044 | } 1045 | break; 1046 | 1047 | case 'tree': 1048 | // Null out all tree values except for parent 1049 | $data = array_fill_keys(array_diff($__treeFields, array($parent)), null); // PHP5.2 1050 | $Model->updateAll($data); 1051 | // Move nodes back into tree structure, one after the other 1052 | $nodes = $Model->find('all', array( 1053 | 'fields' => array_merge(array($Model->primaryKey), $__treeFields), 1054 | 'order' => array( 1055 | "$Model->alias.$parent" => 'asc', 1056 | "$Model->alias.$Model->primaryKey" => 'asc', 1057 | ) 1058 | )); 1059 | foreach ( $nodes as $node ) { 1060 | $node = reset($node); 1061 | $this->move($Model, $node[$Model->primaryKey], $node[$parent], 'lastChild'); 1062 | } 1063 | break; 1064 | } 1065 | } 1066 | 1067 | /** 1068 | * undocumented function 1069 | * 1070 | * @access protected 1071 | * @return void 1072 | **/ 1073 | function _node(&$Model, $id) { 1074 | extract($this->settings[$Model->alias]); 1075 | if ( ($node = $Model->find('first', array( 1076 | 'fields' => array_merge(array($Model->primaryKey), $__treeFields), 1077 | 'conditions' => array($Model->escapeField() => $id), 1078 | 'recursive' => $recursive 1079 | ))) === false ) { 1080 | return false; 1081 | } 1082 | return reset($node); 1083 | } 1084 | 1085 | /** 1086 | * undocumented function 1087 | * 1088 | * @access protected 1089 | * @return void 1090 | **/ 1091 | function _max(&$Model, $field, $conditions = null) { 1092 | $max = $Model->find('all', array( 1093 | 'fields' => $Model->getDataSource()->calculate($Model, 'max', array($Model->escapeField($field), $field)), 1094 | 'conditions' => $conditions, 1095 | 'recursive' => -1 1096 | )); 1097 | return (int)(reset(reset(reset($max)))); 1098 | } 1099 | 1100 | /** 1101 | * undocumented function 1102 | * 1103 | * @access protected 1104 | * @return void 1105 | **/ 1106 | function _min(&$Model, $field, $conditions = null) { 1107 | $max = $Model->find('all', array( 1108 | 'fields' => $Model->getDataSource()->calculate($Model, 'min', array($Model->escapeField($field), $field)), 1109 | 'conditions' => $conditions, 1110 | 'recursive' => -1 1111 | )); 1112 | return (int)(reset(reset(reset($max)))); 1113 | } 1114 | 1115 | /** 1116 | * undocumented function 1117 | * 1118 | * @access protected 1119 | * @return void 1120 | **/ 1121 | function _shift(&$Model, $first, $delta, $rootId = 1) { 1122 | extract($this->settings[$Model->alias]); 1123 | 1124 | $sign = ($delta >= 0) ? ' + ' : ' - '; 1125 | $delta = abs($delta); 1126 | 1127 | // Shift (left) 1128 | $data = array($Model->escapeField($left) => $Model->escapeField($left).$sign.$delta); 1129 | $conditions = array( 1130 | // $Model->escapeField($root) => $rootId, 1131 | $Model->escapeField($left).' >=' => $first, 1132 | ); 1133 | if ( !empty($root) ) 1134 | $conditions[$Model->escapeField($root)] = $rootId; 1135 | 1136 | $limit = 1; 1137 | if ( ($Model->find('count', compact('conditions', 'limit')) > 0) && $Model->updateAll($data, $conditions) === false ) 1138 | return false; 1139 | 1140 | // Shift (right) 1141 | $data = array($Model->escapeField($right) => $Model->escapeField($right).$sign.$delta); 1142 | $conditions = array( 1143 | // $Model->escapeField($root) => $rootId, 1144 | $Model->escapeField($right).' >=' => $first, 1145 | ); 1146 | if ( !empty($root) ) 1147 | $conditions[$Model->escapeField($root)] = $rootId; 1148 | if ( $Model->updateAll($data, $conditions) === false ) 1149 | return false; 1150 | 1151 | return true; 1152 | } 1153 | 1154 | /** 1155 | * undocumented function 1156 | * 1157 | * @access protected 1158 | * @return void 1159 | **/ 1160 | function _shiftRange(&$Model, $first, $last = 0, $delta, $rootId = 1, $destRootId = 1, $levelDelta = 0) { 1161 | extract($this->settings[$Model->alias]); 1162 | 1163 | $sign = ($delta >= 0) ? ' + ' : ' - '; 1164 | $delta = abs($delta); 1165 | $levelSign = ($levelDelta >= 0) ? ' + ' : ' - '; 1166 | $levelDelta = abs($levelDelta); 1167 | 1168 | // Data 1169 | $data = array( 1170 | // $Model->escapeField($root) => $destRootId, 1171 | $Model->escapeField($left) => $Model->escapeField($left).$sign.$delta, 1172 | $Model->escapeField($right) => $Model->escapeField($right).$sign.$delta 1173 | ); 1174 | if ( !empty($root) ) 1175 | $data[$Model->escapeField($root)] = $destRootId; 1176 | if ( !empty($level) ) 1177 | $data[$Model->escapeField($level)] = $Model->escapeField($level).$levelSign.$levelDelta; 1178 | 1179 | // Conditions 1180 | $conditions = array( 1181 | // $Model->escapeField($root) => $rootId, 1182 | $Model->escapeField($left).' >=' => $first, 1183 | $Model->escapeField($right).' <=' => $last, 1184 | ); 1185 | if ( !empty($root) ) 1186 | $conditions[$Model->escapeField($root)] = $rootId; 1187 | return $Model->updateAll($data, $conditions); 1188 | } 1189 | 1190 | /** 1191 | * undocumented function 1192 | * 1193 | * @access private 1194 | * @return void 1195 | **/ 1196 | function __delete(&$Model, $id) { 1197 | return $Model->deleteAll(array( 1198 | $Model->escapeField() => $id 1199 | ), true, $this->settings[$Model->alias]['callbacks']); 1200 | } 1201 | 1202 | /** 1203 | * undocumented function 1204 | * 1205 | * @access private 1206 | * @return void 1207 | **/ 1208 | function __deleteRange(&$Model, $first, $last, $rootId = 1) { 1209 | extract($this->settings[$Model->alias]); 1210 | 1211 | $conditions = array( 1212 | // $Model->escapeField($root) => $rootId, 1213 | $Model->escapeField($left).' >=' => $first, 1214 | $Model->escapeField($right).' <=' => $last 1215 | ); 1216 | if ( !empty($root) ) 1217 | $conditions[$Model->escapeField($root)] = $rootId; 1218 | return $Model->deleteAll($conditions, true, $callbacks); 1219 | } 1220 | 1221 | /** 1222 | * Check if the current tree is valid. 1223 | * 1224 | * Returns true if the tree is valid otherwise an array of (type, incorrect left/right index, message) 1225 | * 1226 | * @param AppModel $Model Model instance 1227 | * @return mixed true if the tree is valid or empty, otherwise an array of erroneous trees each containing the list of detected 1228 | * errors (error type [index, node], [incorrect left/right index,node id], message) 1229 | * @access public 1230 | * @link http://book.cakephp.org/view/1630/Verify 1231 | */ 1232 | function verify(&$Model) { 1233 | 1234 | // check if the model has a dedicated root_id field 1235 | if(empty($this->settings[$Model->alias]['root'])) { 1236 | die('The verify method is only supported on trees with a specified root node column. See \'root\' configuration property of MultiTree Behavior.'); 1237 | } 1238 | 1239 | extract($this->settings[$Model->alias]); 1240 | $rootNodes = $Model->find('list', array('conditions' => array( $Model->escapeField($parent) => null))); 1241 | 1242 | $errors = array(); 1243 | $errorDetected = false; 1244 | foreach(array_keys($rootNodes) as $rootNodeId) { 1245 | $key = "treeId-" . $rootNodeId; 1246 | $errors[$key] = $this->_verifyTree($Model, $rootNodeId); 1247 | if($errors[$key] !== true) { 1248 | $errorDetected = true; 1249 | } 1250 | } 1251 | 1252 | if(!$errorDetected) { 1253 | return true; 1254 | } 1255 | 1256 | return $errors; 1257 | } 1258 | 1259 | /** 1260 | * 1261 | * Verifies a single tree starting with it's root node. 1262 | * 1263 | * @param AppModel $Model Model instance 1264 | * @param int $rootNodeId 1265 | * @return string mixed true if the tree is valid or empty, otherwise an array of (error type [index, node], 1266 | * [incorrect left/right index,node id], message) 1267 | * @access protected 1268 | */ 1269 | function _verifyTree(&$Model, $rootNodeId) { 1270 | extract($this->settings[$Model->alias]); 1271 | $scope = array($Model->escapeField($root) => $rootNodeId); 1272 | if (!$Model->find('count', array('conditions' => $scope))) { 1273 | return true; 1274 | } 1275 | 1276 | $min = $this->_min($Model, $left, $scope); 1277 | $edge = $this->_max($Model, $right, $scope); 1278 | $errors = array(); 1279 | 1280 | for ($i = $min; $i <= $edge; $i++) { 1281 | $count = $Model->find('count', array('conditions' => array( 1282 | $scope, 'OR' => array($Model->escapeField($left) => $i, $Model->escapeField($right) => $i) 1283 | ))); 1284 | 1285 | if ($count != 1) { 1286 | if ($count == 0) { 1287 | $errors[] = array('index', $i, 'missing'); 1288 | } else { 1289 | $errors[] = array('index', $i, 'duplicate'); 1290 | } 1291 | } 1292 | } 1293 | $node = $Model->find('first', array('conditions' => array($scope, $Model->escapeField($right) . '< ' . $Model->escapeField($left)), 'recursive' => 0)); 1294 | if ($node) { 1295 | $errors[] = array('node', $node[$Model->alias][$Model->primaryKey], 'left greater than right.'); 1296 | } 1297 | 1298 | $Model->bindModel(array('belongsTo' => array('VerifyParent' => array( 1299 | 'className' => $Model->alias, 1300 | 'foreignKey' => $parent, 1301 | 'fields' => array($Model->primaryKey, $left, $right, $parent) 1302 | )))); 1303 | 1304 | foreach ($Model->find('all', array('conditions' => $scope, 'recursive' => 0)) as $instance) { 1305 | if (is_null($instance[$Model->alias][$left]) || is_null($instance[$Model->alias][$right])) { 1306 | $errors[] = array('node', $instance[$Model->alias][$Model->primaryKey], 1307 | 'has invalid left or right values'); 1308 | } elseif ($instance[$Model->alias][$left] == $instance[$Model->alias][$right]) { 1309 | $errors[] = array('node', $instance[$Model->alias][$Model->primaryKey], 1310 | 'left and right values identical'); 1311 | } elseif ($instance[$Model->alias][$parent]) { 1312 | if (!$instance['VerifyParent'][$Model->primaryKey]) { 1313 | $errors[] = array('node', $instance[$Model->alias][$Model->primaryKey], 1314 | 'The parent node ' . $instance[$Model->alias][$parent] . ' doesn\'t exist'); 1315 | } elseif ($instance[$Model->alias][$left] < $instance['VerifyParent'][$left]) { 1316 | $errors[] = array('node', $instance[$Model->alias][$Model->primaryKey], 1317 | 'left less than parent (node ' . $instance['VerifyParent'][$Model->primaryKey] . ').'); 1318 | } elseif ($instance[$Model->alias][$right] > $instance['VerifyParent'][$right]) { 1319 | $errors[] = array('node', $instance[$Model->alias][$Model->primaryKey], 1320 | 'right greater than parent (node ' . $instance['VerifyParent'][$Model->primaryKey] . ').'); 1321 | } 1322 | } elseif ($Model->find('count', array('conditions' => array($scope, $Model->escapeField($left) . ' <' => $instance[$Model->alias][$left], $Model->escapeField($right) . ' >' => $instance[$Model->alias][$right]), 'recursive' => 0))) { 1323 | $errors[] = array('node', $instance[$Model->alias][$Model->primaryKey], 'The parent field is blank, but has a parent'); 1324 | } 1325 | } 1326 | if ($errors) { 1327 | return $errors; 1328 | } 1329 | return true; 1330 | } 1331 | } 1332 | ?> 1333 | --------------------------------------------------------------------------------