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.'
'; 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; } }