Source for file MDB2nested.php
Documentation is available at MDB2nested.php
/* vim: set expandtab tabstop=4 shiftwidth=4: */
// +----------------------------------------------------------------------+
// +----------------------------------------------------------------------+
// | Copyright (c) 1997-2004 The PHP Group |
// +----------------------------------------------------------------------+
// | This source file is subject to version 2.02 of the PHP license, |
// | that is bundled with this package in the file LICENSE, and is |
// | available at through the world-wide-web at |
// | http://www.php.net/license/2_02.txt. |
// | If you did not receive a copy of the PHP license and are unable to |
// | obtain it through the world-wide-web, please send a note to |
// | license@php.net so we can mail you a copy immediately. |
// +----------------------------------------------------------------------+
// +----------------------------------------------------------------------+
// $Id: MDB2nested.php 320703 2011-12-08 22:08:40Z danielc $
require_once 'Tree/OptionsMDB2.php';
* This class implements methods to work on a tree saved using the nested
* explaination: http://research.calacademy.org/taf/proceedings/ballew/index.htm
// FIXXME to be implemented
// add on for the where clause, this string is simply added
// behind the WHERE in the select so you better make sure
// its correct SQL :-), i.e. 'uid=3'
// this is needed i.e. when you are saving many trees in one db-table
// since the internal names are fixed, to be portable between different
// DB tables with different column namings, we map the internal name
// to the real column name using this array here, if it stays empty
// the internal names are used, which are:
// since mysql at least doesnt support 'left' ...
// ...as a column name we set default to the first
// needed for sorting the tree, currently only used in Memory_DBnested
// the defined methods here are proposals for the implementation,
// they are named the same, as the methods in the "Memory" branch.
// If possible it would be cool to keep the same naming. And
// if the same parameters would be possible too then it would be
// even better, so one could easily change from any kind
// of tree-implementation to another, without changing the source
// code, only the setupXXX would need to be changed
* @param string the DSN for the DB connection
// {{{ Tree_Dynamic_DBnested()
* @author Wolfram Kriesing <wolfram@kriesing.de>
* @param string the DSN for the DB connection
* add a new element to the tree
* there are three ways to use this method
* Give only the $parentId and the $newValues will be inserted
* as the first child of this parent
* // insert a new element under the parent with the ID=7
* $tree->add(array('name'=>'new element name'), 7);
* Give the $prevId ($parentId will be dismissed) and the new element
* will be inserted in the tree after the element with the ID=$prevId
* the parentId is not necessary because the prevId defines exactly where
* the new element has to be place in the tree, and the parent is
* the same as for the element with the ID=$prevId
* // insert a new element after the element with the ID=5
* $tree->add(array('name'=>'new'), 0, 5);
* neither $parentId nor prevId is given, then the root element will be
* inserted. This requires that programmer is responsible to confirm this.
* This method does not yet check if there is already a root element saved!
* @param array $newValues this array contains the values that shall
* be inserted in the db-table
* @param integer $parentId the id of the element which shall be
* the parent of the new element
* @param integer $prevId the id of the element which shall preceed
* the one to be inserted use either
* 'parentId' or 'prevId'.
* @return integer the ID of the element that had been inserted
function add($newValues, $parentId = 0 , $prevId = 0 )
$lName = $this->_getColName ('left');
$rName = $this->_getColName ('right');
// check the DB-table if the columns which are given as keys
// in the array $newValues do really exist, if not remove them
// FIXXME do the above described
// if no parent and no prevId is given the root shall be added
if ($parentId || $prevId) {
// we also need the parent id of the element
$parentId = $element['parentId'];
$newValues['parentId'] = $parentId;
// get the "visited"-value where to add the new element behind
// if $prevId is given, we need to use the right-value
// if only the $parentId is given we need to use the left-value
// look at it graphically, that made me understand it :-)
// http://research.calacademy.org/taf/proceedings/ballew/sld034.htm
$prevVisited = $prevId ? $element['right'] : $element['left'];
// FIXXME start transaction here
if (Tree::isError($err = $this->_add ($prevVisited, 1 ))) {
//$this->dbh->rollback();
// inserting _one_ new element in the tree
// quote the values, as needed for the insert
foreach ($newValues as $key => $value) {
///////////FIX ME: Add proper quote handling
$newData[$this->_getColName ($key)] = $this->dbh->quote ($value, 'text');
// set the proper right and left values
$newData[$lName] = $prevVisited + 1;
$newData[$rName] = $prevVisited + 2;
// use sequences to create a new id in the db-table
$nextId = $this->dbh->nextId ($this->table);
$query = sprintf('INSERT INTO %s (%s,%s) VALUES (%s,%s)',
$this->_getColName ('id'),
$this->dbh->quote ($nextId, 'integer'),
if (MDB2 ::isError ($res = $this->dbh->exec ($query))) {
return $this->_throwError ($res->getMessage (), __LINE__ );
* this method only updates the left/right values of all the
* elements that are affected by the insertion
* be sure to set the parentId of the element(s) you insert
* @param int this parameter is not the ID!!!
* it is the previous visit number, that means
* if you are inserting a child, you need to use the left-value
* if you are inserting a "next" element, on the same level
* you need to give the right value !!
* @param int the number of elements you plan to insert
* @return mixed either true on success or a Tree_Error on failure
function _add ($prevVisited, $numberOfElements = 1 )
$lName = $this->_getColName ('left');
$rName = $this->_getColName ('right');
// update the elements which will be affected by the new insert
$query = sprintf('UPDATE %s SET %s=%s+%s WHERE%s %s>%s',
if (MDB2 ::isError ($res = $this->dbh->exec ($query))) {
return $this->_throwError ($res->getMessage (), __LINE__ );
$query = sprintf('UPDATE %s SET %s=%s+%s WHERE%s %s>%s',
if (MDB2 ::isError ($res = $this->dbh->exec ($query))) {
return $this->_throwError ($res->getMessage (), __LINE__ );
* this automatically remove all children and their children
* if a node shall be removed that has children
* @param integer $id the id of the element to be removed
* @return boolean returns either true or throws an error
// FIXXME start transaction
//$this->dbh->autoCommit(false);
$query = sprintf( 'DELETE FROM %s WHERE%s %s BETWEEN %s AND %s',
$this->_getColName ('left'),
$element['left'],$element['right']);
if (MDB2 ::isError ($res = $this->dbh->exec ($query))) {
//$this->dbh->rollback();
return $this->_throwError ($res->getMessage (), __LINE__ );
//$this->dbh->rollback();
* removes a tree element, but only updates the left/right values
* to make it seem as if the given element would not exist anymore
* it doesnt remove the row(s) in the db itself!
* @param array the entire element returned by "getElement"
* @return boolean returns either true or throws an error
function _remove ($element)
$delta = $element['right'] - $element['left'] + 1;
$lName = $this->_getColName ('left');
$rName = $this->_getColName ('right');
// update the elements which will be affected by the remove
$lName, $element['left']);
if (MDB2 ::isError ($res = $this->dbh->exec ($query))) {
// the rollback shall be done by the method calling this one
// since it is only private we can do that
return $this->_throwError ($res->getMessage (), __LINE__ );
$lName, $element['left'],
$rName, $element['right']);
if (MDB2 ::isError ($res = $this->dbh->exec ($query))) {
// the rollback shall be done by the method calling this one
// since it is only private
return $this->_throwError ($res->getMessage (), __LINE__ );
// should that not also be done in the method calling this one?
// like when an error occurs?
* move an entry under a given parent or behind a given entry.
* If a newPrevId is given the newParentId is dismissed!
* call it either like this:
* to move the element (or entire tree) with the id x
* under the element with the id y
* $tree->move(x, 0, y); // ommit the second parameter by setting
* to move the element (or entire tree) with the id x
* behind the element with the id y
* $tree->move(array(x1,x2,x3), ...
* the first parameter can also be an array of elements that shall
* be moved. the second and third para can be as described above.
* If you are using the Memory_DBnested then this method would be invain,
* since Memory.php already does the looping through multiple elements.
* But if Dynamic_DBnested is used we need to do the looping here
* @param integer the id(s) of the element(s) that shall be moved
* @param integer the id of the element which will be the new parent
* @param integer if prevId is given the element with the id idToMove
* shall be moved _behind_ the element with id=prevId
* if it is 0 it will be put at the beginning
* @return mixed true for success, Tree_Error on failure
function move($idsToMove, $newParentId, $newPrevId = 0 )
foreach ($idsToMove as $idToMove) {
$ret = $this->_move($idToMove, $newParentId, $newPrevId);
// FIXXME the error in a nicer way, or even better
// let the throwError method do it!!!
return $this->_throwError (serialize($errors), __LINE__ );
* this method moves one tree element
* @param integer the id of the element that shall be moved
* @param integer the id of the element which will be the new parent
* @param integer if prevId is given the element with the id idToMove
* shall be moved _behind_ the element with id=prevId
* if it is 0 it will be put at the beginning
* @return mixed true for success, Tree_Error on failure
function _move($idToMove, $newParentId, $newPrevId = 0 )
// do some integrity checks first
// dont let people move an element behind itself, tell it
// succeeded, since it already is there :-)
if ($newPrevId == $idToMove) {
$newParentId = $newPrevious['parentId'];
return $this->_throwError ('no parent id given', __LINE__ );
// if the element shall be moved under one of its children
if ($this->isChildOf($idToMove,$newParentId)) {
return $this->_throwError (
'can not move an element under one of its children',
// dont do anything to let an element be moved under itself
if ($newParentId== $idToMove) {
// try to retreive the data of the parent element
// get the data of the element itself
$numberOfElements = ($element['right'] - $element['left'] + 1 ) / 2;
$prevVisited = $newPrevId ? $newPrevious['right'] : $newParent['left'];
// FIXXME start transaction
// add the left/right values in the new parent, to have the space
// to move the new values in
$err= $this->_add ($prevVisited, $numberOfElements);
//$this->dbh->rollback();
// update the parentId of the element with $idToMove
$err = $this->update($idToMove, array ('parentId' => $newParentId));
//$this->dbh->rollback();
// update the lefts and rights of those elements that shall be moved
// first get the offset we need to add to the left/right values
// if $newPrevId is given we need to get the right value,
// otherwise the left since the left/right has changed
// because we already updated it up there. We need to get them again.
// We have to do that anyway, to have the proper new left/right values
//$this->dbh->rollback();
$calcWith = $temp['right'];
//$this->dbh->rollback();
$calcWith = $temp['left'];
// get the element that shall be moved again, since the left and
// right might have changed by the add-call
// calc the offset that the element to move has
// to the spot where it should go
$offset = $calcWith - $element['left'];
// correct the offset by one, since it needs to go inbetween!
$lName = $this->_getColName ('left');
$rName = $this->_getColName ('right');
$lName, $element['left']-1 ,
$rName, $element['right']+1 );
if (MDB2 ::isError ($res = $this->dbh->exec ($query))) {
//$this->dbh->rollback();
return $this->_throwError ($res->getMessage (), __LINE__ );
// remove the part of the tree where the element(s) was/were before
//$this->dbh->rollback();
// FIXXME commit all changes
* update the tree element given by $id with the values in $newValues
* @param int the id of the element to update
* @param array the new values, the index is the col name
* @return mixed either true or an Tree_Error
function update($id, $newValues)
// just to be sure nothing gets screwed up :-)
unset ($newValues[$this->_getColName ('left')]);
unset ($newValues[$this->_getColName ('right')]);
unset ($newValues[$this->_getColName ('parentId')]);
// updating _one_ element in the tree
foreach ($newValues as $key=> $value) {
///////////FIX ME: Add proper quote handling
$values[] = $this->_getColName ($key). '='. $this->dbh->quote ($value, 'text');
$query = sprintf('UPDATE %s SET %s WHERE%s %s=%s',
$this->_getColName ('id'),
if (MDB2 ::isError ($res = $this->dbh->exec ($query))) {
return $this->_throwError ($res->getMessage (), __LINE__ );
* copy a subtree/node/... under a new parent or/and behind a given element
* @param integer the ID of the node that shall be copied
* @param integer the new parent ID
* @param integer the new previous ID, if given parent ID will be omitted
* @return boolean true on success
function copy($id, $parentId = 0 , $prevId = 0 )
return $this->_throwError (
'copy-method is not implemented yet!',
* @author Wolfram Kriesing <wolfram@kriesing.de>
* @return mixed either the data of the root element or an Tree_Error
$query = sprintf('SELECT * FROM %s WHERE%s %s=1',
$this->_getColName ('left'));
if (MDB2 ::isError ($res = $this->dbh->queryRow ($query))) {
return $this->_throwError ($res->getMessage (), __LINE__ );
return !$res ? false : $this->_prepareResult ($res);
* @author Wolfram Kriesing <wolfram@kriesing.de>
* @param integer the ID of the element to return
* @return mixed either the data of the requested element
$query = sprintf('SELECT * FROM %s WHERE %s %s=%s',
$this->_getColName ('id'),
if (MDB2 ::isError ($res = $this->dbh->queryRow ($query))) {
return $this->_throwError ($res->getMessage (), __LINE__ );
return $this->_throwError (" Element with id $id does not exist!" , __LINE__ );
return $this->_prepareResult ($res);
* @param integer the ID of the element for which the children
* @return mixed either the data of the requested element or an Tree_Error
// subqueries would be cool :-)
$query = sprintf('SELECT * FROM %s WHERE%s %s=%s',
$this->_getColName ('left'),
if (MDB2 ::isError ($res = $this->dbh->queryRow ($query))) {
return $this->_throwError ($res->getMessage (), __LINE__ );
return $this->_prepareResult ($res);
* gets the path from the element with the given id down
* to the root. The returned array is sorted to start at root
* for simply walking through and retreiving the path
* @param integer the ID of the element for which the path shall be returned
* @return mixed either the data of the requested elements
$res = $this->dbh->queryAll ($this->_getPathQuery ($id));
if (MDB2 ::isError ($res)) {
return $this->_throwError ($res->getMessage (), __LINE__ );
return $this->_prepareResults ($res);
function _getPathQuery ($id)
// subqueries would be cool :-)
$query = sprintf('SELECT * FROM %s '.
'WHERE %s %s<=%s AND %s>=%s '.
// set the additional where add on
// render 'left<=curLeft'
$this->_getColName ('left'), $curElement['left'],
// render right>=curRight'
$this->_getColName ('right'), $curElement['right'],
$this->_getColName ('left'));
$query = $this->_getPathQuery ($id);
// i know this is not really beautiful ...
$query = preg_replace('/^select \* /i','SELECT COUNT(*) ',$query);
if (MDB2 ::isError ($res = $this->dbh->queryOne ($query))) {
return $this->_throwError ($res->getMessage (), __LINE__ );
* gets the element to the left, the left visit
* @author Wolfram Kriesing <wolfram@kriesing.de>
* @param integer the ID of the element
* @return mixed either the data of the requested element
$query = sprintf('SELECT * FROM %s WHERE%s (%s=%s OR %s=%s)',
$this->_getColName ('right'), $element['left'] - 1 ,
$this->_getColName ('left'), $element['left'] - 1 );
if (MDB2 ::isError ($res = $this->dbh->queryRow ($query))) {
return $this->_throwError ($res->getMessage (), __LINE__ );
return $this->_prepareResult ($res);
* gets the element to the right, the right visit
* @author Wolfram Kriesing <wolfram@kriesing.de>
* @param integer the ID of the element
* @return mixed either the data of the requested element
$query = sprintf('SELECT * FROM %s WHERE%s (%s=%s OR %s=%s)',
$this->_getColName ('left'), $element['right'] + 1 ,
$this->_getColName ('right'), $element['right'] + 1 );
if (MDB2 ::isError ($res = $this->dbh->queryRow ($query))) {
return $this->_throwError ($res->getMessage (), __LINE__ );
return $this->_prepareResult ($res);
* get the parent of the element with the given id
* @author Wolfram Kriesing <wolfram@kriesing.de>
* @param integer the ID of the element
* @return mixed the array with the data of the parent element
* or false, if there is no parent, if the element is
* the root or an Tree_Error
$this->table,$this->table,
$this->_getWhereAddOn (' AND ', 'p'),
$this->_getColName ('parentId'),
$this->_getColName ('id'),
$this->_getColName ('id'),
if (MDB2 ::isError ($res = $this->dbh->queryRow ($query))) {
return $this->_throwError ($res->getMessage (), __LINE__ );
return $this->_prepareResult ($res);
* get the children of the given element or if the parameter is an array.
* It gets the children of all the elements given by their ids
* @author Wolfram Kriesing <wolfram@kriesing.de>
* @param mixed (1) int the id of one element
* (2) array an array of ids for which
* the children will be returned
* @param integer the children of how many levels shall be returned
* @return mixed the array with the data of all children
* or false, if there are none
for ($i = 1; $i < $levels + 1; $i++ ) {
// if $ids is an array implode the values
$this->table,$this->table,
$this->_getWhereAddOn (' AND ', 'c'),
$this->_getColName ('id'),
$this->_getColName ('parentId'),
$this->_getColName ('id'),
// order by left, so we have it in the order
// as it is in the tree if no 'order'-option
: $this->_getColName ('left')
if (MDB2 ::isError ($_res = $this->dbh->queryAll ($query))) {
return $this->_throwError ($_res->getMessage (), __LINE__ );
// Column names are now unmapped
$_res = $this->_prepareResults ($_res);
// we use the id as the index, to make the use easier esp.
// for multiple return-values
foreach ($_res as $aRes) {
$tempRes[$aRes['id']] = $aRes;
foreach ($_res as $aRes) {
$ids[] = $aRes[$this->_getColName ('id')];
// quit the for-loop if there are no children in the current level
* get the next element on the same level
* if there is none return false
* @author Wolfram Kriesing <wolfram@kriesing.de>
* @param integer the ID of the element
* @return mixed the array with the data of the next element
* or false, if there is no next
$this->table, $this->table,
$this->_getWhereAddOn (' AND ', 'n'),
$this->_getColName ('right'),
$this->_getColName ('left'),
$this->_getColName ('parentId'),
$this->_getColName ('parentId'),
$this->_getColName ('id'),
if (MDB2 ::isError ($res = $this->dbh->queryRow ($query))) {
return $this->_throwError ($res->getMessage (), __LINE__ );
return !$res ? false : $this->_prepareResult ($res);
* get the previous element on the same level
* if there is none return false
* @author Wolfram Kriesing <wolfram@kriesing.de>
* @param integer the ID of the element
* @return mixed the array with the data of the previous element
* or false, if there is no previous
$this->table,$this->table,
$this->_getWhereAddOn (' AND ', 'p'),
$this->_getColName ('left'),
$this->_getColName ('right'),
$this->_getColName ('parentId'),
$this->_getColName ('parentId'),
$this->_getColName ('id'),
if (MDB2 ::isError ($res = $this->dbh->queryRow ($query))) {
return $this->_throwError ($res->getMessage (), __LINE__ );
return !$res ? false : $this->_prepareResult ($res);
* returns if $childId is a child of $id
* @author Wolfram Kriesing <wolfram@kriesing.de>
* @param int id of the element
* @param int id of the element to check if it is a child
* @return boolean true if it is a child
// check simply if the left and right of the child are within the
// left and right of the parent, if so it definitly is a child :-)
if ($parent['left'] < $child['left']
&& $parent['right'] > $child['right'])
* return the maximum depth of the tree
* @author "Denis Joloudov" <dan@aitart.ru>, Wolfram Kriesing <wolfram@kriesing.de>
* @return integer the depth of the tree
$query = sprintf('SELECT COUNT(*) FROM %s p, %s e '.
'WHERE %s (e.%s BETWEEN p.%s AND p.%s) AND '.
'(e.%s BETWEEN p.%s AND p.%s)',
$this-> table,$this->table,
$this->_getWhereAddOn (' AND ','p'),
$this->_getColName ('left'),$this->_getColName ('left'),
$this->_getColName ('right'),
$this->_getColName ('right'),$this->_getColName ('left'),
$this->_getColName ('right')
if (MDB2 ::isError ($res= $this->dbh->queryOne ($query))) {
return $this->_throwError ($res->getMessage (), __LINE__ );
return $this->_prepareResult ($res);
* Tells if the node with the given ID has children.
* @author Wolfram Kriesing <wolfram@kriesing.de>
* @param integer the ID of a node
* @return boolean if the node with the given id has children
// if the diff between left and right > 1 then there are children
return ($element['right'] - $element['left']) > 1;
* return the id of the element which is referenced by $path
* this is useful for xml-structures, like: getIdByPath('/root/sub1/sub2')
* this requires the structure to use each name uniquely
* if this is not given it will return the first proper path found
* i.e. there should only be one path /x/y/z
* experimental: the name can be non unique if same names are in different levels
* @author Pierre-Alain Joye <paj@pearfr.org>
* @param string $path the path to search for
* @param integer $startId the id where to start the search
* @param string $nodeName the name of the key that contains
* @param string $seperator the path seperator
* @return integer the id of the searched element
function getIdByPath($path, $startId = 0 , $nodeName = 'name', $separator = '/')
// should this method be called getElementIdByPath ????
// Yes, with an optional private paramater to get the whole node
// in preference to only the id?
return $this->_throwError (
'getIdByPath: Empty separator not allowed', __LINE__ );
if ($path == $separator) {
if (!($colname= $this->_getColName ($nodeName))) {
return $this->_throwError (
'getIdByPath: Invalid node name', __LINE__ );
// If the start node has no child, returns false
// hasChildren calls getElement. Not very good right
$rangeStart = $startElem['left'];
$rangeEnd = $startElem['right'];
// Not clean, we should call hasChildren, but I do not
// want to call getELement again :). See TODO
$startHasChild = ($rangeEnd- $rangeStart) > 1 ? true : false;
$t = $this->_preparePath ($path, $cwd, $separator);
list ($elems, $sublevels) = $t;
. $this->_getColName ('id')
$query .= "='". $elems[0 ]. "'";
$query .= "='". $elems[$cntElems-1 ]. "'";
$this->_getColName ('left'). '>'. $rangeStart.
$this->_getColName ('right'). '<'. $rangeEnd. ')';
$res = $this->dbh->queryOne ($query);
if (MDB2 ::isError ($res)) {
return $this->_throwError ($res->getMessage (), __LINE__ );
return ($res ? (int) $res : false );
* @author Wolfram Kriesing <wolfram@kriesing.de>
* @param string the current where clause
* @return string the where clause we want to add to a query
function _getWhereAddOn ($addAfter = ' AND ', $tableName = '')
return ' '. ($tableName ? $tableName. '.' : '')." $where$addAfter ";
// for compatibility to Memory methods
* gets the tree under the given element in one array, sorted
* so you can go through the elements from begin to end and list them
* as they are in the tree, where every child (until the deepest) is retreived
* @author Wolfram Kriesing <wolfram@kriesing.de>
* @param integer $startId the id where to start walking
* @param integer $depth this number says how deep into
* the structure the elements shall
* @return array sorted as listed in the tree
function &getNode($startId = 0 , $depth = 0 )
//FIXXXME use getChildren()
Documentation generated on Mon, 11 Mar 2019 15:47:05 -0400 by phpDocumentor 1.4.4. PEAR Logo Copyright © PHP Group 2004.
|