summaryrefslogtreecommitdiff
path: root/modules/mptt
diff options
context:
space:
mode:
Diffstat (limited to 'modules/mptt')
-rw-r--r--modules/mptt/libraries/MPTT.php1187
1 files changed, 1187 insertions, 0 deletions
diff --git a/modules/mptt/libraries/MPTT.php b/modules/mptt/libraries/MPTT.php
new file mode 100644
index 00000000..84182a90
--- /dev/null
+++ b/modules/mptt/libraries/MPTT.php
@@ -0,0 +1,1187 @@
+<?php
+
+/**
+* File: libraries/MPTT.php
+*
+* An implementation of Joe Celko's Nested Sets as a Kohana model with ORM support.
+*
+*
+* Many thanks to
+* * Thunder who supplied a similar class for Code Igniter and gave permission to me
+* to release it under whatever license I deem necessary:
+* http://www.codeigniter.com/wiki/Nested_Sets/
+* * mpatek, his class was the initial start
+* http://bakery.cakephp.org/articles/view/modified-preorder-tree-traversal-component
+* * Propel, for inspiring some methods and the parent_id and scope stuff
+*
+* MPTT class
+* author - dlib
+* license - BSD
+*/
+
+class MPTT extends ORM {
+
+ public $children;
+
+ public $parent;
+
+ protected $left_column = 'lft';
+
+ protected $right_column = 'rgt';
+
+ protected $parent_column = 'parent_id';
+
+ protected $scope_column = 'scope';
+
+ protected $_scope = 1;
+
+ /**
+ * Patch through to ORM construct
+ *
+ * @return void
+ */
+ public function __construct($id = FALSE)
+ {
+ parent::__construct($id);
+
+ if (!empty($id))
+ {
+ //Set the right scope on new objects
+ $scope_column=$this->scope_column;
+ $this->set_scope($this->$scope_column);
+ $this->where='';
+ }
+
+
+ }
+
+ //////////////////////////////////////////
+ // Lock functions
+ //////////////////////////////////////////
+
+ /**
+ * Locks tree table
+ * This is a straight write lock - the database blocks until the previous lock is released
+ */
+ protected function lock_tree($aliases = array())
+ {
+ $sql = "LOCK TABLE " . $this->table . " WRITE";
+ return self::$db->query($sql);
+ }
+
+ /**
+ * Unlocks tree table
+ * Releases previous lock
+ */
+ protected function unlock_tree()
+ {
+ $sql = "UNLOCK TABLES";
+ return self::$db->query($sql);
+ }
+
+ /**
+ * Bit of an odd function since the node is supplied
+ * supply a node object and it will return a whole bunch of descendants
+ * @return tree object
+ * @param $root_node mixed
+
+ */
+ public function get_tree($root_node)
+ {
+
+ $lft_col = $this->left_column;
+ $rgt_col = $this->right_column;
+
+ if(is_object($root_node))
+ {
+ $root=$root_node;
+ }
+ else
+ {
+ $root=$this->get_node($root_node);
+ }
+
+ $children=$this->new_node();
+ $children->where('`'.$lft_col.'`>'.$root->$lft_col.' AND `'.$rgt_col.'`<'.$root->$rgt_col);
+ $children->where($this->scope_column,$this->get_scope());
+ $children->orderby($lft_col,'ASC');
+ $children=$children->find_all();
+
+ if(count($children)>0)
+ {
+
+ $parent=$root;
+ $parent->children=array();
+
+ foreach($children as $child_data)
+ {
+
+ $child=$child_data;
+ $child->children=array();
+ $child->parent=$parent;
+ $child->scope=$this->get_scope();
+
+ while(!$this->_is_descendant_of($child, $parent))
+ {
+ $parent = $parent->parent;
+ }
+
+ $parent->children[]=$child;
+ $parent=$child;
+
+ }
+ }
+
+ return $root;
+ }
+ /*
+ * *****************
+ * Retrieval methods
+ * *****************
+ */
+ /**
+ * Current object will obtain its children
+ * @see http://trac.symfony-project.com/wiki/sfPropelActAsNestedSetBehaviorPlugin
+ * @see http://www.phpriot.com/articles/nested-trees-2/5
+ * @return
+ */
+ public function get_children($return=false)
+ {
+ if(!$this->has_descendants())
+ return false;
+
+ $parent_id=$this->id;
+
+ $this->children=array();
+ $children=$this->new_node();
+ $children->where($this->parent_column,$parent_id);
+ $children->where($this->scope_column,$this->get_scope());
+ $children->orderby($this->left_column);
+
+ foreach($children->find_all() as $child)
+ {
+ $child->parent=$this;
+
+ $this->children[]=$child;
+ }
+ if($return== true)
+ {
+ return $this->children;
+ }
+ return $this;
+ }
+
+ /**
+ * The current object will obtain all descendants
+ * @return ORM object
+ * @param $node_id Object
+ */
+ public function get_descendants()
+ {
+ if($this->has_descendants())
+ return $this->get_tree($this);
+ }
+ /**
+ * Return an array of all leaves in the entire tree
+ *
+ * @return array of ORM objects
+ */
+ public function get_leaves()
+ {
+
+ $lft_col = $this->left_column;
+ $rgt_col = $this->right_column;
+
+ $leaves=$this->new_node();
+ $leaves->where($this->scope_column,$this->get_scope());
+ $leaves->where($rgt_col.' = '.$lft_col . ' + 1');
+
+ return $leaves->find_all();
+ }
+ /**
+ * Get the path leading up to the current node
+ * @return array with ORM objects
+ *
+ */
+ public function get_path()
+ {
+
+ $lft_col = $this->left_column;
+ $rgt_col = $this->right_column;
+
+ $path=$this->new_node();
+ $path->where($this->scope_column,$this->get_scope());
+ $path->where($lft_col . ' <= '.$this->$lft_col . ' AND ' . $rgt_col . ' >=' .$this->$rgt_col . ' ORDER BY '.$lft_col);
+
+
+ return $path->find_all();
+
+ }
+
+ /**
+ * Returns the root node
+ * @return array $resultNode The node returned
+ */
+ function get_root()
+ {
+ return $this->get_node_where('`'.$this->left_column . '` = 1 ');
+ }
+ /**
+ * Returns a node by id
+ * @return object
+ * @param $node_id integer [optional]
+ */
+ function get_node($node_id)
+ {
+ $scope_column=$this->scope_column;
+ $class = get_class($this);
+ $node=new $class();
+
+ $node=$node->find($node_id,true);
+
+ return $node;
+ }
+ /**
+ * Returns a new empty node
+ * @return object
+ * @param $node_id integer [optional]
+ */
+ function new_node(){
+
+ $class = get_class($this);
+ $node=new $class();
+ return $node;
+ }
+ /**
+ * Returns one node with where condition
+ * @return object
+ * @param $node_id integer [optional]
+ */
+ function get_node_where($where)
+ {
+ $scope_column=$this->scope_column;
+ $class = get_class($this);
+ $node = new $class();
+ $node->where($where);
+ $node->where($this->scope_column,$this->get_scope());
+
+ $node=$node->find(false,false);
+ return $node;
+ }
+ /**
+ * Returns the first child node of the given parentNode
+ *
+ * @return array $resultNode The first child of the parent node supplied
+ */
+ function get_first_child()
+ {
+ $lft_col = $this->left_column;
+
+ return $this->get_node_where($this->left_column . " = " . ($this->$lft_col+1));
+ }
+
+ /**
+ * Returns the last child node of the given parentNode
+ *
+ * @return array $resultNode the last child of the parent node supplied
+ */
+ function get_last_child()
+ {
+ $rgt_col = $this->right_column;
+
+ return $this->get_node_where($this->right_column . " = " . ($this->$rgt_col-1));
+ }
+
+ /**
+ * Returns the node that is the immediately prior sibling of the given node
+ *
+ * @return array $resultNode The node returned
+ */
+ function get_prev_sibling()
+ {
+ $lft_col = $this->left_column;
+
+ return $this->get_node_where($this->right_column . " = " . ($this->$lft_col-1));
+ }
+
+ /**
+ * Returns the node that is the next sibling of the given node
+ *
+ * @return array $resultNode The node returned
+ */
+ function get_next_sibling()
+ {
+ $rgt_col = $this->right_column;
+
+ return $this->get_node_where($this->left_column . " = " . ($this->$rgt_col+1));
+ }
+ /**
+ * Returns the node that represents the parent of the given node
+ *
+ * @return array $resultNode the node returned
+ */
+ function get_parent()
+ {
+
+ $leftcol = $this->left_column;
+ $rightcol = $this->right_column;
+
+ $whereArg = " $leftcol < " . $this->$leftcol .
+ " AND $rightcol > " . $this->$rightcol .
+ " ORDER BY $rightcol ASC";
+ return $this->get_node_where($whereArg);
+ }
+/*
+ * *****************
+ * Modifying methods
+ * *****************
+ */
+ /**
+ * Adds the first entry to the table (only call once in an empty table) else corruption will follow
+ * @return $node an array of left and right values
+ */
+ function make_root()
+ {
+ $lft_col = $this->left_column;
+ $rgt_col = $this->right_column;
+ $scp_col=$this->scope_column;
+
+ if(is_numeric($this->$lft_col) )
+ {
+ Log::add('error', 'Cannot make existing node root' );
+ //existing nodes cannot become root
+ return false;
+ }
+
+ $this->$lft_col=1;
+ $this->$rgt_col=2;
+ $this->$scp_col=$this->get_scope();
+
+ $this->save_node();
+
+ return $this;
+ }
+ /**
+ * Not yet implemented
+ *
+ */
+ function insert_as_parent_of($child_node)
+ {
+
+ }
+ /**
+ * inserts a the object node as the first child of the supplied parent node
+ * @param array $parentNode The node array of the parent to use
+ *
+ * @return
+ */
+ function insert_as_first_child_of($parent_node)
+ {
+ $lft_col = $this->left_column;
+ $rgt_col = $this->right_column;
+ $scp_col=$this->scope_column;
+ $parent_column=$this->parent_column;
+
+ //Set parent id (id of the parent, is childs parent id)
+ $this->$parent_column=$parent_node->id;
+
+ $this->$lft_col=$parent_node->$lft_col+1;
+ $this->$rgt_col=$parent_node->$lft_col+2;
+ //Get scope from current object (obsolete)
+ $this->$scp_col=$this->get_scope();
+
+ $this->lock_tree();
+ $this->modify_node($this->$lft_col,2);
+ $this->save_node();
+ $this->unlock_tree();
+
+ return $this;
+ }
+ /**
+ * Same as insertNewChild except the new node is added as the last child
+ * @param array $parentNode The node array of the parent to use
+ *
+ * @return
+ */
+ function insert_as_last_child_of($parent_node)
+ {
+ $lft_col = $this->left_column;
+ $rgt_col = $this->right_column;
+ $scp_col=$this->scope_column;
+ $parent_column=$this->parent_column;
+
+ //Set parent id (id of the parent, is childs parent id)
+ $this->$parent_column=$parent_node->id;
+
+ $this->$lft_col=$parent_node->$rgt_col;
+ $this->$rgt_col=$parent_node->$rgt_col+1;
+ $this->$scp_col=$this->get_scope();
+
+ $this->lock_tree();
+ $this->modify_node($this->$lft_col,2);
+ $this->save_node();
+ $this->unlock_tree();
+
+ return $this;
+ }
+ /**
+ * Adds a new node to the left of the supplied focusNode
+ * @param array $focusNode The node to use as the position marker
+ *
+ * @return
+ */
+ function insert_as_prev_sibling_of($focus_node)
+ {
+ $lft_col = $this->left_column;
+ $rgt_col = $this->right_column;
+ $parent_column=$this->parent_column;
+ $scp_col=$this->scope_column;
+
+ //Set parent id (siblings have the same parent)
+ $this->$parent_column=$focus_node->$parent_column;
+
+ $this->$lft_col=$focus_node->$lft_col;
+ $this->$rgt_col=$focus_node->$lft_col+1;
+ $this->$scp_col=$this->get_scope();
+
+ $this->lock_tree();
+ $this->modify_node($this->$lft_col,2);
+ $this->save_node();
+ $this->unlock_tree();
+
+ return $this;
+ }
+ /**
+ * Adds a new node to the right of the supplied focusNode
+ * @param array $focusNode The node to use as the position marker
+ *
+ * @return
+ */
+ function insert_as_next_sibling_of($focus_node)
+ {
+ $lft_col = $this->left_column;
+ $rgt_col = $this->right_column;
+ $parent_column=$this->parent_column;
+ $scp_col=$this->scope_column;
+
+ //Set parent id (siblings have the same parent)
+ $this->$parent_column=$focus_node->$parent_column;
+
+ $this->$lft_col=$focus_node->$rgt_col+1;
+ $this->$rgt_col=$focus_node->$rgt_col+2;
+ $this->$scp_col=$this->get_scope();
+
+ $this->lock_tree();
+ $this->modify_node($this->$lft_col,2);
+ $this->save_node();
+ $this->unlock_tree();
+
+ return $this;
+ }
+ /**
+ * Why not, kill the entire tree
+ *
+ * @return
+ */
+ function delete_tree()
+ {
+ $where=array($this->scope_column=>$this->get_scope());
+
+ self::$db->delete($this->table, $where);
+ return;
+ }
+ /**
+ *
+ * overrides delete of ORM
+ */
+ public function delete()
+ {
+ return $this->delete_node();
+ }
+ /**
+ * Deletes the given node ((itself) from the tree table
+ * @param children boolean set to false to not delete children
+ * and move them up the tree
+ * @return boolean
+ */
+ function delete_node($children=true)
+ {
+
+ $table = $this->table;
+ $leftcol = $this->left_column;
+ $rightcol = $this->right_column;
+ $leftanchor = $this->$leftcol;
+ $leftval = $this->$leftcol;
+ $rightval = $this->$rightcol;
+ $parent_column = $this->parent_column;
+ if($children==true)
+ {
+
+ $where=$leftcol . '>='.$leftval .'
+ AND '. $rightcol .' <= ' . $rightval . '
+ AND '. $this->scope_column .' = '.$this->get_scope() ;
+
+ $this->lock_tree();
+ //Delete node and children
+ self::$db->delete($this->table, $where);
+ //Modify other nodes to restore tree integrity
+ $this->modify_node($rightval+1, $leftval - $rightval - 1);
+ $this->unlock_tree();
+ }
+ else
+ {
+ if($this->is_root()){
+ Log::add('error', 'Cannot delete root node without deleting its children' );
+ return false;
+ }
+ $this->lock_tree();
+ //Get children before the parent is gone
+ //Set parent ids right again
+ $parent_node=$this->get_node($this->$parent_column);
+
+ $children=$this->get_children(true);
+
+ //Delete the node
+ $where=array('id'=>$this->id);
+ self::$db->delete($this->table, $where);
+
+ //First update
+ $sql = 'UPDATE `' . $this->table . '`
+ SET '. $this->left_column .'='.$this->left_column .'-1, '.
+ $this->right_column.' = '.$this->right_column . '-1
+ WHERE '. $this->left_column .' >= '.$this->$leftcol . '
+ AND '.$this->right_column .' <= '.$this->$rightcol .'
+ AND '.$this->scope_column.' = '.$this->get_scope().';';
+ self::$db->query($sql);
+
+ //Second update
+ $sql = 'UPDATE `' . $this->table . '`
+ SET '. $this->left_column .'='.$this->left_column .' -2
+ WHERE '. $this->right_column .' > '.$this->$rightcol . '-1
+ AND '.$this->left_column .' > '.$this->$rightcol .'-1
+ AND '.$this->scope_column.' = '.$this->get_scope().';';
+ self::$db->query($sql);
+
+ //Third update
+ $sql = 'UPDATE `' . $this->table . '`
+ SET '. $this->right_column .'='.$this->right_column .'-2
+ WHERE '. $this->right_column .' > '.$this->$rightcol . '-1
+ AND '.$this->scope_column.' = '.$this->get_scope().';';
+ self::$db->query($sql);
+
+ //Set the parent ids
+ if(is_array($children))
+ {
+ foreach($children as $child)
+ {
+ $child->$parent_column=$parent_node->id;
+ $child->save_node();
+ }
+ }
+ $this->unlock_tree();
+
+ }
+
+
+ return true;
+ }
+
+ /**
+ * Delete descendants but not node itself
+ *
+ * @return
+ */
+ function delete_descendants()
+ {
+ $this->lock_tree();
+ $this->get_children();
+ foreach($this->children as $child)
+ {
+ $child->delete_node();
+
+ }
+ $this->unlock_tree();
+ $this->children=array();
+ return true;
+ }
+ /**
+ * Deletes children but not descendants,
+ * descendants move up the tree
+ * @return
+ */
+ function delete_children()
+ {
+ $this->get_children();
+ foreach($this->children as $child)
+ {
+ $child->delete_node(false);
+
+ }
+ $this->unlock_tree();
+ $this->children=array();
+ return true;
+ }
+
+ // -------------------------------------------------------------------------
+ // MODIFY/REORGANISE TREE
+ //
+ // Methods to move nodes around the tree. Method names should be
+ // relatively self-explanatory! Hopefully ;)
+ //
+ // -------------------------------------------------------------------------
+
+ //inter-scope moves might be something for later
+ /**
+ * Moves the given node to make it the next sibling of "target"
+ *
+ * @param array $target The node to use as the position marker
+ * @return array $newpos The new left and right values of the node moved
+ */
+ function move_to_next_sibling_of( $target)
+ {
+ $rgt_col = $this->right_column;
+ $parent_column=$this->parent_column;
+
+ //Set parent id (siblings have the same parent)
+ $parent_id=$target->$parent_column;
+
+ //only move when scopes are equal
+ if($target->get_scope()==$this->get_scope())
+ {
+ $this->update_parent_id($parent_id);
+
+ return $this->move_sub_tree( $target->$rgt_col+1);
+ }
+ return false;
+ }
+
+ /**
+ * Moves the given node to make it the prior sibling of "target"
+ *
+ * @param array $target The node to use as the position marker
+ * @return array $newpos The new left and right values of the node moved
+ */
+ function move_to_prev_sibling_of( $target)
+ {
+ $lft_col = $this->left_column;
+ $parent_column=$this->parent_column;
+
+ //Set parent id (siblings have the same parent)
+ $parent_id=$target->$parent_column;
+
+ //only move when scopes are equal
+ if($target->get_scope()==$this->get_scope())
+ {
+ $this->update_parent_id($parent_id);
+
+ return $this->move_sub_tree( $target->$lft_col);
+ }
+ return false;
+
+ }
+
+ /**
+ * Moves the given node to make it the first child of "target"
+ *
+ * @param array $target The node to use as the position marker
+ * @return array $newpos The new left and right values of the node moved
+ */
+ function move_to_first_child_of( $target)
+ {
+ $lft_col = $this->left_column;
+ $parent_column=$this->parent_column;
+
+ //Set parent id (id of the parent, is childs parent id)
+ $parent_id=$target->id;
+
+ //only move when scopes are equal
+ if($target->get_scope()==$this->get_scope())
+ {
+ $this->update_parent_id($parent_id);
+
+ return $this->move_sub_tree( $target->$lft_col+1);
+ }
+ return false;
+ }
+
+ /**
+ * Moves the given node to make it the last child of "target"
+ *
+ * @param array $target The node to use as the position marker
+ * @return array $newpos The new left and right values of the node moved
+ */
+ function move_to_last_child_of($target)
+ {
+ $rgt_col = $this->right_column;
+ $parent_column=$this->parent_column;
+
+ //Set parent id (id of the parent, is childs parent id)
+ $parent_id=$target->id;
+
+ //only move when scopes are equal
+ if($target->get_scope()==$this->get_scope())
+ {
+ $this->update_parent_id($parent_id);
+
+ return $this->move_sub_tree($target->$rgt_col);
+ }
+ return false;
+ }
+
+ /*
+ * *****************
+ * Check methods
+ * *****************
+ */
+ /**
+ * Returns true or false
+ * (in reality, it checks to see if the given left and
+ * right values _appear_ to be valid not necessarily that they _are_ valid)
+ *
+ * @return boolean
+ */
+ function is_valid_node()
+ {
+ $leftcol = $this->left_column;
+ $rightcol = $this->right_column;
+
+ return ($this->$leftcol < $this->$rightcol);
+ }
+
+ /**
+ * Tests whether the given node has an ancestor
+ * (effectively the opposite of isRoot yes|no)
+ *
+ * @return boolean
+ */
+ function has_parent()
+ {
+ return $this->is_valid_node($this->get_parent());
+ }
+
+ /**
+ * Tests whether the given node has a prior sibling or not
+ *
+ * @return boolean
+ */
+ function has_prev_sibling()
+ {
+ return $this->is_valid_node($this->get_prev_sibling());
+ }
+
+ /**
+ * Test to see if node has siblings after itself
+ *
+ * @return boolean
+ */
+ function has_next_sibling()
+ {
+ return $this->is_valid_node($this->get_next_sibling());
+ }
+
+ /**
+ * Test to see if node has children
+ *
+ * @return boolean
+ */
+ function has_descendants()
+ {
+ $leftcol = $this->left_column;
+ $rightcol = $this->right_column;
+ return (($this->$rightcol - $this->$leftcol) > 1);
+ }
+
+ /**
+ * Test to see if the given node is also the root node
+ *
+ * @return boolean
+ */
+ function is_root()
+ {
+ $leftcol = $this->left_column;
+ return ($this->$leftcol == 1);
+ }
+
+ /**
+ * Test to see if the given node is a leaf node (ie has no children)
+ *
+ * @return boolean
+ */
+ function is_leaf()
+ {
+ $leftcol = $this->left_column;
+ $rightcol = $this->right_column;
+ return (($this->$rightcol - $this->$leftcol) == 1);
+ }
+
+ /**
+ * Test to see if the node is a descendant of the given node
+ * @param array $control_node the node to use as the parent or ancestor
+ * @return boolean
+ */
+ function is_descendant_of($control_node)
+ {
+ $leftcol = $this->left_column;
+ $rightcol = $this->right_column;
+
+ return ( ($this->$leftcol > $control_node->$leftcol)
+ and ($this->$rightcol < $control_node->$rightcol)
+ );
+ }
+
+ /**
+ * Test to see if the node is a descendant of the given node
+ * @param array $control_node the node to use as the parent or ancestor
+ * @return boolean
+ */
+ function is_child_of($control_node)
+ {
+ $child_id=$this->id;
+ $parent_id=$control_node->id;
+
+ self::$db->select('count(*) as is_child');
+ self::$db->from($this->table);
+ self::$db->where('id',$child_id);
+ self::$db->where($this->parent_column,$parent_id);
+ self::$db->where($this->scope_column, $this->get_scope());
+
+ $result=self::$db->get();
+
+ if ($row = $result->current())
+ {
+ return $row->is_child > 0;
+ }
+
+ return false;
+
+ }
+
+ /**
+ * Test to determine whether the node is infact also the $control_node (is A === B)
+ * @param array $control_node The node prototype to use for the comparison
+ * @return boolean
+ */
+ function is_equal_to($control_node)
+ {
+ $leftcol = $this->left_column;
+ $rightcol = $this->right_column;
+
+ return (($this->$leftcol==$control_node->$leftcol) and ($this->$rightcol==$control_node->$rightcol));
+ }
+
+ /**
+ * Combination method of is_descendant and is_equal
+ *
+ * @param array $controlNode The node prototype to use for the comparison
+ * @return boolean
+ */
+ function is_descendant_or_equal_to($controlNode)
+ {
+ $leftcol = $this->left_column;
+ $rightcol = $this->right_column;
+
+ return (($this->$leftcol>=$control_node->$leftcol) and ($this->$rightcol<=$control_node->$rightcol));
+ }
+ /*
+ * *****************
+ * Informational methods
+ * *****************
+ */
+ /**
+ * Returns the tree level for the given node (assuming root node is at level 0)
+ *
+ * @return integer The level of the supplied node
+ */
+ function get_level()
+ {
+ $leftval = (int) $this->$lft_col;
+ $rightval = (int) $this->$rgt_col;
+
+ self::$db->select('COUNT(*) AS level');
+ self::$db->from($this->table);
+ self::$db->where($this->left_column.' < '.$leftval);
+ self::$db->where($this->right_column.' < '.$rightval);
+ self::$db->where($this->scope_column, $this->get_scope());
+
+ $query=self::$db->get();
+ if($query->count() > 0) {
+ $result = $query->current();
+ return (int) $result->level;
+ } else {
+ return 0;
+ }
+ }
+ /**
+ * Output number of descendants this node has
+ * @return integer
+ *
+ */
+ function get_number_of_descendants(){
+ $lft_col = $this->left_column;
+ $rgt_col = $this->right_column;
+ // Generate WHERE
+
+ return (int) ( $this->$rgt_col - $this->$lft_col -1)/2;
+
+ }
+ /**
+ * Output number of children of this node
+ * @return integer
+ */
+ function get_number_of_children()
+ {
+
+ self::$db->select('count(*) as num_children');
+ self::$db->from($this->table);
+ self::$db->where($this->parent_column,$this->id);
+ self::$db->where($this->scope_column, $this->get_scope());
+
+ $result=self::$db->get();
+
+ if($row = $result->current())
+ return (int) $row->num_children;
+
+ return -1;
+ }
+ /**
+ * Get current scope of the object
+ * @return integer
+ */
+ function get_scope()
+ {
+ return $this->_scope;
+ }
+ /**
+ * Set scope of current object, retrieved objects calls this in constructor
+ */
+ function set_scope($value)
+ {
+ $this->_scope=$value;
+ return $this;
+ }
+
+
+
+ /* *****************************************
+ * Print methods, more or less debug methods
+ * *****************************************
+ */
+ /**
+ * Debug tree
+ */
+ function debug_tree($tree, $disp_col, $ind = '')
+ {
+ $lft_col = $this->left_column;
+ $rgt_col = $this->right_column;
+ $parent_column=$this->parent_column;
+
+ echo $ind .'#'.$tree->id.' '.$tree->$lft_col.'- '.$tree->$disp_col .' p:'.$tree->$parent_column.' -'.$tree->$rgt_col.'<br>';
+ if(is_array($tree->children))
+ {
+ foreach($tree->children as $child)
+ {
+ $this->debug_tree($child,$disp_col,'....'.$ind);
+ }
+ }
+
+
+ }
+ /**
+ * Will rebuild tree according to the parent_id column
+ * Warning, the order of the tree might not exactly be maintained
+ * Might be slow for big trees
+ * Call this method only with the root_id and its left value.
+ * @return
+ * @param $parent_id Object
+ * @param $left Object
+ */
+ function rebuild_tree($parent_id, $left) {
+ $this->lock_tree();
+ // the right value of this node is the left value + 1
+ $right = $left+1;
+
+ // get all children of this node
+ self::$db->select('id');
+ self::$db->where($this->parent_column, $parent_id);
+ self::$db->where($this->scope_column, $this->get_scope());
+ self::$db->from($this->table);
+ $result=self::$db->get();
+
+ foreach ($result as $row) {
+ // recursive execution of this function for each
+ // child of this node
+ // $right is the current right value, which is
+ // incremented by the rebuild_tree function
+ $right = $this->rebuild_tree($row->id, $right);
+ }
+
+ // we've got the left value, and now that we've processed
+ // the children of this node we also know the right value
+
+ self::$db->set($this->left_column, $left);
+ self::$db->set($this->right_column, $right);
+ self::$db->where('id',$parent_id);
+ self::$db->where($this->scope_column, $this->get_scope());
+ self::$db->update($this->table);
+ // return the right value of this node + 1
+ return $right+1;
+
+ $this->unlock_tree();
+ }
+
+ /*
+ * Protected functions
+ *
+ */
+ /**
+ * check whether child is child of parent (internal)
+ *
+ * @return
+ * @param $child Object
+ * @param $parent Object
+ */
+ protected function _is_descendant_of($child, $parent)
+ {
+ $lft_col = $this->left_column;
+ $rgt_col = $this->right_column;
+
+ return ($child->$lft_col > $parent->$lft_col && $child->$rgt_col < $parent->$rgt_col);
+ }
+
+ /**
+ * The method that performs moving/renumbering operations
+ *
+ * @param array $targetValue Position integer to use as the target
+ * @return array $newpos The new left and right values of the node moved
+ * @access private
+ */
+ protected function move_sub_tree($targetValue)
+ {
+ $leftcol = $this->left_column;
+ $rightcol = $this->right_column;
+
+ $sizeOfTree = $this->$rightcol - $this->$leftcol + 1;
+ $this->modify_node($targetValue, $sizeOfTree);
+
+
+ if($this->$leftcol >= $targetValue)
+ {
+ $this->$leftcol += $sizeOfTree;
+ $this->$rightcol += $sizeOfTree;
+ }
+
+ $newpos = $this->modify_node_range($this->$leftcol, $this->$rightcol, $targetValue - $this->$leftcol);
+
+ $this->modify_node($this->$rightcol+1, - $sizeOfTree);
+
+ if($this->$leftcol <= $targetValue)
+ {
+ $newpos[$this->left_column] -= $sizeOfTree;
+ $newpos[$this->right_column] -= $sizeOfTree;
+ }
+
+ return $newpos;
+ }
+ /**
+ * _modifyNodeRange
+ *
+ * @param $lowerbound integer value of lowerbound of range to move
+ * @param $upperbound integer value of upperbound of range to move
+ * @param $changeVal unsigned integer of change amount
+ * @access private
+ */
+
+ protected function modify_node_range($lowerbound, $upperbound, $changeVal)
+ {
+ $leftcol = $this->left_column;
+ $rightcol = $this->right_column;
+ $table = $this->table;
+ $scope_col = $this->scope_column;
+
+ $sql = "UPDATE $table
+ SET $leftcol = $leftcol + $changeVal
+ WHERE $leftcol >= $lowerbound
+ AND $leftcol <= $upperbound
+ AND ".$this->scope_column.' = '.$this->$scope_col.';';
+
+ self::$db->query($sql);
+
+ $sql = "UPDATE $table
+ SET $rightcol = $rightcol + $changeVal
+ WHERE $rightcol >= $lowerbound
+ AND $rightcol <= $upperbound
+ AND ".$this->scope_column.' = '.$this->$scope_col.';';
+
+ self::$db->query($sql);
+
+ $retArray = array(
+ $this->left_column => $lowerbound+$changeVal,
+ $this->right_column => $upperbound+$changeVal
+ );
+ return $retArray;
+ } // END: Method _modifyNodeRange
+
+ /**
+ * Update the parent id of this record, the ORM class handles
+ * it when the parent id didn't change
+ * @return
+ * @param $node Object
+ * @param $parent_id Object
+ */
+ protected function update_parent_id($parent_id)
+ {
+ $parent_column=$this->parent_column;
+ $this->$parent_column=$parent_id;
+ return $this->save_node();
+ }
+ /**
+ * _modifyNode
+ *
+ * Adds $changeVal to all left and right values that are greater than or
+ * equal to $node_int
+ *
+ * @param $node_int The value to start the shift from
+ * @param $changeVal unsigned integer value for change
+ * @access private
+ */
+ protected function modify_node($node_int, $changeVal)
+ {
+ $leftcol = $this->left_column;
+ $rightcol = $this->right_column;
+ $table = $this->table;
+ $scope_col = $this->scope_column;
+
+ $sql = "UPDATE $table " .
+ "SET $leftcol = $leftcol + $changeVal ".
+ "WHERE $leftcol >= $node_int
+ AND ".$this->scope_column.' = '.$this->$scope_col.';';
+
+ self::$db->query($sql);
+
+ $sql = "UPDATE $table " .
+ "SET $rightcol = $rightcol + $changeVal ".
+ "WHERE $rightcol >= $node_int
+ AND ".$this->scope_column.' = '.$this->$scope_col.';';
+
+ self::$db->query($sql);
+
+ return true;
+ } // END: _modifyNode
+ /**
+ * save_node
+ *
+ * Inserts a new node into the tree, or saves the current one
+ *
+ * @return boolean True/False dependent upon the success of the operation
+ * @access private
+ */
+ protected function save_node( )
+ {
+ if ($this->save()) {
+ // Return true on success
+ return TRUE;
+ }
+
+ return false;
+ }
+}