Current File : //opt/RZphp56/includes/Tree/Dynamic/MDBnested.php |
<?php
/* vim: set expandtab tabstop=4 shiftwidth=4: */
// +----------------------------------------------------------------------+
// | PHP Version 4 |
// +----------------------------------------------------------------------+
// | Copyright (c) 1997-2003 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. |
// +----------------------------------------------------------------------+
// | Authors: |
// +----------------------------------------------------------------------+
//
// $Id: MDBnested.php,v 1.3.2.2 2009/03/12 17:19:55 dufuz Exp $
require_once 'Tree/OptionsMDB.php';
/**
* This class implements methods to work on a tree saved using the nested
* tree model.
* explaination: http://research.calacademy.org/taf/proceedings/ballew/index.htm
*
* @access public
* @package Tree
*/
class Tree_Dynamic_MDBnested extends Tree_OptionsMDB
{
// {{{ properties
var $debug = 0;
var $options = array(
// 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
'whereAddOn'=>'',
'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:
// id, left, right
'columnNameMaps'=>array(
// since mysql at least doesnt support 'left' ...
'left' => 'l',
// ...as a column name we set default to the first
//letter only
'right' => 'r',
// parent id
'parentId' => 'parent'
),
// needed for sorting the tree, currently only used in Memory_DBnested
'order' => ''
);
// }}}
// {{{ __construct()
// 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
/**
*
*
* @access public
* @version 2002/03/02
* @param string the DSN for the DB connection
* @return void
*/
function __construct($dsn, $options = array())
{
$this->Tree_Dynamic_MDBnested($dsn, $options);
}
// }}}
// {{{ Tree_Dynamic_DBnested()
/**
*
*
* @access public
* @version 2002/03/02
* @author Wolfram Kriesing <wolfram@kriesing.de>
* @param string the DSN for the DB connection
* @return void
*/
function Tree_Dynamic_MDBnested($dsn, $options = array())
{
parent::Tree_OptionsMDB($dsn, $options); // instanciate DB
$this->table = $this->getOption('table');
}
// }}}
// {{{ add()
/**
* add a new element to the tree
* there are three ways to use this method
* Method 1:
* Give only the $parentId and the $newValues will be inserted
* as the first child of this parent
* <code>
* // insert a new element under the parent with the ID=7
* $tree->add(array('name'=>'new element name'), 7);
* </code>
*
* Method 2:
* 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
* <code>
* // insert a new element after the element with the ID=5
* $tree->add(array('name'=>'new'), 0, 5);
* </code>
*
* Method 3:
* 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!
*
* @access public
* @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');
$prevVisited = 0;
// check the DB-table if the columns which are given as keys
// in the array $newValues do really exist, if not remove them
// from the array
// FIXXME do the above described
// if no parent and no prevId is given the root shall be added
if ($parentId || $prevId) {
if ($prevId) {
$element = $this->getElement($prevId);
// we also need the parent id of the element
// to write it in the db
$parentId = $element['parentId'];
} else {
$element = $this->getElement($parentId);
}
$newValues['parentId'] = $parentId;
if (Tree::isError($element)) {
return $element;
}
// 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 :-)
// See:
// 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))) {
// FIXXME rollback
//$this->dbh->rollback();
return $err;
}
}
// inserting _one_ new element in the tree
$newData = array();
// 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->getTextValue($value);
}
// 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->table,
$this->_getColName('id'),
implode(',', array_keys($newData)) ,
$this->dbh->getIntegerValue($nextId),
implode(',', $newData)
);
if (MDB::isError($res = $this->dbh->query($query))) {
// rollback
return $this->_throwError($res->getMessage(), __LINE__);
}
// commit here
return $nextId;
}
// }}}
// {{{ _add()
/**
* 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
* of the parent
* 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',
$this->table,
$lName,
$lName,
$numberOfElements*2,
$this->_getWhereAddOn(),
$lName,
$prevVisited);
if (MDB::isError($res = $this->dbh->query($query))) {
// FIXXME rollback
return $this->_throwError($res->getMessage(), __LINE__);
}
$query = sprintf('UPDATE %s SET %s=%s+%s WHERE%s %s>%s',
$this->table,
$rName,$rName,
$numberOfElements*2,
$this->_getWhereAddOn(),
$rName,
$prevVisited);
if (MDB::isError($res = $this->dbh->query($query))) {
// FIXXME rollback
return $this->_throwError($res->getMessage(), __LINE__);
}
return true;
}
// }}}
// {{{ remove()
/**
* remove a tree element
* this automatically remove all children and their children
* if a node shall be removed that has children
*
* @access public
* @param integer $id the id of the element to be removed
* @return boolean returns either true or throws an error
*/
function remove($id)
{
$element = $this->getElement($id);
if (Tree::isError($element)) {
return $element;
}
// FIXXME start transaction
//$this->dbh->autoCommit(false);
$query = sprintf( 'DELETE FROM %s WHERE%s %s BETWEEN %s AND %s',
$this->table,
$this->_getWhereAddOn(),
$this->_getColName('left'),
$element['left'],$element['right']);
if (MDB::isError($res = $this->dbh->query($query))) {
// FIXXME rollback
//$this->dbh->rollback();
return $this->_throwError($res->getMessage(), __LINE__);
}
if (Tree::isError($err = $this->_remove($element))) {
// FIXXME rollback
//$this->dbh->rollback();
return $err;
}
return true;
}
// }}}
// {{{ _remove()
/**
* 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!
*
* @see getElement()
* @access private
* @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
$query = sprintf("UPDATE
%s
SET
%s=%s-$delta,
%s=%s-$delta
WHERE%s %s>%s",
$this->table,
$lName, $lName,
$rName, $rName,
$this->_getWhereAddOn(),
$lName, $element['left']);
if (MDB::isError($res = $this->dbh->query($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__);
}
$query = sprintf("UPDATE
%s
SET %s=%s-$delta
WHERE
%s %s < %s
AND
%s>%s",
$this->table,
$rName, $rName,
$this->_getWhereAddOn(),
$lName, $element['left'],
$rName, $element['right']);
if (MDB::isError($res = $this->dbh->query($query))) {
// the rollback shall be done by the method calling this one
// since it is only private
return $this->_throwError($res->getMessage(), __LINE__);
}
// FIXXME commit:
// should that not also be done in the method calling this one?
// like when an error occurs?
//$this->dbh->commit();
return true;
}
// }}}
// {{{ move()
/**
* 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:
* $tree->move(x, y)
* to move the element (or entire tree) with the id x
* under the element with the id y
* or
* $tree->move(x, 0, y); // ommit the second parameter by setting
* it to 0
* to move the element (or entire tree) with the id x
* behind the element with the id y
* or
* $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
*
* @version 2002/06/08
* @access public
* @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)
{
settype($idsToMove,'array');
$errors = array();
foreach ($idsToMove as $idToMove) {
$ret = $this->_move($idToMove, $newParentId, $newPrevId);
if (Tree::isError($ret)) {
$errors[] = $ret;
}
}
// FIXXME the error in a nicer way, or even better
// let the throwError method do it!!!
if (sizeof($errors)) {
return $this->_throwError(serialize($errors), __LINE__);
}
return true;
}
// }}}
// {{{ _move()
/**
* this method moves one tree element
*
* @see move()
* @version 2002/04/29
* @access public
* @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
if ($newPrevId) {
// dont let people move an element behind itself, tell it
// succeeded, since it already is there :-)
if ($newPrevId == $idToMove) {
return true;
}
if (Tree::isError($newPrevious=$this->getElement($newPrevId))) {
return $newPrevious;
}
$newParentId = $newPrevious['parentId'];
} else {
if ($newParentId == 0) {
return $this->_throwError('no parent id given', __LINE__);
}
// if the element shall be moved under one of its children
// return false
if ($this->isChildOf($idToMove,$newParentId)) {
return $this->_throwError(
'can not move an element under one of its children' ,
__LINE__
);
}
// dont do anything to let an element be moved under itself
// which is bullshit
if ($newParentId==$idToMove) {
return true;
}
// try to retreive the data of the parent element
if (Tree::isError($newParent=$this->getElement($newParentId))) {
return $newParent;
}
}
// get the data of the element itself
if (Tree::isError($element=$this->getElement($idToMove))) {
return $element;
}
$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);
if (Tree::isError($err)) {
// FIXXME rollback
//$this->dbh->rollback();
return $err;
}
// update the parentId of the element with $idToMove
$err = $this->update($idToMove, array('parentId' => $newParentId));
if (Tree::isError($err)) {
// FIXXME rollback
//$this->dbh->rollback();
return $err;
}
// 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
if ($newPrevId) {
if (Tree::isError($temp = $this->getElement($newPrevId))) {
// FIXXME rollback
//$this->dbh->rollback();
return $temp;
}
$calcWith = $temp['right'];
} else {
if (Tree::isError($temp = $this->getElement($newParentId))) {
// FIXXME rollback
//$this->dbh->rollback();
return $temp;
}
$calcWith = $temp['left'];
}
// get the element that shall be moved again, since the left and
// right might have changed by the add-call
if (Tree::isError($element=$this->getElement($idToMove))) {
return $element;
}
// 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!
$offset++;
$lName = $this->_getColName('left');
$rName = $this->_getColName('right');
$query = sprintf("UPDATE
%s
SET
%s=%s+$offset,
%s=%s+$offset
WHERE
%s %s>%s
AND
%s < %s",
$this->table,
$rName, $rName,
$lName, $lName,
$this->_getWhereAddOn(),
$lName, $element['left']-1,
$rName, $element['right']+1);
if (MDB::isError($res=$this->dbh->query($query))) {
// FIXXME rollback
//$this->dbh->rollback();
return $this->_throwError($res->getMessage(), __LINE__);
}
// remove the part of the tree where the element(s) was/were before
if (Tree::isError($err=$this->_remove($element))) {
// FIXXME rollback
//$this->dbh->rollback();
return $err;
}
// FIXXME commit all changes
//$this->dbh->commit();
return true;
}
// }}}
// {{{ update()
/**
* update the tree element given by $id with the values in $newValues
*
* @access public
* @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
$values = array();
foreach ($newValues as $key=>$value) {
///////////FIX ME: Add proper quote handling
$values[] = $this->_getColName($key).'='.$this->dbh->getTextValue($value);
}
$query = sprintf('UPDATE %s SET %s WHERE%s %s=%s',
$this->table,
implode(',',$values),
$this->_getWhereAddOn(),
$this->_getColName('id'),
$id);
if (MDB::isError($res=$this->dbh->query($query))) {
return $this->_throwError($res->getMessage(), __LINE__);
}
return true;
}
// }}}
// {{{ update()
/**
* copy a subtree/node/... under a new parent or/and behind a given element
*
*
* @access public
* @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!' ,
__LINE__
);
// get element tree
// $this->addTree
}
// }}}
// {{{ getRoot()
/**
* get the root
*
* @access public
* @version 2002/03/02
* @author Wolfram Kriesing <wolfram@kriesing.de>
* @return mixed either the data of the root element or an Tree_Error
*/
function getRoot()
{
$query = sprintf('SELECT * FROM %s WHERE%s %s=1',
$this->table,
$this->_getWhereAddOn(),
$this->_getColName('left'));
if (MDB::isError($res = $this->dbh->getRow($query))) {
return $this->_throwError($res->getMessage(), __LINE__);
}
return !$res ? false : $this->_prepareResult($res);
}
// }}}
// {{{ getElement()
/**
*
*
* @access public
* @version 2002/03/02
* @author Wolfram Kriesing <wolfram@kriesing.de>
* @param integer the ID of the element to return
*
* @return mixed either the data of the requested element
* or an Tree_Error
*/
function getElement($id)
{
$query = sprintf('SELECT * FROM %s WHERE %s %s=%s',
$this->table,
$this->_getWhereAddOn(),
$this->_getColName('id'),
$id);
if (MDB::isError($res = $this->dbh->getRow($query))) {
return $this->_throwError($res->getMessage(), __LINE__);
}
if (!$res) {
return $this->_throwError("Element with id $id does not exist!" ,
__LINE__);
}
return $this->_prepareResult($res);
}
// }}}
// {{{ getChild()
/**
*
*
* @access public
* @version 2002/03/02
* @param integer the ID of the element for which the children
* shall be returned
* @return mixed either the data of the requested element or an Tree_Error
*/
function getChild($id)
{
// subqueries would be cool :-)
$curElement = $this->getElement($id);
if (Tree::isError($curElement)) {
return $curElement;
}
$query = sprintf('SELECT * FROM %s WHERE%s %s=%s',
$this->table,
$this->_getWhereAddOn(),
$this->_getColName('left'),
$curElement['left']+1);
if (MDB::isError($res = $this->dbh->getRow($query))) {
return $this->_throwError($res->getMessage(), __LINE__);
}
return $this->_prepareResult($res);
}
// }}}
// {{{ getPath()
/**
* 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
*
* @access public
* @param integer the ID of the element for which the path shall be returned
* @return mixed either the data of the requested elements
* or an Tree_Error
*/
function getPath($id)
{
$res = $this->dbh->getAll($this->_getPathQuery($id));
if (MDB::isError($res)) {
return $this->_throwError($res->getMessage(), __LINE__);
}
return $this->_prepareResults($res);
}
// }}}
// {{{ _getPathQuery()
function _getPathQuery($id)
{
// subqueries would be cool :-)
$curElement = $this->getElement($id);
$query = sprintf('SELECT * FROM %s '.
'WHERE %s %s<=%s AND %s>=%s '.
'ORDER BY %s',
// set the FROM %s
$this->table,
// set the additional where add on
$this->_getWhereAddOn(),
// render 'left<=curLeft'
$this->_getColName('left'), $curElement['left'],
// render right>=curRight'
$this->_getColName('right'), $curElement['right'],
// set the order column
$this->_getColName('left'));
return $query;
}
// }}}
// {{{ getLevel()
function getLevel($id)
{
$query = $this->_getPathQuery($id);
// i know this is not really beautiful ...
$query = preg_replace('/^select \* /i','SELECT COUNT(*) ',$query);
if (MDB::isError($res = $this->dbh->getOne($query))) {
return $this->_throwError($res->getMessage(), __LINE__);
}
return $res-1;
}
// }}}
// {{{ getLeft()
/**
* gets the element to the left, the left visit
*
* @access public
* @version 2002/03/07
* @author Wolfram Kriesing <wolfram@kriesing.de>
* @param integer the ID of the element
* @return mixed either the data of the requested element
* or an Tree_Error
*/
function getLeft($id)
{
$element = $this->getElement($id);
if (Tree::isError($element)) {
return $element;
}
$query = sprintf('SELECT * FROM %s WHERE%s (%s=%s OR %s=%s)',
$this->table,
$this->_getWhereAddOn(),
$this->_getColName('right'), $element['left'] - 1,
$this->_getColName('left'), $element['left'] - 1);
if (MDB::isError($res = $this->dbh->getRow($query))) {
return $this->_throwError($res->getMessage(), __LINE__);
}
return $this->_prepareResult($res);
}
// }}}
// {{{ getRight()
/**
* gets the element to the right, the right visit
*
* @access public
* @version 2002/03/07
* @author Wolfram Kriesing <wolfram@kriesing.de>
* @param integer the ID of the element
* @return mixed either the data of the requested element
* or an Tree_Error
*/
function getRight($id)
{
$element = $this->getElement($id);
if (Tree::isError($element))
return $element;
$query = sprintf('SELECT * FROM %s WHERE%s (%s=%s OR %s=%s)',
$this->table,
$this->_getWhereAddOn(),
$this->_getColName('left'), $element['right'] + 1,
$this->_getColName('right'), $element['right'] + 1);
if (MDB::isError($res = $this->dbh->getRow($query))) {
return $this->_throwError($res->getMessage(), __LINE__);
}
return $this->_prepareResult($res);
}
// }}}
// {{{ getParent()
/**
* get the parent of the element with the given id
*
* @access public
* @version 2002/04/15
* @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
*/
function getParent($id)
{
$query = sprintf('SELECT
p.*
FROM
%s p,%s e
WHERE
%s e.%s=p.%s
AND
e.%s=%s',
$this->table,$this->table,
$this->_getWhereAddOn(' AND ', 'p'),
$this->_getColName('parentId'),
$this->_getColName('id'),
$this->_getColName('id'),
$id);
if (MDB::isError($res = $this->dbh->getRow($query))) {
return $this->_throwError($res->getMessage(), __LINE__);
}
return $this->_prepareResult($res);
}
// }}}
// {{{ getChildren()
/**
* 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
* in the array.
*
* @access public
* @version 2002/04/15
* @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
*/
function getChildren($ids, $levels = 1)
{
$res = array();
for ($i = 1; $i < $levels + 1; $i++) {
// if $ids is an array implode the values
$getIds = is_array($ids) ? implode(',', $ids) : $ids;
$query = sprintf('SELECT
c.*
FROM
%s c,%s e
WHERE
%s e.%s=c.%s
AND
e.%s IN (%s) '.
'ORDER BY
c.%s',
$this->table,$this->table,
$this->_getWhereAddOn(' AND ', 'c'),
$this->_getColName('id'),
$this->_getColName('parentId'),
$this->_getColName('id'),
$getIds,
// order by left, so we have it in the order
// as it is in the tree if no 'order'-option
// is given
$this->getOption('order')?
$this->getOption('order')
: $this->_getColName('left')
);
if (MDB::isError($_res = $this->dbh->getAll($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
$tempRes = array();
foreach ($_res as $aRes) {
$tempRes[$aRes['id']] = $aRes;
}
$_res = $tempRes;
if ($levels > 1) {
$ids = array();
foreach ($_res as $aRes) {
$ids[] = $aRes[$this->_getColName('id')];
}
}
$res = array_merge($res, $_res);
// quit the for-loop if there are no children in the current level
if (!sizeof($ids)) {
break;
}
}
return $res;
}
// }}}
// {{{ getNext()
/**
* get the next element on the same level
* if there is none return false
*
* @access public
* @version 2002/04/15
* @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
* or Tree_Error
*/
function getNext($id)
{
$query = sprintf('SELECT
n.*
FROM
%s n,%s e
WHERE
%s e.%s=n.%s-1
AND
e.%s=n.%s
AND
e.%s=%s',
$this->table, $this->table,
$this->_getWhereAddOn(' AND ', 'n'),
$this->_getColName('right'),
$this->_getColName('left'),
$this->_getColName('parentId'),
$this->_getColName('parentId'),
$this->_getColName('id'),
$id);
if (MDB::isError($res = $this->dbh->getRow($query))) {
return $this->_throwError($res->getMessage(), __LINE__);
}
return !$res ? false : $this->_prepareResult($res);
}
// }}}
// {{{ getPrevious()
/**
* get the previous element on the same level
* if there is none return false
*
* @access public
* @version 2002/04/15
* @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
* or a Tree_Error
*/
function getPrevious($id)
{
$query = sprintf('SELECT
p.*
FROM
%s p,%s e
WHERE
%s e.%s=p.%s+1
AND
e.%s=p.%s
AND
e.%s=%s',
$this->table,$this->table,
$this->_getWhereAddOn(' AND ', 'p'),
$this->_getColName('left'),
$this->_getColName('right'),
$this->_getColName('parentId'),
$this->_getColName('parentId'),
$this->_getColName('id'),
$id);
if (MDB::isError($res = $this->dbh->getRow($query))) {
return $this->_throwError($res->getMessage(), __LINE__);
}
return !$res ? false : $this->_prepareResult($res);
}
// }}}
// {{{ isChildOf()
/**
* returns if $childId is a child of $id
*
* @abstract
* @version 2002/04/29
* @access public
* @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
*/
function isChildOf($id, $childId)
{
// 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 :-)
$parent = $this->getElement($id);
$child = $this->getElement($childId);
if ($parent['left'] < $child['left']
&& $parent['right'] > $child['right'])
{
return true;
}
return false;
}
// }}}
// {{{ getDepth()
/**
* return the maximum depth of the tree
*
* @version 2003/02/25
* @access public
* @author "Denis Joloudov" <dan@aitart.ru>, Wolfram Kriesing <wolfram@kriesing.de>
* @return integer the depth of the tree
*/
function getDepth()
{
// FIXXXME TODO!!!
$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,
// first line in where
$this->_getWhereAddOn(' AND ','p'),
$this->_getColName('left'),$this->_getColName('left'),
$this->_getColName('right'),
// second where line
$this->_getColName('right'),$this->_getColName('left'),
$this->_getColName('right')
);
if (MDB::isError($res=$this->dbh->getOne($query))) {
return $this->_throwError($res->getMessage(), __LINE__);
}
if (!$res) {
return false;
}
return $this->_prepareResult($res);
}
// }}}
// {{{ hasChildren()
/**
* Tells if the node with the given ID has children.
*
* @version 2003/03/04
* @access public
* @author Wolfram Kriesing <wolfram@kriesing.de>
* @param integer the ID of a node
* @return boolean if the node with the given id has children
*/
function hasChildren($id)
{
$element = $this->getElement($id);
// if the diff between left and right > 1 then there are children
return ($element['right'] - $element['left']) > 1;
}
// }}}
// {{{ getIdByPath()
/**
* 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
*
* @version 2003/05/11
* @access public
* @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
* the node name
* @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?
{
if ($separator == '') {
return $this->_throwError(
'getIdByPath: Empty separator not allowed', __LINE__);
}
if ($path == $separator) {
$root = $this->getRoot();
if (Tree::isError($root)) {
return $root;
}
return $root['id'];
}
if (!($colname=$this->_getColName($nodeName))) {
return $this->_throwError(
'getIdByPath: Invalid node name', __LINE__);
}
if ($startId != 0) {
// If the start node has no child, returns false
// hasChildren calls getElement. Not very good right
// now. See the TODO
$startElem = $this->getElement($startId);
if (!is_array($startElem) || Tree::isError($startElem)) {
return $startElem;
}
// No child? return
if (!is_array($startElem)) {
return null;
}
$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;
$cwd = '/'.$this->getPathAsString($startId);
} else {
$cwd = '/';
$startHasChild = false;
}
$t = $this->_preparePath($path, $cwd, $separator);
if (Tree::isError($t)) {
return $t;
}
list($elems, $sublevels) = $t;
$cntElems = sizeof($elems);
$where = '';
$query = 'SELECT '
.$this->_getColName('id')
.' FROM '
.$this->table
.' WHERE '
.$colname;
if ($cntElems == 1) {
$query .= "='".$elems[0]."'";
} else {
$query .= "='".$elems[$cntElems-1]."'";
}
if ($startHasChild) {
$where .= ' AND ('.
$this->_getColName('left').'>'.$rangeStart.
' AND '.
$this->_getColName('right').'<'.$rangeEnd.')';
}
$res = $this->dbh->getOne($query);
if (MDB::isError($res)) {
return $this->_throwError($res->getMessage(),
__LINE__);
}
return ($res ? (int)$res : false);
}
// }}}
//
// PRIVATE METHODS
//
// {{{ _getWhereAddOn()
/**
*
*
* @access private
* @version 2002/04/20
* @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 = '')
{
if ($where=$this->getOption('whereAddOn')) {
return ' '.($tableName ? $tableName.'.' : '')." $where$addAfter ";
}
return '';
}
// }}}
// {{{ getFirstRoot()
// for compatibility to Memory methods
function getFirstRoot()
{
return $this->getRoot();
}
// }}}
// {{{ getNode()
/**
* 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
*
* @see &_getNode()
* @access public
* @version 2001/12/17
* @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
* be retreived
* @return array sorted as listed in the tree
*/
function &getNode($startId = 0, $depth = 0)
{
//FIXXXME use getChildren()
if ($startId) {
$startNode = $this->getElement($startId);
if (Tree::isError($startNode)) {
return $startNode;
}
} else {
}
}
}
/*
* Local Variables:
* mode: php
* tab-width: 4
* c-basic-offset: 4
* End:
*/