Location: PHPKode > scripts > HTML dbtree > html-dbtree/class-htmldbtree.php
<?php
/**
* Class for easy management of Mmodified Preordered Tree Traversal data represetation 
* stored in database
* based on a tutorial about Modified Preordered Tree Traversal data model 
* found at http://www.aesthetic-theory.com/learn.php?mptt
* @author Jonathan Gotti <nathan at the-ring dot homelinux dot net>
* @copyleft (l) 2003-2004  Jonathan Gotti
* @package DB tree manipulation
* @license http://opensource.org/licenses/gpl-license.php GNU Public License
* @changelog - 2006-03-08 - HtmlDbTree::make_sibling() take now a third parameter $younger to put the node before instead of after the sibling node.
*                         - new methods: HtmlDbTree::next/prevbrother(), HtmlDbTree::moveup/down() as asked by Bruno Gaspar
*            - 2006-03-07 - now HtmlDbTree::cookmenu() will skip node when the $dataMapping callback return FALSE
*                         - now HtmlDbTree::cookmenu() return the div element too.
*            - 2006-03-06 - new method HtmlDbTree::cookmenu() for jscookmenu (by Heng Yuan) var generation (requested by Marco Valk <hide@address.com>)
*                           (see http://www.cs.ucla.edu/~heng/JSCookMenu/index.html for more detail on jscookmenu)
*                         - HtmlDbTree::get_childs() now use the cached datas by default instead to query the database
*                                                    added third parameter $fromdb to retrieve datas from database
*            - 2006-02-27 - remove some more error_notice 
*            - 2006-02-10 - bug correction in get_tree method when left_id and right_id are named in an other way. 
*                           Thx to Azlan Muhamad Sufian for point it out <azlanms1 at tnb dot com dot my>
*            - 2005-07-27 - add some SQL injection protection by setting some types or add some slashes
*            - 2005-06-17 - bug correction in the get_path method
*            - 2005-06-14 - get_path() method improvement commit by Lasse Lofquist <lass_phpclass at tvartom.com>
*            - 2005-02-28 - modify the remove method to use the new method optimize of both mysqldb and sqlitedb
*            - 2005-02-25 - add forgotten support for sqlitedb
*                         - now remove can return the full removed tree structure
*            - 2004-11-26 --
* @require class-mysqldb.php or class-sqlitedb
*/

class HtmlDbTree{
  /** we'll store the data in this */
  var $datas;
  /** our database object */
  var $db;
  /** the database table name */
  var $tablename;
  /** contain the corresponding fields name for the table */
  var $fields;
  /** the defaults fields name */
  var $default_fields;
  /** you can assign a SQL query string of your own for tree retrieving by seting this param */
  var $Q_STR = null;
  /**
  * create a tree object from a database table storing Modified Preordered Tree Traversal datas
  * @param mysqldb $db a mysqldb object (a sqlitedb could be used too)
  * @param strings $tablename
  * @param array $fields array('right_id'=>'','left_id'=>'','level'=>'','id'=>'','parent'=>'') map the columns names
  * @uses mysqldb, sqlitedb
  */
  function HtmlDbTree($db=null,$tablename,$fields=null,$autoload=TRUE,$Q_STR=null){
    if(! ((is_a($db,'mysqldb') && $db->check_conn('active')) || (is_a($db,'sqlitedb') && $db->db!==null)) )
      return null;
    $this->db = $db;
    $this->tablename = $tablename;
    # ADDON FOR JS COOKMENU 
    if( isset($fields['icon']) || isset($fields['desc']) || isset($fields['url']) || isset($fields['target']) || isset($fields['label'])){
      $this->default_fields = array('right_id','left_id','level','id','parent','icon','url','target','desc','label');
    }else{
      $this->default_fields = array('right_id','left_id','level','id','parent');
    }
    $this->set_fields($fields);
    $this->Q_STR = $Q_STR;
    $this->ordrby = ' ORDER BY '.$this->fields['left_id'].' ASC'; //I use this one to set my optionnals group by
    if($autoload)
      $this->load_tree();
  }
  /**
  * set the fieldname in database corresponding to the default ones
  * @param string $field the field to retrieve in (right_id,left_id,level,id)
  * @param string $field_db the corresponding name of the table column
  */
  function set_field($field,$field_db){
    if(! in_array($field,$this->default_fields))
      return FALSE;
    $this->fields[$field] = addslashes($field_db);
    return TRUE;
  }
  /**
  * set all fields name at once from an indexed array where 
  * key are right_id, left_id, level ,id and the values are the corresponding column name
  * @param array $fields array('right_id'=>'','left_id'=>'','level'=>'','id'=>'') if null will reset to default values
  * @return true
  */
  function set_fields($fields=null){
    foreach($this->default_fields as $fld) # reset first
      $this->fields[$fld] = $fld;
    if(! is_array($fields))
      return TRUE;
    foreach($fields as $k=>$v) # and set 
      $this->set_field($k,$v);
    return TRUE;
  }
  /**
  * rebuild the tree (right_id,left_id,and level) from the parent column
  */
  # function rebuild_tree($parent_col,$parent_id=0,$left=1,$level=0){
  function rebuild_tree($parent_id=0,$left=1,$level=-1){
    // the right value of this node is the left value + 1
    settype($parent_id,'int');
    $right = $left+1;
    $level += 1;
    $items = $this->db->select_to_array( $this->tablename,$this->fields['id'],
                                "WHERE ".$this->fields['parent']." = '$parent_id' ORDER by ".$this->fields['left_id'].' ASC');
    if($items)
      foreach($items as $item)
        $right = $this->rebuild_tree($item[$this->fields['id']],$right,$level);
    
    $this->db->update($this->tablename,array($this->fields['left_id']=>$left,
                                             $this->fields['right_id']=>$right,
                                             $this->fields['level']=>$level,)
                      ,"WHERE ".$this->fields['id'] ." = '$parent_id'");           
    return $right+1;
    $this->load_tree();
  }
  /**
  * @uses get_tree
  */
  function load_tree(){
    $this->datas = $this->get_tree();
  }
  
  /**
  * this will return the tree structure.
  * @param int $startnode this will be an integer or false,
  *        if its an integer we will get all children,
  *        if its false we will get the entire tree
  * @param int $depth: this will be an integer, or false
  *        represents the number of levels to dig for data.
  * @param bool $inc_startnode: whether or not to include parent in result set
  * @return array
  */
  function get_tree($startnode=FALSE,$depth=FALSE,$inc_startnode=TRUE){
    if($startnode){
      settype($startnode,'int');
      $parent_ = $this->db->select_single_to_array($this->tablename,$this->fields,"WHERE ".$this->fields['id']." ='$startnode'");
      $inc_startnode = $inc_startnode?'=':'';
      $where = " WHERE ".$this->fields['left_id']." >$inc_startnode '".$parent_[$this->fields['left_id']]."' AND "
              .$this->fields['right_id']." <$inc_startnode '".$parent_[$this->fields['right_id']]."'";
    }
    if($depth){
      settype($depth,'int');
      $nest_limit = isset($parent_)?$parent_['level']+$depth:$depth;
      $level_adjustment = $nest_limit - $depth; // we'll use this later
      $where = (isset($where)?$where.' AND ':' WHERE ').$this->fields['level']." <= '$nest_limit'";
    }else{
      $level_adjustment = (isset($parent_)?$parent_['level']-1:0);
      # echo "ADJ $level_adjustment";
    }
    $where = (isset($where)?"$where ":'').$this->ordrby;
    if(!$this->Q_STR){
      $items = $this->db->select2associative_array($this->tablename,'*',$where,$this->fields['id'],null,TRUE);
    }else{
      //find where to put the where clause
      if($res = $this->db->query_to_array($this->Q_STR.$where))
        $items = $this->db->associative_array_from_q2a_res($this->fields['id'],null,$res,TRUE);
    }
    if( ! (isset($items) && is_array($items)) )
      return null;
    foreach($items as $id=>$item){
      foreach($this->default_fields as $fld)
        $items[$id][$fld] = $item[$this->fields[$fld]];
      $items[$id]['relative_level']     = $item[$this->fields['level']] - $level_adjustment;
      $items[$id]['number_of_children'] = ($item[$this->fields['right_id']] - $item[$this->fields['left_id']] - 1)/2;
    }
    return $items;
  }
  /**
  * get html option for select form input in a really simple way
  * @param string $show_fld the column name used as option's labels
  * @param string $val_fld the column name used as option's values
  * @param int $selected_value the optionnal selected option's value
  * @param int | array $removednode the nodes ids to remove
  * @uses HtmlDbTree::get_tree
  * @return string or FALSE
  */
  function html_get_options($show_fld,$val_fld='id',$selected_value=null,$removednode=null,$startnode=FALSE,$depth=FALSE,$inc_startnode=TRUE){
    if(!$items = $this->get_tree($startnode,$depth,$inc_startnode))
      return FALSE;
    foreach($items as $item){
      if($removednode){
        if(is_array($removednode)){
          if( in_array($item[$val_fld],$removednode))
            continue;
          foreach($removednode as $node){
            if($this->is_child($item['id'],$node))
              continue 2;
          }
        }elseif(($removednode==$item[$val_fld]) || $this->is_child($item['id'], $removednode)){
          continue;
        }
      }
      @$str .= '    <option value="'.$item[$val_fld].'"'
              .($item[$val_fld]==$selected_value?' selected="selected"':'').'>'
              .str_repeat('&nbsp;',max(0,2*($item['relative_level']-1)) ).$item[$show_fld]."</option>\n";
    }
    return $str;
  }
  
  /**
  * return the jscookmenu code from the currently loaded tree structure
  * @see http://www.cs.ucla.edu/~heng/JSCookMenu/index.html
  * @param string $jsname the js export name
  * @param int $rootparentnode Parent of first level items or false to render the complete tree
  * @param int $depth which depth to dig to construct the menu
  * @param mixed $dataMapping string or array callback for data mapping (see sample.php)
  * @return string generated jscookmenu code 
  */
  function cookmenu($jsname,$rootparentnode=FALSE,$depth=FALSE,$dataMapping=null){
    if(! $items = $this->get_childs($rootparentnode,$depth))
      return FALSE;
    # return $this->_cookaddnode(1,1,1 );
    $jsStr = "<DIV ID=\"$jsname\"></DIV>\n  <SCRIPT LANGUAGE=\"JavaScript\"><!--\n    var $jsname = [";
    
    $openednode=array();
    
    foreach($items as $id=>$item){
      if($dataMapping){
        if(! $item = call_user_func($dataMapping,$item) ){
          foreach($openednode as $oid=>$ochildsnb){ # close opened node as needed
            if( --$openednode[$oid] < 1 ){
              unset($openednode[$oid]);
              $jsStr .= str_repeat("\t",$this->datas[$oid]['level'])."],\n";
            }
          }
          continue;
        }
      }
      # is this a leef or a folder?
      if( $item['number_of_children'] > 0 ){
        $openednode[$id] = $item['number_of_children']+1;
        $jsStr .= str_repeat("\t",$item['level'])."['$item[icon]', '$item[label]', '$item[url]', '$item[target]', '$item[desc]',  // a folder item\n";
      }else{ # this is a leaf
        if( strtolower($item['label']) == 'splitter' )
          $jsStr .= str_repeat("\t",$item['level'])."_cmSplit,\n";
        else
          $jsStr .= str_repeat("\t",$item['level'])."['$item[icon]', '$item[label]', '$item[url]', '$item[target]', '$item[desc]'],  // a menu item\n";
      }
      # close opened node as needed
      foreach($openednode as $oid=>$ochildsnb){
        if( --$openednode[$oid] < 1 ){
          unset($openednode[$oid]);
          $jsStr .= str_repeat("\t",$this->datas[$oid]['level'])."],\n";
        }
      }
    }
    $jsStr .= '];
    --></SCRIPT>
    ';

    return $jsStr;
  }
  /**
  * return a subset of links to get the path of previous node
  * @param str linkexp is an expression with fields replacement 
  *         ie: "index.php?cat=%=id=%" where %=id=% will be replace by the value in the id field
  *         so this could be id, left_id,right_id, level, and so on
  * @param str $labelfield is the db field name to use as link label
  * @param int $item
  * @param bool $keep_item set it to false if you don't want to exclude the current item of the returned path
  * @param str  $stringglue string use as link separator
  * @return string or FALSE
  */
  function html_get_path($linkexp,$labelfield,$item,$keep_item=TRUE,$stringglue=' / '){
    if(! $paths = $this->get_path($item,$keep_item))
      return FALSE;
    preg_match_all('!%=([^\s=]+)=%!',$linkexp,$m);
    foreach($paths as $path){
      $url = $linkexp;
      if(count($m[1])){
        foreach($m[1] as $tag)
          $url =str_replace("%=$tag=%",$path[$tag],$url);
      }
      $str[] ='<a href="'.$url.'">'.$path[$labelfield].'</a>';
    }
    return implode($stringglue,$str);
  }
  /**
  * check if $child is a descendant of $parent
  * @param int $child
  * @param int $parent
  * @return bool
  */
  function is_child($child, $parent){
    if(! (isset($this->datas[$parent]) && isset($this->datas[$child])) )
      return FALSE;
    $ret = (bool)(($this->datas[$parent]['left_id'] < $this->datas[$child]['left_id']) &&
    ($this->datas[$parent]['right_id'] > $this->datas[$child]['right_id']));
    return $ret;
  }
  /**
  * return parents node of this item
  * @param int $item the item to retrieve path to
  * @param bool $keep_item include item in path or not
  * @param bool $dont_use_parent the default way this method work is to use the parent propertie of a node
  *                              (thx Lasse Lofquist <lass_phpclass at tvartom.com>) if set to true then it 
  *                             will use the is_child method instead (use it when there's no parent id or when you don't trust them)
  *                             and so will only trust the right and left ids of nodes
  * @return array indexed by level 
  */
  function get_path($item,$keep_item=TRUE,$onlyids=FALSE,$dont_use_parent=FALSE){
    if(! isset($this->datas[$item]) ) return FALSE;
    if(! $dont_use_parent){ # really better way to do the same using parent ids by Lasse Lofquist
      if($keep_item)
        $path_array[$this->datas[$item]['level']] = ($onlyids ? $item : $this->datas[$item]);
      while ($item = $this->datas[$item]['parent'])
        $path_array[$this->datas[$item]['level']] = ($onlyids ? $item : $this->datas[$item]);
      return(isset($path_array)?array_reverse($path_array, true):null);
    }else{ # old fashion way that don't use the parent id but slower 
      foreach($this->datas as $item_id => $item_data){
        if($this->is_child($item, $item_id)){
          $path_array[$item_data['level']] = ($onlyids?$item_id:$item_data);
          # we have all the data we need for path
          if($item_data['relative_level'] == ($this->datas[$item]['relative_level']-1))
            break;
        }
      }
      if( $keep_item )
        $path_array[$this->datas[$item]['level']] = ($onlyids?$item:$this->datas[$item]);
      return isset($path_array)?$path_array:null;
    }
  }
  /**
  * return the extended path to the node with all ancestors expanded
  * the principle is pretty simple we take all nodes and remove nodes that aren't direct child of ancestors.
  * the first level nodes will always be displayed
  * @param int $item
  */
  function get_extended_path($item=0,$withchild=TRUE,$onlyids=FALSE){
    if(! $this->datas) return FALSE;
    if( $item==0 || ($this->datas[$item]['level'] < 1) || (! is_array($ancestors = $this->get_path($item))) ) # first level node so return only the first level nodes
      return $this->get_tree(null,1);
    $items = $this->datas;
    $curlevel = $this->datas[$item]['level'];
    # removed unnecessary nodes
    foreach($items as $id=>$item){
      $ids[$id] = $id;
      if($item['level']==1)continue; # first level nodes have to be keeped
      # remove child with too high level 
      if($item['level']>$curlevel && !$withchild){ 
        unset($items[$id],$ids[$id]);continue;
      }elseif($item['level']>$curlevel+1){
        unset($items[$id],$ids[$id]);continue;
      }
      # keep brothers of ancestors nodes
      if($this->is_child($id,$ancestors[$item['level']-1]['id']) ) continue;
      # all other childs have to be removed
      unset($items[$id],$ids[$id]);
    }
    return ($onlyids?$ids:$items);
  }
  /**
  * reparent the given child as a $newparent child
  * @param int $item the item id
  * @param int the new parent id
  * @param str $parent_col name of the parent column name in database
  */
  function reparent($item,$newparent=0){
    if($this->is_child($newparent,$item)) # daddy couldn't be a child
      return FALSE;
    settype($item,'int');
    $this->db->update($this->tablename,array($this->fields['parent']=>$newparent),"WHERE `".$this->fields['id']."` = '$item'");
    $this->rebuild_tree();
    return $this->load_tree();
  }
  /**
  * move the given node to be the direct brother of the $sibling node
  * @param int $item
  * @param int $sibling
	* @parama bool $younger if set to true then will be move before the sibling instead of next
  */
  function make_sibling($item,$sibling=0,$younger=FALSE){
		# this method take advantage of the fact that the rebuild_tree method rely on parent and left_id fields to work 
		# I admit this is a sort of hack but is relatively easy to understand and is just quicker for me to write so....
    settype($item,'int');
    if(! $this->db->update($this->tablename,
                            array(  $this->fields['parent']=>(int) @$this->datas[$sibling][$this->fields['parent']],
                                    'left_id'=>(int) ($younger?@$this->datas[$sibling]['left_id']-1:@$this->datas[$sibling]['right_id'])),
														"WHERE ".$this->fields['id']." = '$item' ") )
      return FALSE;
    $this->rebuild_tree();    
    return $this->load_tree();
  }
  
  /**
  * @param int $item node to query for childs false for first level nodes
  * @param int $depth leave false for no depth constraints
  * @param bool $fromdb set this to true if you want to get the datas from the database instead of the cached datas.
  */
  function get_childs($item,$depth=FALSE,$fromdb=FALSE){
    if( $fromdb ) # return from database so no more work
      return $this->get_tree($item,$depth,FALSE);
    
    # return from cached datas
    if( $item!==FALSE && ! isset($this->datas[$item]) ) 
      return FALSE;
    if( $item===FALSE ){
      $maxlvl = $depth;
    }else{
      $maxlvl = $depth? $this->datas[$item]['level']+$depth : FALSE ;
    }
    foreach($this->datas as $id=>$data){
      if( $item!==FALSE && ! $this->is_child($id,$item) ) continue; # check is child
      if($maxlvl && $data['level'] > $maxlvl) continue; # check depth
      # prepare result
      $res[$id] = $data;
    }
    
    return isset($res)?$res:FALSE;
  }
  /**
  * remove an item from the database and re-index left and right ids.
  * @param int $item id 
  */
  function remove($item,$return_removed_tree=FALSE){
    $this->load_tree(); # reload the entire tree before processing anything
    settype($item,'int');
    if($return_removed_tree)
      $return_removed_tree = $this->get_tree($item);
    $where = "WHERE ".$this->fields['right_id']." <= '".@$this->datas[$item]['right_id'] ."' AND "
                      .$this->fields['left_id']." >= '".@$this->datas[$item]['left_id']."'";
    
    if(! $this->db->delete($this->tablename,$where))
      return FALSE;
    /* lets fix some right ids. this sets all right ids to the number of children * 2 (recognize the formula?)
    We use 2 because each node is worth 2 in our table, the 1 right id and 1 left id */
    $delta = $this->datas[$item]['right_id'] - $this->datas[$item]['left_id'] + 1;
    $upstr =  $this->fields['right_id'].' = '.$this->fields['right_id']." - $delta";
    $upstr2=  $this->fields['left_id'].' = '.$this->fields['left_id']." - $delta";
    $this->db->update($this->tablename,$upstr,' WHERE '.$this->fields['right_id'].' > \''.$this->datas[$item]['right_id']."'");
    $this->db->update($this->tablename,$upstr2,' WHERE '.$this->fields['left_id'].' > \''.$this->datas[$item]['left_id']."'");
    // optimize the table to remove overhead
    $this->db->optimize($this->tablename);
    $this->load_tree(); # reload the tree to ensure correlation with the stored datas
    if($return_removed_tree)
      return $return_removed_tree;
    return TRUE;
  }
  /**
  * add - adds an element from the SQL tree You must call load_tree after this one
  * @Public
  * @param  array   $info Array   Other coloumns
  * @param  Integer/false  $parent  Parent ID (false = root)
  * @return  true or the node id if autoincrement on id field is enabled
  **/
  function add($info,$parent=0){
    // does the object have a daddy?
    if($parent > 0){
      # WE HAVE A PARENT SO: 
      # level = daddy level + 1
      $info['level'] = $this->datas[$parent]['level']+1;
      # check for brothers
      if($this->datas[$parent]['number_of_children']){ # WE HAVE BROTHERS 
        # get the last daddy's child (the one with the highest left_id)
        $brother_info = $this->db->select_single_to_array($this->tablename,
                  $this->fields['right_id'].', '.$this->fields['left_id'].', '.$this->fields['id'],
                  'WHERE '.$this->fields['level'].' = '.($this->datas[$parent]['level']+1) 
                          .' AND '.$this->fields['left_id'].' >= '.$this->datas[$parent]['left_id']
                          .' AND '.$this->fields['right_id'].' <= '.$this->datas[$parent]['right_id'] 
                          .' ORDER BY '.$this->fields['left_id'].' DESC');
        # left_id = youngest brother right_id + 1
        # right_id = left_id + 1
        $info['left_id']  = $brother_info[$this->fields['right_id']] + 1;
        $info['right_id'] = $info['left_id'] + 1;
        # all above will be update
        $update_past = $brother_info[$this->fields['right_id']];
      }else{ # WE ARE UNIQUE CHILD
        #update all greater than daddy's left ID
        $update_past =  $this->datas[$parent]['left_id'];
        $info['left_id'] =  $this->datas[$parent]['left_id'] + 1; # no brother so left_id = daddy's left_id +1 
        $info['right_id'] = $info['left_id'] + 1 ;# right_id = left_id +1
      }
      # UPDATE ALL NODES IDS +2 ABOVE THE NEW ONE
      $this->db->update($this->tablename,$this->fields['left_id'].' = '.$this->fields['left_id'].' + 2',
                                          'WHERE '.$this->fields['left_id'].' > ' . $update_past);
      $this->db->update($this->tablename,$this->fields['right_id'].' = '.$this->fields['right_id'].' + 2',
                                          'WHERE '.$this->fields['right_id'].' > ' . $update_past);
    }else{
      # WE ARE ROOT NODE
      if(is_array($this->datas)){
        $rarray = array_reverse($this->datas);
        // his left id is one past the highest right id, which we can get with this formula
        $info['left_id'] = $rarray[0]['right_id'] + $rarray[0]['level'];
      }else{ # first node of the tree
        $info['left_id'] = 1;
        $info['level'] = $info['left_id'];
      }
      $info['right_id'] = $info['left_id'] + 1;
      $info['level'] = 1;
    }
    # remap infarray
    $info['parent'] = $parent;
    foreach($this->default_fields as $fld){
      if($this->fields[$fld]!=$fld){
        $info[$this->fields[$fld]]=@$info[$fld];
        unset($info[$fld]);
      }
    }
    #INSERT NEW NODE
    return $this->db->insert($this->tablename,$info);
  }
  /**
  * add a new node as a younger or older  brother of the $brother node
  * @param array $info
  * @param  array   $info Array   Other columns
  * @param  Integer/false  $brother brother ID (false = root)
  * @return bool 
  */
  function add_as_brother($info,$brother=0,$younger=TRUE){
    $info['level'] = $this->datas[$brother]['level'];
    # left_id = brother right_id + 1
    # right_id = left_id + 1
    if($younger)
      $info['left_id']  = ($update_past = $this->datas[$brother][$this->fields['right_id']])+ 1;
    else
      $info['left_id']  = ($update_past = $this->datas[$brother][$this->fields['left_id']]);
    $info['right_id'] = $info['left_id'] + 1;
    
    # UPDATE ALL NODES IDS +2 ABOVE THE NEW ONE
    $this->db->update($this->tablename,$this->fields['left_id'].' = '.$this->fields['left_id'].' + 2',
                                      'WHERE left_id >'.($younger==FALSE?'=':'').' ' . $update_past);
    $this->db->update($this->tablename,$this->fields['right_id'].' = '.$this->fields['right_id'].' + 2',
                                      'WHERE right_id > ' . $update_past);
    # remap infarray
    $info['parent'] = $this->datas[$brother]['parent'];
    foreach($this->default_fields as $fld){
      if($this->fields[$fld]!=$fld){
        $info[$this->fields[$fld]]=$info[$fld];
        unset($info[$fld]);
      }
    }
    # INSERT NEW NODE
    return $this->db->insert($this->tablename,$info);
  }
  
	/**
	* move up a node (keep the same parent).
	* if the node have any higher/older brother then it will make it higher in the hierarchie, 
	* return false if it has no more higher/older brother (so it's the first child of his parent) 
	* @param int $item the node id you want to move
	*/
	function moveup($item){
		if(! $prevbrother = $this->prevbrother($item) )
			return FALSE;
		return $this->make_sibling($item,$prevbrother,TRUE);
	}
	/**
	* move down a node (keep the same parent).
	* if the node have any lower/younger brother then it will make it lower in the hierarchie, 
	* return FALSE if it has no more lower/younger brother (mean it's the last child of his parent) 
	* @param int $item the node id you want to move
	*/
	function movedown($item){
		if(! $nextbrother = $this->nextbrother($item) )
			return FALSE;
		$this->make_sibling($item,$nextbrother);
	}
	/**
	* return the next node of the same level or false if not exists
	*/
	function nextbrother($item){
		if(! isset($this->datas[$item]) ) return FALSE;
		reset($this->datas);
		
		# go to the given node
		$node = next($this->datas);
		while($node != $this->datas[$item])
			$node = next($this->datas);

		# then seek for next brother
		while($nextbrother = next($this->datas) ){
			if($nextbrother['parent'] != $this->datas[$item]['parent'])
				continue;
			else
				return $nextbrother['id'];
		}
		return FALSE;
	}
	/**
	* return the previous brother id (same parent, same level)
	*/
	function prevbrother($item){
		if(! isset($this->datas[$item]) ) return FALSE;
		reset($this->datas);
		$p = $this->datas[$item]['parent'];
		
		while($prev = next($this->datas)){
			if($prev['id'] == $item){ # we haven't found any prev brother so return
				return isset($prevbrother)?$prevbrother['id']:FALSE;
			}
			if($prev['parent'] != $p ){
				continue;
			}else{
				$prevbrother = $prev;
			}
		}
		return FALSE;
	}
}

?>
Return current item: HTML dbtree