├── Nested_set.php └── README.md /Nested_set.php: -------------------------------------------------------------------------------- 1 | , intel352 16 | * @copyright Copyright (c) 2007 Thunder; Copyright (c) 2008 intel352 17 | */ 18 | 19 | 20 | /** 21 | * @package Nested_set 22 | * @author Thunder , intel352 , olimortimer 23 | * @version 1.2.1 24 | * @copyright Copyright (c) 2007 Thunder; Copyright (c) 2008 intel352 25 | * @todo Keep semi-persistent model of data retrieved, to reduce query access of the same data. 26 | * Need to convert this to a generic TREE class, supporting nested and adjacent, a la cakephp 27 | */ 28 | class Nested_set { 29 | 30 | private $table_name; 31 | private $left_column_name; 32 | private $right_column_name; 33 | private $primary_key_column_name; 34 | private $parent_column_name; 35 | private $text_column_name; 36 | private $db; 37 | 38 | /** 39 | * Constructor 40 | * 41 | * @access public 42 | */ 43 | public function __construct() { 44 | $CI =& get_instance(); // to access CI resources, use $CI instead of $this 45 | $this->db =& $CI->db; 46 | } 47 | 48 | // ------------------------------------------------------------------------- 49 | // OBJECT INITIALISATION METHODS 50 | // 51 | // For setting instance properties 52 | // 53 | // ------------------------------------------------------------------------- 54 | 55 | /** 56 | * On initialising the instance, this method should be called to set the 57 | * database table name that we're dealing and also to identify the names 58 | * of the left and right value columns used to form the tree structure. 59 | * Typically, this would be done automatically by the model class that 60 | * extends this "base" class (eg. a Categories class would set the table_name 61 | * to "categories", a Site_structure class would set the table_name to 62 | * "pages" etc) 63 | * 64 | * @param string $table_name The name of the db table to use 65 | * @param string $left_column_name The name of the field representing the left identifier 66 | * @param string $right_column_name The name of the field representing the right identifier 67 | * @param string $primary_key_column_name The name of the primary identifier field 68 | * @param string $parent_column_name The name of the parent column field 69 | */ 70 | public function setControlParams($table_name, $left_column_name = 'lft', $right_column_name = 'rgt', $primary_key_column_name = 'id', $parent_column_name = 'parent_id', $text_column_name = 'name') { 71 | $this->table_name = $table_name; 72 | $this->left_column_name = $left_column_name; 73 | $this->right_column_name = $right_column_name; 74 | $this->primary_key_column_name = $primary_key_column_name; 75 | $this->parent_column_name = $parent_column_name; 76 | $this->text_column_name = $text_column_name; 77 | } 78 | 79 | /** 80 | * Used to identify the primary key of the table in use. Commonly, this will 81 | * be an auto_incrementing ID column (eg CategoryId) 82 | * 83 | * @param string $primary_key_name 84 | */ 85 | public function setPrimaryKeyColumn($primary_key_name) { 86 | $this->primary_key_column_name = $primary_key_name; 87 | } 88 | 89 | 90 | // ------------------------------------------------------------------------- 91 | // NODE MANIPULATION FUNCTIONS 92 | // 93 | // Methods to add/remove nodes in your tree 94 | // 95 | // ------------------------------------------------------------------------- 96 | 97 | 98 | /** 99 | * Adds the first entry to the table 100 | * @param $extrafields An array of field->value pairs for the database record 101 | * @return $node an array of left and right values 102 | * @deprecated 103 | */ 104 | public function initialiseRoot($extrafields = array()) { 105 | return $this->insertNewTree($extrafields); 106 | } 107 | 108 | /** 109 | * Adds the first entry to the table 110 | * @param $extrafields An array of field->value pairs for the database record 111 | * @return $node an array of left and right values 112 | */ 113 | public function insertNewTree($extrafields = array()) { 114 | 115 | $this->db->select_max($this->right_column_name, 'lft'); 116 | $query = $this->db->get($this->table_name); 117 | $result = $query->row_array(); 118 | 119 | $node = array( 120 | $this->parent_column_name => 0, 121 | $this->left_column_name => $result['lft'] + 1, 122 | $this->right_column_name => $result['lft'] + 2, 123 | ); 124 | 125 | $this->_setNewNode($node, $extrafields); 126 | 127 | return $this->getNodeWhereLeft($node[$this->left_column_name]); 128 | } 129 | 130 | /** 131 | * inserts a new node as the first child of the supplied parent node 132 | * @param array $parentNode The node array of the parent to use 133 | * @param array $extrafields An associative array of fieldname=>value for the other fields in the recordset 134 | * @return array $childNode An associative array representing the new node 135 | */ 136 | public function insertNewChild($parentNode, $extrafields = array()) { 137 | $childNode[$this->parent_column_name] = $parentNode[$this->primary_key_column_name]; 138 | $childNode[$this->left_column_name] = $parentNode[$this->left_column_name]+1; 139 | $childNode[$this->right_column_name] = $parentNode[$this->left_column_name]+2; 140 | 141 | $this->_modifyNode($childNode[$this->left_column_name], 2); 142 | $this->_setNewNode($childNode, $extrafields); 143 | 144 | return $this->getNodeWhereLeft($childNode[$this->left_column_name]); 145 | } 146 | 147 | /** 148 | * Same as insertNewChild except the new node is added as the last child 149 | * @param array $parentNode The node array of the parent to use 150 | * @param array $extrafields An associative array of fieldname=>value for the other fields in the recordset 151 | * @return array $childNode An associative array representing the new node 152 | */ 153 | public function appendNewChild($parentNode, $extrafields = array()) { 154 | $childNode[$this->parent_column_name] = $parentNode[$this->primary_key_column_name]; 155 | $childNode[$this->left_column_name] = $parentNode[$this->right_column_name]; 156 | $childNode[$this->right_column_name] = $parentNode[$this->right_column_name]+1; 157 | 158 | $this->_modifyNode($childNode[$this->left_column_name], 2); 159 | $this->_setNewNode($childNode, $extrafields); 160 | 161 | return $this->getNodeWhereLeft($childNode[$this->left_column_name]); 162 | } 163 | /** 164 | * Adds a new node to the left of the supplied focusNode 165 | * @param array $focusNode The node to use as the position marker 166 | * @param array $extrafields An associative array of node attributes 167 | * @return array $siblingNode The new node 168 | */ 169 | public function insertSibling($focusNode, $extrafields) { 170 | $siblingNode[$this->parent_column_name] = $focusNode[$this->parent_column_name]; 171 | $siblingNode[$this->left_column_name] = $focusNode[$this->left_column_name]; 172 | $siblingNode[$this->right_column_name] = $focusNode[$this->left_column_name]+1; 173 | 174 | $this->_modifyNode($siblingNode[$this->left_column_name], 2); 175 | $this->_setNewNode($siblingNode, $extrafields); 176 | 177 | return $this->getNodeWhereLeft($siblingNode[$this->left_column_name]); 178 | } 179 | 180 | /** 181 | * Adds a new node to the right of the supplied focusNode 182 | * @param array $focusNode The node to use as the position marker 183 | * @param array $extrafields An associative array of node attributes 184 | * @return array $siblingNode The New Node 185 | */ 186 | public function appendSibling($focusNode, $extrafields) { 187 | $siblingNode[$this->parent_column_name] = $focusNode[$this->parent_column_name]; 188 | $siblingNode[$this->left_column_name] = $focusNode[$this->right_column_name]+1; 189 | $siblingNode[$this->right_column_name] = $focusNode[$this->right_column_name]+2; 190 | 191 | $this->_modifyNode($siblingNode[$this->left_column_name], 2); 192 | $this->_setNewNode($siblingNode, $extrafields); 193 | 194 | return $this->getNodeWhereLeft($siblingNode[$this->left_column_name]); 195 | } 196 | 197 | 198 | /** 199 | * Empties the table currently in use - use with extreme caution! 200 | * 201 | * @return boolean 202 | */ 203 | public function deleteTree() { 204 | return $this->db->delete($this->table_name); 205 | } 206 | 207 | /** 208 | * Deletes the given node (and any children) from the tree table 209 | * @param array $node The node to remove from the tree 210 | * @return array $newnode The node that replaced the deleted node 211 | */ 212 | public function deleteNode($node) { 213 | $leftanchor = $node[$this->left_column_name]; 214 | $leftcol = $this->left_column_name; 215 | $rightcol = $this->right_column_name; 216 | $leftval = $node[$this->left_column_name]; 217 | $rightval = $node[$this->right_column_name]; 218 | 219 | $where = array( 220 | $leftcol . ' >=' => $leftval, 221 | $rightcol . ' <=' => $rightval, 222 | ); 223 | $this->db->delete($this->table_name, $where); 224 | 225 | $this->_modifyNode($node[$this->right_column_name]+1, $node[$this->left_column_name] -$node[$this->right_column_name] - 1); 226 | 227 | return $this->getNodeWhere($leftcol . ' < ' . $leftanchor . ' ORDER BY ' . $leftcol . ' DESC'); 228 | } 229 | 230 | // ------------------------------------------------------------------------- 231 | // MODIFY/REORGANISE TREE 232 | // 233 | // Methods to move nodes around the tree. Method names should be 234 | // relatively self-explanatory! Hopefully ;) 235 | // 236 | // ------------------------------------------------------------------------- 237 | 238 | /** 239 | * Moves the given node to make it the next sibling of "target" 240 | * @param array $node The node to move 241 | * @param array $target The node to use as the position marker 242 | * @return array $newpos The new left and right values of the node moved 243 | */ 244 | public function setNodeAsNextSibling($node, $target) { 245 | $this->_setParent($node, $target[$this->parent_column_name]); 246 | return $this->_moveSubtree($node, $target[$this->right_column_name]+1); 247 | } 248 | 249 | /** 250 | * Moves the given node to make it the prior sibling of "target" 251 | * @param array $node The node to move 252 | * @param array $target The node to use as the position marker 253 | * @return array $newpos The new left and right values of the node moved 254 | */ 255 | public function setNodeAsPrevSibling($node, $target) { 256 | $this->_setParent($node, $target[$this->parent_column_name]); 257 | return $this->_moveSubtree($node, $target[$this->left_column_name]); 258 | } 259 | 260 | /** 261 | * Moves the given node to make it the first child of "target" 262 | * @param array $node The node to move 263 | * @param array $target The node to use as the position marker 264 | * @return array $newpos The new left and right values of the node moved 265 | */ 266 | public function setNodeAsFirstChild($node, $target) { 267 | $this->_setParent($node, $target[$this->primary_key_column_name]); 268 | return $this->_moveSubtree($node, $target[$this->left_column_name]+1); 269 | } 270 | 271 | /** 272 | * Moves the given node to make it the last child of "target" 273 | * @param array $node The node to move 274 | * @param array $target The node to use as the position marker 275 | * @return array $newpos The new left and right values of the node moved 276 | */ 277 | public function setNodeAsLastChild($node, $target) { 278 | $this->_setParent($node, $target[$this->primary_key_column_name]); 279 | return $this->_moveSubtree($node, $target[$this->right_column_name]); 280 | } 281 | 282 | // ------------------------------------------------------------------------- 283 | // QUERY METHODS 284 | // 285 | // Selecting nodes from the tree 286 | // 287 | // ------------------------------------------------------------------------- 288 | 289 | /** 290 | * Selects the first node to match the given where clause argument 291 | * @param mixed $whereArg Any valid SQL to follow the WHERE keyword in an SQL statement 292 | * @return array $resultNode The node returned from the query 293 | */ 294 | public function getNodeWhere($whereArg = '"1"="1"') { 295 | $resultNode[$this->left_column_name] = $resultNode[$this->right_column_name] = 0; 296 | 297 | $query = $this->db->get_where($this->table_name, $whereArg); 298 | 299 | $resultNode = array(); 300 | if($query->num_rows() > 0) 301 | { 302 | $result = $query->result_array(); 303 | $resultNode = array_shift($result); // assumes CI standard $row[0] = first row 304 | } 305 | 306 | return $resultNode; 307 | } 308 | 309 | /** 310 | * Gets an array of nodes 311 | * 312 | * @param mixed $whereArg String or array of where arguments 313 | * @param string $orderArg Orderby argument 314 | * @param integer $limit_start Number of rows to retrieve 315 | * @param mixed $limit_offset Row to start retrieving from 316 | * @return array Returns array of nodes found 317 | */ 318 | public function getNodesWhere($whereArg = '"1"="1"', $orderArg = '', $limit_start = 0, $limit_offset = null) { 319 | $resultNode[$this->left_column_name] = $resultNode[$this->right_column_name] = 0; 320 | 321 | if($orderArg) { 322 | $this->db->order_by($orderArg); 323 | } 324 | if($limit_start || $limit_offset) { 325 | $this->db->limit($limit_offset, $limit_start); 326 | } 327 | $query = $this->db->get_where($this->table_name, $whereArg); 328 | 329 | $resultNodes = array(); 330 | if($query->num_rows() > 0) 331 | { 332 | $resultNodes = $query->result_array(); 333 | } 334 | 335 | return $resultNodes; 336 | } 337 | 338 | /** 339 | * Returns the node identified by the given left value 340 | * @param integer $leftval The left value to use to select the node 341 | * @return array $resultNode The node returned 342 | */ 343 | public function getNodeWhereLeft($leftval) { 344 | return $this->getNodeWhere($this->left_column_name . ' = ' . $leftval); 345 | } 346 | 347 | /** 348 | * Returns the node identified by the given right value 349 | * @param integer $rightval The right value to use to select the node 350 | * @return array $resultNode The node returned 351 | */ 352 | public function getNodeWhereRight($rightval) { 353 | return $this->getNodeWhere($this->right_column_name . ' = ' . $rightval); 354 | } 355 | 356 | /** 357 | * Returns the root nodes 358 | * @return array $resultNode The node returned 359 | */ 360 | public function getRootNodes() { 361 | return $this->getNodesWhere($this->parent_column_name . ' = 0 '); 362 | } 363 | 364 | /** 365 | * Returns the node with the appropriate primary key field value. 366 | * Typically, this will be an auto_incrementing primary key column 367 | * such as categoryid 368 | * @param mixed $primarykey The value to look up in the primary key index 369 | * @return array $resultNode The node returned 370 | */ 371 | public function getNodeFromId($primarykey) { 372 | // Test if we've set the primary key column name property 373 | if(empty($this->primary_key_column_name)) return false; 374 | 375 | return $this->getNodeWhere($this->primary_key_column_name . ' = "' . $primarykey . '"'); 376 | } 377 | 378 | /** 379 | * Returns the first child node of the given parentNode 380 | * @param array $parentNode The parent node to use 381 | * @return array $resultNode The first child of the parent node supplied 382 | */ 383 | public function getFirstChild($parentNode) { 384 | return $this->getNodeWhere($this->left_column_name . ' = ' . ($parentNode[$this->left_column_name]+1)); 385 | } 386 | 387 | /** 388 | * Returns the last child node of the given parentNode 389 | * @param array $parentNode The parent node to use 390 | * @return array $resultNode the last child of the parent node supplied 391 | */ 392 | public function getLastChild($parentNode) { 393 | return $this->getNodeWhere($this->right_column_name . ' = ' . ($parentNode[$this->right_column_name]-1)); 394 | } 395 | 396 | /** 397 | * Returns the node that is the immediately prior sibling of the given node 398 | * @param array $currNode The node to use as the initial focus of enquiry 399 | * @return array $resultNode The node returned 400 | */ 401 | public function getPrevSibling($currNode) { 402 | return $this->getNodeWhere($this->right_column_name . ' = ' . ($currNode[$this->left_column_name]-1)); 403 | } 404 | 405 | /** 406 | * Returns the node that is the next sibling of the given node 407 | * @param array $currNode The node to use as the initial focus of enquiry 408 | * @return array $resultNode The node returned 409 | */ 410 | public function getNextSibling($currNode) { 411 | return $this->getNodeWhere($this->left_column_name . ' = ' . ($currNode[$this->right_column_name]+1)); 412 | } 413 | 414 | /** 415 | * Returns the node that represents the parent of the given node 416 | * @param array $currNode The node to use as the initial focus of enquiry 417 | * @return array $resultNode the node returned 418 | */ 419 | public function getAncestor($currNode) { 420 | return $this->getNodeWhere($this->primary_key_column_name . ' = "' . $currNode[$this->parent_column_name] . '"'); 421 | } 422 | 423 | 424 | // ------------------------------------------------------------------------- 425 | // NODE TEST METHODS 426 | // 427 | // Boolean tests for nodes 428 | // 429 | // ------------------------------------------------------------------------- 430 | 431 | 432 | /** 433 | * Returns true or false 434 | * (in reality, it checks to see if the given left and 435 | * right values _appear_ to be valid not necessarily that they _are_ valid) 436 | * @param array $node The node to test 437 | * @return boolean 438 | */ 439 | public function checkIsValidNode($node) { 440 | return (empty($node) ? false : ($node[$this->left_column_name] < $node[$this->right_column_name]) ); 441 | } 442 | 443 | /** 444 | * Tests whether the given node has an ancestor 445 | * (effectively the opposite of isRoot yes|no) 446 | * @param array $node The node to test 447 | * @return boolean 448 | */ 449 | public function checkNodeHasAncestor($node) { 450 | return $this->checkIsValidNode($this->getAncestor($node)); 451 | } 452 | 453 | /** 454 | * Tests whether the given node has a prior sibling or not 455 | * @param array $node 456 | * @return boolean 457 | */ 458 | public function checkNodeHasPrevSibling($node) { 459 | return $this->checkIsValidNode($this->getPrevSibling($node)); 460 | } 461 | 462 | /** 463 | * Test to see if node has siblings after itself 464 | * @param array $node The node to test 465 | * @return boolean 466 | */ 467 | public function checkNodeHasNextSibling($node) { 468 | return $this->checkIsValidNode($this->getNextSibling($node)); 469 | } 470 | 471 | /** 472 | * Test to see if node has children 473 | * @param array $node The node to test 474 | * @return boolean 475 | */ 476 | public function checkNodeHasChildren($node) { 477 | return (($node[$this->right_column_name] - $node[$this->left_column_name]) > 1); 478 | } 479 | 480 | /** 481 | * Test to see if the given node is also a root node 482 | * @param array $node The node to test 483 | * @return boolean 484 | */ 485 | public function checkNodeIsRoot($node) { 486 | return ($node[$this->parent_column_name] == 0); 487 | } 488 | 489 | /** 490 | * Test to see if the given node is a leaf node (ie has no children) 491 | * @param array $node The node to test 492 | * @return boolean 493 | */ 494 | public function checkNodeIsLeaf($node) { 495 | return (($node[$this->right_column_name] - $node[$this->left_column_name]) == 1); 496 | } 497 | 498 | /** 499 | * Test to see if the first given node is a child of the second given node 500 | * @param array $testNode the node to test for child status 501 | * @param array $controlNode the node to use as the parent or ancestor 502 | * @return boolean 503 | */ 504 | public function checkNodeIsChild($testNode, $controlNode) { 505 | return ($testNode[$this->parent_column_name] == $controlNode[$this->primary_key_column_name]); 506 | } 507 | 508 | /** 509 | * Test to determine whether testNode is infact also controlNode (is A === B) 510 | * @param array $testNode The node to test 511 | * @param array $controlNode The node prototype to use for the comparison 512 | * @return boolean 513 | */ 514 | public function checkNodeIsEqual($testNode, $controlNode) { 515 | return (($testNode[$this->left_column_name]==$controlNode[$this->left_column_name]) and ($testNode[$this->right_column_name]==$controlNode[$this->right_column_name])); 516 | } 517 | 518 | /** 519 | * Combination method of IsChild and IsEqual 520 | * @param array $testNode The node to test 521 | * @param array $controlNode The node prototype to use for the comparison 522 | * @return boolean 523 | */ 524 | public function checkNodeIsChildOrEqual($testNode, $controlNode) { 525 | return (($testNode[$this->left_column_name]>=$controlNode[$this->left_column_name]) and ($testNode[$this->right_column_name]<=$controlNode[$this->right_column_name])); 526 | } 527 | 528 | 529 | // ------------------------------------------------------------------------- 530 | // TREE QUERY METHODS 531 | // 532 | // Query the tree itself 533 | // 534 | // ------------------------------------------------------------------------- 535 | 536 | /** 537 | * Returns the number of descendents that a node has 538 | * @param array $node The node to query 539 | * @return integer The number of descendents 540 | */ 541 | public function getNumberOfChildren($node) { 542 | return (($node[$this->right_column_name] - $node[$this->left_column_name] - 1) / 2); 543 | } 544 | 545 | /** 546 | * Returns the tree level for the given node (assuming root node is at level 0) 547 | * @param array $node The node to query 548 | * @return integer The level of the supplied node 549 | */ 550 | public function getNodeLevel($node) { 551 | $leftcol = $this->left_column_name; 552 | $rightcol = $this->right_column_name; 553 | $leftval = (int) $node[$leftcol]; 554 | $rightval = (int) $node[$rightcol]; 555 | 556 | $this->db->where($leftcol . ' <', $leftval); 557 | $this->db->where($rightcol . ' >', $rightval); 558 | 559 | return $this->db->count_all_results($this->table_name); 560 | } 561 | 562 | /** 563 | * Returns an array of the tree starting from the supplied node 564 | * @param array $node The node to use as the starting point (typically root) 565 | * @param boolean $direct When true, will retrieve only immediate children using parent col 566 | * @return array $tree_handle The tree represented as an array to assist with 567 | * the other tree traversal operations 568 | */ 569 | public function getTreePreorder($node, $direct=false) { 570 | $leftcol = $this->left_column_name; 571 | $rightcol = $this->right_column_name; 572 | $leftval = (int) $node[$leftcol]; 573 | $rightval = (int) $node[$rightcol]; 574 | 575 | $primarykeycol = $this->primary_key_column_name; 576 | $parentcol = $this->parent_column_name; 577 | $primarykeyval = (int) $node[$primarykeycol]; 578 | 579 | if( $direct ) { 580 | $this->db->where($parentcol, $primarykeyval); 581 | }else{ 582 | $this->db->where($leftcol . ' >=', $leftval); 583 | $this->db->where($rightcol . ' <=', $rightval); 584 | } 585 | $this->db->order_by($leftcol, 'asc'); 586 | $query = $this->db->get($this->table_name); 587 | 588 | $treeArray = array(); 589 | 590 | if($query->num_rows() > 0) { 591 | foreach($query->result_array() AS $result) { 592 | $treeArray[] = $result; 593 | } 594 | } 595 | 596 | $retArray = array( 'result_array' => $treeArray, 597 | 'prev_left' => $node[$leftcol], 598 | 'prev_right' => $node[$rightcol], 599 | 'level' => -2); 600 | 601 | return $retArray; 602 | } 603 | 604 | /** 605 | * Returns the next element from the tree and updates the tree_handle with the 606 | * new positions 607 | * @param array $tree_handle Passed by reference to allow for modifications 608 | * @return array The next node in the tree 609 | */ 610 | public function getTreeNext(&$tree_handle) { 611 | $leftcol = $this->left_column_name; 612 | $rightcol = $this->right_column_name; 613 | 614 | if(!empty($tree_handle['result_array'])) { 615 | if($row = array_shift($tree_handle['result_array'])) { 616 | 617 | $tree_handle['level']+= $tree_handle['prev_left'] - $row[$leftcol] + 2; 618 | // store current node 619 | $tree_handle['prev_left'] = $row[$leftcol]; 620 | $tree_handle['prev_right'] = $row[$rightcol]; 621 | $tree_handle['row'] = $row; 622 | 623 | return array( $leftcol => $row[$leftcol], 624 | $rightcol => $row[$rightcol] 625 | ); 626 | } 627 | } 628 | 629 | return FALSE; 630 | } 631 | 632 | /** 633 | * Returns the given attribute (database field) for the current node in $tree_handle 634 | * @param array $tree_handle The tree as an array 635 | * @param string $attribute A string containing the fieldname to retrieve 636 | * @return string The value requested 637 | */ 638 | public function getTreeAttribute($tree_handle,$attribute) { 639 | return $tree_handle['row'][$attribute]; 640 | } 641 | 642 | /** 643 | * Returns the current node of the tree contained in $tree_handle 644 | * @param array $tree_handle The tree as an array 645 | * @return array The left and right values of the current node 646 | */ 647 | public function getTreeCurrent($tree_handle) { 648 | return array( 649 | $this->left_column_name => $tree_handle['prev_left'], 650 | $this->right_column_name => $tree_handle['prev_right'], 651 | ); 652 | } 653 | 654 | /** 655 | * Returns the current level from the tree 656 | * @param array $tree_handle The tree as an array 657 | * @return integer The integer value of the current level 658 | */ 659 | public function getTreeLevel($tree_handle) { 660 | return $tree_handle['level']; 661 | } 662 | 663 | /** 664 | * Find the path of a given node 665 | * @param array $node The node to start with 666 | * @param boolean $includeSelf Wheter or not to include given node in result 667 | * @param boolean $returnAsArray Wheter or not to return array or unordered list 668 | * @return array or unordered list 669 | */ 670 | public function getPath($node, $includeSelf=FALSE, $returnAsArray=FALSE) { 671 | 672 | if(empty($node)) return FALSE; 673 | 674 | $leftcol = $this->left_column_name; 675 | $rightcol = $this->right_column_name; 676 | $leftval = (int) $node[$leftcol]; 677 | $rightval = (int) $node[$rightcol]; 678 | 679 | if($includeSelf) 680 | { 681 | $this->db->where($leftcol . ' <= ' . $leftval . ' AND ' . $rightcol . ' >= ' . $rightval); 682 | } 683 | else 684 | { 685 | $this->db->where($leftcol . ' < ' . $leftval . ' AND ' . $rightcol . ' > ' . $rightval); 686 | } 687 | 688 | $this->db->order_by($leftcol); 689 | $query = $this->db->get($this->table_name); 690 | 691 | if($query->num_rows() > 0) 692 | { 693 | if($returnAsArray) 694 | { 695 | return $query->result_array(); 696 | } 697 | else 698 | { 699 | return $this->buildCrumbs($query->result_array()); 700 | } 701 | } 702 | 703 | return FALSE; 704 | } 705 | 706 | function buildCrumbs($crumbData) 707 | { 708 | $retVal = ''; 709 | 710 | $retVal = ''; 727 | 728 | return $retVal; 729 | } 730 | 731 | 732 | // ------------------------------------------------------------------------- 733 | // NODE FIELD QUERIES 734 | // 735 | // ------------------------------------------------------------------------- 736 | 737 | /** 738 | * Queries the database for the value of the given field 739 | * @param array $node The node to be queried 740 | * @param string $fieldname The name of the field to query 741 | * @return string $retval The value of the field for the node looked up 742 | */ 743 | public function getNodeAttribute($node, $fieldname) { 744 | $leftcol = $this->left_column_name; 745 | $leftval = (int) $node[$leftcol]; 746 | 747 | $this->db->where($leftcol, $leftval); 748 | $query = $this->db->get($this->table_name); 749 | 750 | if($query->num_rows() > 0) { 751 | $res = $query->result(); 752 | return $res->$fieldname; 753 | } else { 754 | return ''; 755 | } 756 | } 757 | 758 | /** 759 | * Renders the fields for each node starting at the given node 760 | * @param array $node The node to start with 761 | * @param array $fields The fields to display for each node 762 | * @return string Sample HTML render of tree 763 | */ 764 | public function getSubTreeAsHTML($nodes, $fields = array()) { 765 | if(isset($nodes[0]) && !is_array($nodes[0])) { 766 | $nodes = array($nodes); 767 | } 768 | 769 | $retVal = ''; 770 | foreach($nodes AS $node) { 771 | $tree_handle = $this->getTreePreorder($node); 772 | 773 | while($this->getTreeNext($tree_handle)) 774 | { 775 | // print indentation 776 | $retVal .= (str_repeat(' ', $this->getTreeLevel($tree_handle)*4)); 777 | 778 | // print requested fields 779 | $field = reset($fields); 780 | while($field){ 781 | $retVal .= $tree_handle['row'][$field] . "\n"; 782 | $field = next($fields); 783 | } 784 | $retVal .= "
\n"; 785 | 786 | } 787 | } 788 | 789 | return $retVal; 790 | } 791 | 792 | /** 793 | * Renders the tree starting from given node 794 | * @param array $node The node to start with 795 | * @return string Unordered HTML list of the tree 796 | */ 797 | public function getSubTree($node) { 798 | 799 | if(empty($node)) return FALSE; 800 | 801 | $tree_handle = $this->getTreePreorder($node); 802 | 803 | $menuData = array( 804 | 'items' => array(), 805 | 'parents' => array() 806 | ); 807 | 808 | foreach ($tree_handle['result_array'] as $menuItem) 809 | { 810 | $menuData['items'][$menuItem[$this->primary_key_column_name]] = $menuItem; 811 | $menuData['parents'][$menuItem[$this->parent_column_name]][] = $menuItem[$this->primary_key_column_name]; 812 | } 813 | 814 | return $menuData; 815 | // return $this->buildMenu($node['parent_id'], $menuData); 816 | } 817 | 818 | function buildMenu($parentId, $menuData, $depth=0) 819 | { 820 | $retVal = ''; 821 | 822 | if (isset($menuData['parents'][$parentId])) 823 | { 824 | $retVal = '
    '; 825 | 826 | foreach ($menuData['parents'][$parentId] as $itemId) 827 | { 828 | 829 | $retVal .= '
  • ' . anchor( 830 | 'shop/category/' . $menuData['items'][$itemId]['id'], 831 | $menuData['items'][$itemId][$this->text_column_name], 832 | array( 833 | 'class' => 'id-' . $itemId['id'] 834 | ) 835 | ); 836 | 837 | $retVal .= $this->buildMenu($itemId, $menuData, $depth+1); 838 | 839 | $retVal .= '
  • '; 840 | } 841 | 842 | $retVal .= '
'; 843 | } 844 | 845 | return $retVal; 846 | } 847 | 848 | 849 | /** 850 | * Renders the entire tree as per getSubTreeAsHTML starting from root 851 | * @param array $fields An array of the fields to display 852 | */ 853 | public function getTreeAsHTML($fields=array()) { 854 | return $this->getSubTreeAsHTML($this->getRootNodes(), $fields); 855 | } 856 | 857 | // ------------------------------------------------------------------------- 858 | // INTERNALS 859 | // 860 | // Private, internal methods 861 | // 862 | // ------------------------------------------------------------------------- 863 | 864 | /** 865 | * _setNewNode 866 | * 867 | * Inserts a new node into the tree 868 | * 869 | * @param array $node An array containing the left and right values to use 870 | * @param array $extrafields An associative array of field names to values for \ 871 | * additional columns in tree table (eg CategoryName etc) 872 | * 873 | * @return boolean True/False dependent upon the success of the operation 874 | * @access private 875 | */ 876 | private function _setNewNode($node, $extrafields) { 877 | $parentcol = $this->parent_column_name; 878 | $leftcol = $this->left_column_name; 879 | $rightcol = $this->right_column_name; 880 | $parentval = (int) $node[$parentcol]; 881 | $leftval = (int) $node[$leftcol]; 882 | $rightval = (int) $node[$rightcol]; 883 | 884 | $data = array( 885 | $parentcol => $parentval, 886 | $leftcol => $leftval, 887 | $rightcol => $rightval, 888 | ); 889 | if(is_array($extrafields) && !empty($extrafields)) $data = array_merge($data, $extrafields); 890 | 891 | $result = $this->db->insert($this->table_name, $data); 892 | 893 | if(!$result) { 894 | log_message('error', 'Node addition failed for ' . $leftval . ' - ' . $rightval); 895 | } 896 | 897 | return $result; 898 | } 899 | 900 | /** 901 | * Sets the parent for the specified node 902 | * 903 | * @param array $node The child node 904 | * @param integer $parent_id The id of the parent node 905 | * @access private 906 | */ 907 | private function _setParent($node, $parent_id) { 908 | $primarykeycol = $this->primary_key_column_name; 909 | $parentcol = $this->parent_column_name; 910 | $primarykeyval = (int) $node[$primarykeycol]; 911 | $parentval = (int) $node[$parentcol]; 912 | 913 | if($parentval != $parent_id) { 914 | $data = array( 915 | $parentcol => $parent_id, 916 | ); 917 | 918 | $this->db->where($primarykeycol, $primarykeyval); 919 | $this->db->update($this->table_name, $data); 920 | } 921 | } 922 | 923 | /** 924 | * The method that performs moving/renumbering operations 925 | * 926 | * @param array $node The node to move 927 | * @param array $targetValue Position integer to use as the target 928 | * @return array $newpos The new left and right values of the node moved 929 | * @access private 930 | */ 931 | private function _moveSubtree($node, $targetValue) { 932 | $sizeOfTree = $node[$this->right_column_name] - $node[$this->left_column_name] + 1; 933 | $this->_modifyNode($targetValue, $sizeOfTree); 934 | 935 | if($node[$this->left_column_name] >= $targetValue) 936 | { 937 | $node[$this->left_column_name] += $sizeOfTree; 938 | $node[$this->right_column_name] += $sizeOfTree; 939 | } 940 | 941 | $newpos = $this->_modifyNodeRange($node[$this->left_column_name], $node[$this->right_column_name], $targetValue - $node[$this->left_column_name]); 942 | 943 | $this->_modifyNode($node[$this->right_column_name]+1, - $sizeOfTree); 944 | 945 | if($node[$this->left_column_name] <= $targetValue) 946 | { 947 | $newpos[$this->left_column_name] -= $sizeOfTree; 948 | $newpos[$this->right_column_name] -= $sizeOfTree; 949 | } 950 | 951 | return $newpos; 952 | } 953 | 954 | /** 955 | * _modifyNode 956 | * 957 | * Adds $changeVal to all left and right values that are greater than or 958 | * equal to $node_int 959 | * 960 | * @param integer $node_int The value to start the shift from 961 | * @param integer $changeVal unsigned integer value for change 962 | * @access private 963 | */ 964 | private function _modifyNode($node_int, $changeVal) { 965 | $leftcol = $this->left_column_name; 966 | $rightcol = $this->right_column_name; 967 | 968 | $this->db->set($leftcol, $leftcol . '+' . (int) $changeVal, FALSE); 969 | $this->db->where($leftcol . ' >=', (int) $node_int); 970 | $this->db->update($this->table_name); 971 | 972 | $this->db->set($rightcol, $rightcol . '+' . (int) $changeVal, FALSE); 973 | $this->db->where($rightcol . ' >=', (int) $node_int); 974 | $this->db->update($this->table_name); 975 | } 976 | 977 | /** 978 | * _modifyNodeRange 979 | * 980 | * @param integer $lowerbound integer value of lowerbound of range to move 981 | * @param integer $upperbound integer value of upperbound of range to move 982 | * @param integer $changeVal unsigned integer of change amount 983 | * @return array Returns array of the new left & right column values 984 | * @access private 985 | */ 986 | private function _modifyNodeRange($lowerbound, $upperbound, $changeVal) { 987 | $leftcol = $this->left_column_name; 988 | $rightcol = $this->right_column_name; 989 | 990 | $this->db->set($leftcol, $leftcol . '+' . (int) $changeVal, FALSE); 991 | $this->db->where($leftcol . ' >=', (int) $lowerbound); 992 | $this->db->where($leftcol . ' <=', (int) $upperbound); 993 | $this->db->update($this->table_name); 994 | 995 | $this->db->set($rightcol, $rightcol . '+' . (int) $changeVal, FALSE); 996 | $this->db->where($rightcol . ' >=', (int) $lowerbound); 997 | $this->db->where($rightcol . ' <=', (int) $upperbound); 998 | $this->db->update($this->table_name); 999 | 1000 | $retArray = array( 1001 | $this->left_column_name => $lowerbound+$changeVal, 1002 | $this->right_column_name => $upperbound+$changeVal 1003 | ); 1004 | return $retArray; 1005 | } 1006 | 1007 | } 1008 | 1009 | ?> 1010 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CodeIgniter - Nested Sets Library 2 | ============== 3 | 4 | Improved / Fixed Nested Set Library for CodeIgniter, originally created by Thunder / intel352 5 | 6 | ## Support on Beerpay 7 | Hey dude! Help me out for a couple of :beers:! 8 | 9 | [![Beerpay](https://beerpay.io/olimortimer/ci-nested-sets/badge.svg?style=beer-square)](https://beerpay.io/olimortimer/ci-nested-sets) [![Beerpay](https://beerpay.io/olimortimer/ci-nested-sets/make-wish.svg?style=flat-square)](https://beerpay.io/olimortimer/ci-nested-sets?focus=wish) --------------------------------------------------------------------------------