| 1 |
<?php |
| 2 |
// |
| 3 |
// +----------------------------------------------------------------------+ |
| 4 |
// | PHP Version 4 | |
| 5 |
// +----------------------------------------------------------------------+ |
| 6 |
// | Copyright (c) 1997-2003 The PHP Group | |
| 7 |
// +----------------------------------------------------------------------+ |
| 8 |
// | This source file is subject to version 2.02 of the PHP license, | |
| 9 |
// | that is bundled with this package in the file LICENSE, and is | |
| 10 |
// | available at through the world-wide-web at | |
| 11 |
// | http://www.php.net/license/2_02.txt. | |
| 12 |
// | If you did not receive a copy of the PHP license and are unable to | |
| 13 |
// | obtain it through the world-wide-web, please send a note to | |
| 14 |
// | license@php.net so we can mail you a copy immediately. | |
| 15 |
// +----------------------------------------------------------------------+ |
| 16 |
// | Authors: | |
| 17 |
// +----------------------------------------------------------------------+ |
| 18 |
// |
| 19 |
// $Id: DBnested.php,v 1.16 2003/03/04 18:57:18 cain Exp $ |
| 20 |
|
| 21 |
require_once('Tree/Common.php'); |
| 22 |
require_once('Tree/Error.php'); |
| 23 |
|
| 24 |
/** |
| 25 |
* this class implements methods to work on a tree saved using the nested |
| 26 |
* tree model |
| 27 |
* explaination: http://research.calacademy.org/taf/proceedings/ballew/index.htm |
| 28 |
* |
| 29 |
* @access public |
| 30 |
* @package Tree |
| 31 |
*/ |
| 32 |
class Tree_Dynamic_DBnested extends Tree_Common |
| 33 |
// FIXXME should actually extend Tree_Common, to use the methods provided in there... but we need to connect |
| 34 |
// to the db here, so we extend optionsDB for now, may be use "aggregate" function to fix that |
| 35 |
{ |
| 36 |
|
| 37 |
var $debug = 0; |
| 38 |
|
| 39 |
var $options = array( |
| 40 |
// FIXXME to be implemented |
| 41 |
'whereAddOn'=>'' // add on for the where clause, this string is simply added behind the WHERE in the select |
| 42 |
// so you better make sure its correct SQL :-), i.e. 'uid=3' |
| 43 |
// this is needed i.e. when you are saving many trees in one db-table |
| 44 |
,'table' =>'' // |
| 45 |
// since the internal names are fixed, to be portable between different |
| 46 |
// DB tables with different column namings, we map the internal name to the real column name |
| 47 |
// using this array here, if it stays empty the internal names are used, which are: |
| 48 |
// id, left, right |
| 49 |
,'columnNameMaps'=>array( |
| 50 |
//'id' => 'node_id', // use "node_id" as "id" |
| 51 |
'left' => 'l' // since mysql at least doesnt support 'left' ... |
| 52 |
,'right' => 'r' // ...as a column name we set default to the first letter only |
| 53 |
//'name' => 'nodeName' // |
| 54 |
,'parentId' => 'parent' // parent id |
| 55 |
) |
| 56 |
,'order' => '' // needed for sorting the tree, currently only used in Memory_DBnested |
| 57 |
); |
| 58 |
|
| 59 |
// the defined methods here are proposals for the implementation, |
| 60 |
// they are named the same, as the methods in the "Memory" branch, if possible |
| 61 |
// it would be cool to keep the same naming and if the same parameters would be possible too |
| 62 |
// then it would be even better, so one could easily change from any kind of |
| 63 |
// tree-implementation to another, without changing the source code, only the setupXXX would need to be changed |
| 64 |
|
| 65 |
/** |
| 66 |
* |
| 67 |
* |
| 68 |
* @access public |
| 69 |
* @version 2002/03/02 |
| 70 |
* @author Wolfram Kriesing <wolfram@kriesing.de> |
| 71 |
* @param |
| 72 |
* @return |
| 73 |
*/ |
| 74 |
function __construct( $dsn , $options=array() ) |
| 75 |
{ |
| 76 |
Tree_Dynamic_DBnested( $dsn , $options ); |
| 77 |
} |
| 78 |
|
| 79 |
/** |
| 80 |
* |
| 81 |
* |
| 82 |
* @access public |
| 83 |
* @version 2002/03/02 |
| 84 |
* @author Wolfram Kriesing <wolfram@kriesing.de> |
| 85 |
* @param |
| 86 |
* @return |
| 87 |
*/ |
| 88 |
function Tree_Dynamic_DBnested( $dsn , $options=array() ) |
| 89 |
{ |
| 90 |
parent::Tree_OptionsDB( $dsn , $options ); // instanciate DB |
| 91 |
$this->table = $this->getOption('table'); |
| 92 |
} |
| 93 |
|
| 94 |
/** |
| 95 |
* add a new element to the tree |
| 96 |
* there are three ways to use this method |
| 97 |
* 1. |
| 98 |
* give only the $parentId and the $newValues will be inserted as the first child of this parent |
| 99 |
* i.e. // insert a new element under the parent with the ID=7 |
| 100 |
* $tree->add( array('name'=>'new element name') , 7 ); |
| 101 |
* 2. |
| 102 |
* give the $prevId ($parentId will be dismissed) and the new element |
| 103 |
* will be inserted in the tree after the element with the ID=$prevId |
| 104 |
* the parentId is not necessary because the prevId defines exactly where |
| 105 |
* the new element has to be place in the tree, and the parent is the same as |
| 106 |
* for the element with the ID=$prevId |
| 107 |
* i.e. // insert a new element after the element with the ID=5 |
| 108 |
* $tree->add( array('name'=>'new') , 0 , 5 ); |
| 109 |
* 3. |
| 110 |
* neither $parentId nor prevId is given, then the root element will be inserted |
| 111 |
* this requires that programmer is responsible to confirm this |
| 112 |
* this method does not (yet) check if there is already a root element saved !!! |
| 113 |
* |
| 114 |
* @access public |
| 115 |
* @author Wolfram Kriesing <wolfram@kriesing.de> |
| 116 |
* @param array $newValues this array contains the values that shall be inserted in the db-table |
| 117 |
* @param integer $parentId the id of the element which shall be the parent of the new element |
| 118 |
* @param integer $prevId the id of the element which shall preceed the one to be inserted |
| 119 |
* use either 'parentId' or 'prevId' |
| 120 |
* @return integer the ID of the element that had been inserted |
| 121 |
*/ |
| 122 |
function add( $newValues , $parentId=0 , $prevId=0 ) |
| 123 |
{ |
| 124 |
$lName = $this->_getColName('left'); |
| 125 |
$rName = $this->_getColName('right'); |
| 126 |
$prevVisited = 0; |
| 127 |
|
| 128 |
// check the DB-table if the columns which are given as keys |
| 129 |
// in the array $newValues do really exist, if not remove them from the array |
| 130 |
// FIXXME do the above described |
| 131 |
|
| 132 |
if( $parentId || $prevId ) // if no parent and no prevId is given the root shall be added |
| 133 |
{ |
| 134 |
if( $prevId ) |
| 135 |
{ |
| 136 |
$element = $this->getElement( $prevId ); |
| 137 |
$parentId = $element['parentId']; // we also need the parent id of the element, to write it in the db |
| 138 |
} |
| 139 |
else |
| 140 |
{ |
| 141 |
$element = $this->getElement( $parentId ); |
| 142 |
} |
| 143 |
$newValues['parentId'] = $parentId; |
| 144 |
|
| 145 |
if( PEAR::isError($element) ) |
| 146 |
return $element; |
| 147 |
|
| 148 |
// get the "visited"-value where to add the new element behind |
| 149 |
// if $prevId is given, we need to use the right-value |
| 150 |
// if only the $parentId is given we need to use the left-value |
| 151 |
// look at it graphically, that made me understand it :-) |
| 152 |
// i.e. at: http://research.calacademy.org/taf/proceedings/ballew/sld034.htm |
| 153 |
$prevVisited = $prevId ? $element['right'] : $element['left']; |
| 154 |
|
| 155 |
// FIXXME start transaction here |
| 156 |
|
| 157 |
if( PEAR::isError($err=$this->_add( $prevVisited , 1 )) ) |
| 158 |
{ |
| 159 |
// FIXXME rollback |
| 160 |
//$this->dbh->rollback(); |
| 161 |
return $err; |
| 162 |
} |
| 163 |
} |
| 164 |
|
| 165 |
// inserting _one_ new element in the tree |
| 166 |
$newData = array(); |
| 167 |
foreach( $newValues as $key=>$value ) // quote the values, as needed for the insert |
| 168 |
{ |
| 169 |
$newData[$this->_getColName($key)] = $this->dbh->quote($value); |
| 170 |
} |
| 171 |
|
| 172 |
// set the proper right and left values |
| 173 |
$newData[$lName] = $prevVisited+1; |
| 174 |
$newData[$rName] = $prevVisited+2; |
| 175 |
|
| 176 |
// use sequences to create a new id in the db-table |
| 177 |
$nextId = $this->dbh->nextId($this->table); |
| 178 |
$query = sprintf( 'INSERT INTO %s (%s,%s) VALUES (%s,%s)', |
| 179 |
$this->table , |
| 180 |
$this->_getColName('id'), |
| 181 |
implode( "," , array_keys($newData) ) , |
| 182 |
$nextId, |
| 183 |
implode( "," , $newData ) |
| 184 |
); |
| 185 |
if( DB::isError( $res = $this->dbh->query($query) ) ) |
| 186 |
{ |
| 187 |
// rollback |
| 188 |
return $this->_throwError( $res->getMessage() , __LINE__ ); |
| 189 |
} |
| 190 |
// commit here |
| 191 |
|
| 192 |
return $nextId; |
| 193 |
} // end of function |
| 194 |
|
| 195 |
/** |
| 196 |
* this method only updates the left/right values of all the |
| 197 |
* elements that are affected by the insertion |
| 198 |
* be sure to set the parentId of the element(s) you insert |
| 199 |
* |
| 200 |
* @param int this parameter is not the ID!!! |
| 201 |
* it is the previous visit number, that means |
| 202 |
* if you are inserting a child, you need to use the left-value |
| 203 |
* of the parent |
| 204 |
* if you are inserting a "next" element, on the same level |
| 205 |
* you need to give the right value !! |
| 206 |
* @param int the number of elements you plan to insert |
| 207 |
* @return mixed either true on success or a Tree_Error on failure |
| 208 |
*/ |
| 209 |
function _add( $prevVisited , $numberOfElements=1 ) |
| 210 |
{ |
| 211 |
$lName = $this->_getColName('left'); |
| 212 |
$rName = $this->_getColName('right'); |
| 213 |
|
| 214 |
// update the elements which will be affected by the new insert |
| 215 |
$query = sprintf( 'UPDATE %s SET %s=%s+%s WHERE%s %s>%s', |
| 216 |
$this->table, |
| 217 |
$lName,$lName, |
| 218 |
$numberOfElements*2, |
| 219 |
$this->_getWhereAddOn(), |
| 220 |
$lName, |
| 221 |
$prevVisited ); |
| 222 |
if( DB::isError( $res = $this->dbh->query($query) ) ) |
| 223 |
{ |
| 224 |
// FIXXME rollback |
| 225 |
return $this->_throwError( $res->getMessage() , __LINE__ ); |
| 226 |
} |
| 227 |
|
| 228 |
$query = sprintf( 'UPDATE %s SET %s=%s+%s WHERE%s %s>%s', |
| 229 |
$this->table, |
| 230 |
$rName,$rName, |
| 231 |
$numberOfElements*2, |
| 232 |
$this->_getWhereAddOn(), |
| 233 |
$rName, |
| 234 |
$prevVisited ); |
| 235 |
if( DB::isError( $res = $this->dbh->query($query) ) ) |
| 236 |
{ |
| 237 |
// FIXXME rollback |
| 238 |
return $this->_throwError( $res->getMessage() , __LINE__ ); |
| 239 |
} |
| 240 |
return true; |
| 241 |
} |
| 242 |
|
| 243 |
/** |
| 244 |
* remove a tree element |
| 245 |
* this automatically remove all children and their children |
| 246 |
* if a node shall be removed that has children |
| 247 |
* |
| 248 |
* @access public |
| 249 |
* @author Wolfram Kriesing <wolfram@kriesing.de> |
| 250 |
* @param integer $id the id of the element to be removed |
| 251 |
* @return boolean returns either true or throws an error |
| 252 |
*/ |
| 253 |
function remove( $id ) |
| 254 |
{ |
| 255 |
$element = $this->getElement( $id ); |
| 256 |
if( PEAR::isError($element) ) |
| 257 |
return $element; |
| 258 |
|
| 259 |
// FIXXME start transaction |
| 260 |
//$this->dbh->autoCommit(false); |
| 261 |
$query = sprintf( 'DELETE FROM %s WHERE%s %s BETWEEN %s AND %s', |
| 262 |
$this->table, |
| 263 |
$this->_getWhereAddOn(), |
| 264 |
$this->_getColName('left'), |
| 265 |
$element['left'],$element['right']); |
| 266 |
if( DB::isError( $res = $this->dbh->query($query) ) ) |
| 267 |
{ |
| 268 |
// FIXXME rollback |
| 269 |
//$this->dbh->rollback(); |
| 270 |
return $this->_throwError( $res->getMessage() , __LINE__ ); |
| 271 |
} |
| 272 |
|
| 273 |
if( PEAR::isError($err=$this->_remove( $element )) ) |
| 274 |
{ |
| 275 |
// FIXXME rollback |
| 276 |
//$this->dbh->rollback(); |
| 277 |
return $err; |
| 278 |
} |
| 279 |
return true; |
| 280 |
} |
| 281 |
|
| 282 |
/** |
| 283 |
* removes a tree element, but only updates the left/right values |
| 284 |
* to make it seem as if the given element would not exist anymore |
| 285 |
* it doesnt remove the row(s) in the db itself! |
| 286 |
* |
| 287 |
* @see getElement() |
| 288 |
* @access private |
| 289 |
* @author Wolfram Kriesing <wolfram@kriesing.de> |
| 290 |
* @param array the entire element returned by "getElement" |
| 291 |
* @return boolean returns either true or throws an error |
| 292 |
*/ |
| 293 |
function _remove( $element ) |
| 294 |
{ |
| 295 |
$delta = $element['right'] - $element['left'] +1; |
| 296 |
$lName = $this->_getColName('left'); |
| 297 |
$rName = $this->_getColName('right'); |
| 298 |
|
| 299 |
// update the elements which will be affected by the remove |
| 300 |
$query = sprintf( "UPDATE %s SET %s=%s-$delta, %s=%s-$delta WHERE%s %s>%s", |
| 301 |
$this->table, |
| 302 |
$lName,$lName, |
| 303 |
$rName,$rName, |
| 304 |
$this->_getWhereAddOn(), |
| 305 |
$lName,$element['left'] ); |
| 306 |
if( DB::isError( $res = $this->dbh->query($query) ) ) |
| 307 |
{ |
| 308 |
// the rollback shall be done by the method calling this one, since it is only private we can do that |
| 309 |
return $this->_throwError( $res->getMessage() , __LINE__ ); |
| 310 |
} |
| 311 |
|
| 312 |
$query = sprintf( "UPDATE %s SET %s=%s-$delta WHERE%s %s<%s AND %s>%s", |
| 313 |
$this->table, |
| 314 |
$rName,$rName, |
| 315 |
$this->_getWhereAddOn(), |
| 316 |
$lName,$element['left'], |
| 317 |
$rName,$element['right'] ); |
| 318 |
if( DB::isError( $res = $this->dbh->query($query) ) ) |
| 319 |
{ |
| 320 |
// the rollback shall be done by the method calling this one, since it is only private |
| 321 |
return $this->_throwError( $res->getMessage() , __LINE__ ); |
| 322 |
} |
| 323 |
// FIXXME commit - should that not also be done in the method calling this one? like when an error occurs? |
| 324 |
//$this->dbh->commit(); |
| 325 |
return true; |
| 326 |
} // end of function |
| 327 |
|
| 328 |
/** |
| 329 |
* move an entry under a given parent or behind a given entry. |
| 330 |
* If a newPrevId is given the newParentId is dismissed! |
| 331 |
* call it either like this: |
| 332 |
* $tree->move( x , y ) |
| 333 |
* to move the element (or entire tree) with the id x |
| 334 |
* under the element with the id y |
| 335 |
* or |
| 336 |
* $tree->move( x , 0 , y ); // ommit the second parameter by setting it to 0 |
| 337 |
* to move the element (or entire tree) with the id x |
| 338 |
* behind the element with the id y |
| 339 |
* or |
| 340 |
* $tree->move( array(x1,x2,x3) , ... |
| 341 |
* the first parameter can also be an array of elements that shall be moved |
| 342 |
* the second and third para can be as described above |
| 343 |
* If you are using the Memory_DBnested then this method would be invain, |
| 344 |
* since Memory.php already does the looping through multiple elements, but if |
| 345 |
* Dynamic_DBnested is used we need to do the looping here |
| 346 |
* |
| 347 |
* @version 2002/06/08 |
| 348 |
* @access public |
| 349 |
* @author Wolfram Kriesing <wolfram@kriesing.de> |
| 350 |
* @param integer the id(s) of the element(s) that shall be moved |
| 351 |
* @param integer the id of the element which will be the new parent |
| 352 |
* @param integer if prevId is given the element with the id idToMove |
| 353 |
* shall be moved _behind_ the element with id=prevId |
| 354 |
* if it is 0 it will be put at the beginning |
| 355 |
* @return mixed true for success, Tree_Error on failure |
| 356 |
*/ |
| 357 |
function move( $idsToMove , $newParentId , $newPrevId=0 ) |
| 358 |
{ |
| 359 |
settype($idsToMove,'array'); |
| 360 |
$errors = array(); |
| 361 |
foreach( $idsToMove as $idToMove ) |
| 362 |
{ |
| 363 |
$ret = $this->_move( $idToMove , $newParentId , $newPrevId ); |
| 364 |
if( PEAR::isError($ret) ) |
| 365 |
$errors[] = $ret; |
| 366 |
} |
| 367 |
// FIXXME the error in a nicer way, or even better let the throwError method do it!!! |
| 368 |
if( sizeof($errors) ) |
| 369 |
{ |
| 370 |
return $this->_throwError(serialize($errors),__LINE__); |
| 371 |
} |
| 372 |
return true; |
| 373 |
} |
| 374 |
|
| 375 |
/** |
| 376 |
* this method moves one tree element |
| 377 |
* |
| 378 |
* @see move() |
| 379 |
* @version 2002/04/29 |
| 380 |
* @access public |
| 381 |
* @author Wolfram Kriesing <wolfram@kriesing.de> |
| 382 |
* @param integer the id of the element that shall be moved |
| 383 |
* @param integer the id of the element which will be the new parent |
| 384 |
* @param integer if prevId is given the element with the id idToMove |
| 385 |
* shall be moved _behind_ the element with id=prevId |
| 386 |
* if it is 0 it will be put at the beginning |
| 387 |
* @return mixed true for success, Tree_Error on failure |
| 388 |
*/ |
| 389 |
function _move( $idToMove , $newParentId , $newPrevId=0 ) |
| 390 |
{ |
| 391 |
// do some integrity checks first |
| 392 |
if ($newPrevId) { |
| 393 |
if ($newPrevId==$idToMove) { // dont let people move an element behind itself, tell it succeeded, since it already is there :-) |
| 394 |
return true; |
| 395 |
} |
| 396 |
if (PEAR::isError($newPrevious=$this->getElement($newPrevId))) { |
| 397 |
return $newPrevious; |
| 398 |
} |
| 399 |
$newParentId = $newPrevious['parentId']; |
| 400 |
} else { |
| 401 |
if ($newParentId==0) { |
| 402 |
return $this->_throwError( 'no parent id given' , __LINE__ ); |
| 403 |
} |
| 404 |
if ($this->isChildOf($idToMove,$newParentId)) { // if the element shall be moved under one of its children, return false |
| 405 |
return $this->_throwError( 'can not move an element under one of its children' , __LINE__ ); |
| 406 |
} |
| 407 |
if ($newParentId==$idToMove) { // dont do anything to let an element be moved under itself, which is bullshit |
| 408 |
return true; |
| 409 |
} |
| 410 |
if (PEAR::isError($newParent=$this->getElement($newParentId))) { // try to retreive the data of the parent element |
| 411 |
return $newParent; |
| 412 |
} |
| 413 |
} |
| 414 |
|
| 415 |
if (PEAR::isError($element=$this->getElement($idToMove))) { // get the data of the element itself |
| 416 |
return $element; |
| 417 |
} |
| 418 |
|
| 419 |
$numberOfElements = ($element['right'] - $element['left']+1)/2; |
| 420 |
$prevVisited = $newPrevId ? $newPrevious['right'] : $newParent['left']; |
| 421 |
|
| 422 |
// FIXXME start transaction |
| 423 |
|
| 424 |
// add the left/right values in the new parent, to have the space to move the new values in |
| 425 |
if (PEAR::isError($err=$this->_add( $prevVisited , $numberOfElements ))) { |
| 426 |
// FIXXME rollback |
| 427 |
//$this->dbh->rollback(); |
| 428 |
return $err; |
| 429 |
} |
| 430 |
|
| 431 |
// update the parentId of the element with $idToMove |
| 432 |
if (PEAR::isError($err=$this->update($idToMove,array('parentId'=>$newParentId)))) { |
| 433 |
// FIXXME rollback |
| 434 |
//$this->dbh->rollback(); |
| 435 |
return $err; |
| 436 |
} |
| 437 |
|
| 438 |
// update the lefts and rights of those elements that shall be moved |
| 439 |
|
| 440 |
// first get the offset we need to add to the left/right values |
| 441 |
// if $newPrevId is given we need to get the right value, otherwise the left |
| 442 |
// since the left/right has changed, because we already updated it up there we need to |
| 443 |
// get them again, we have to do that anyway, to have the proper new left/right values |
| 444 |
if ($newPrevId) { |
| 445 |
if (PEAR::isError($temp = $this->getElement( $newPrevId ))) { |
| 446 |
// FIXXME rollback |
| 447 |
//$this->dbh->rollback(); |
| 448 |
return $temp; |
| 449 |
} |
| 450 |
$calcWith = $temp['right']; |
| 451 |
} else { |
| 452 |
if (PEAR::isError($temp=$this->getElement($newParentId))) { |
| 453 |
// FIXXME rollback |
| 454 |
//$this->dbh->rollback(); |
| 455 |
return $temp; |
| 456 |
} |
| 457 |
$calcWith = $temp['left']; |
| 458 |
} |
| 459 |
|
| 460 |
// get the element that shall be moved again, since the left and right might have changed by the add-call |
| 461 |
if (PEAR::isError($element=$this->getElement($idToMove))) { |
| 462 |
return $element; |
| 463 |
} |
| 464 |
|
| 465 |
$offset = $calcWith - $element['left']; // calc the offset that the element to move has to the spot where it should go |
| 466 |
$offset++; // correct the offset by one, since it needs to go inbetween! |
| 467 |
|
| 468 |
$lName = $this->_getColName('left'); |
| 469 |
$rName = $this->_getColName('right'); |
| 470 |
$query = sprintf( "UPDATE %s SET %s=%s+$offset,%s=%s+$offset WHERE%s %s>%s AND %s<%s", |
| 471 |
$this->table, |
| 472 |
$rName,$rName, |
| 473 |
$lName,$lName, |
| 474 |
$this->_getWhereAddOn(), |
| 475 |
$lName,$element['left']-1, |
| 476 |
$rName,$element['right']+1 ); |
| 477 |
if (DB::isError($res=$this->dbh->query($query))) { |
| 478 |
// FIXXME rollback |
| 479 |
//$this->dbh->rollback(); |
| 480 |
return $this->_throwError( $res->getMessage() , __LINE__ ); |
| 481 |
} |
| 482 |
|
| 483 |
// remove the part of the tree where the element(s) was/were before |
| 484 |
if (PEAR::isError($err=$this->_remove($element))) { |
| 485 |
// FIXXME rollback |
| 486 |
//$this->dbh->rollback(); |
| 487 |
return $err; |
| 488 |
} |
| 489 |
// FIXXME commit all changes |
| 490 |
//$this->dbh->commit(); |
| 491 |
|
| 492 |
return true; |
| 493 |
} // end of function |
| 494 |
|
| 495 |
/** |
| 496 |
* update the tree element given by $id with the values in $newValues |
| 497 |
* |
| 498 |
* @access public |
| 499 |
* @author Wolfram Kriesing <wolfram@kriesing.de> |
| 500 |
* @param int the id of the element to update |
| 501 |
* @param array the new values, the index is the col name |
| 502 |
* @return mixed either true or an Tree_Error |
| 503 |
*/ |
| 504 |
function update($id,$newValues) |
| 505 |
{ |
| 506 |
// jsut to be sure nothing gets screwed up :-) |
| 507 |
unset($newValues[$this->_getColName('left')]); |
| 508 |
unset($newValues[$this->_getColName('right')]); |
| 509 |
unset($newValues[$this->_getColName('parentId')]); |
| 510 |
|
| 511 |
// updating _one_ element in the tree |
| 512 |
$values = array(); |
| 513 |
foreach ($newValues as $key=>$value) { |
| 514 |
$values[] = $this->_getColName($key).'='.$this->dbh->quote($value); |
| 515 |
} |
| 516 |
$query = sprintf( 'UPDATE %s SET %s WHERE%s %s=%s', |
| 517 |
$this->table, |
| 518 |
implode(',',$values), |
| 519 |
$this->_getWhereAddOn(), |
| 520 |
$this->_getColName('id'), |
| 521 |
$id); |
| 522 |
if (DB::isError( $res=$this->dbh->query($query))) { |
| 523 |
// FIXXME raise PEAR error |
| 524 |
return $this->_throwError( $res->getMessage() , __LINE__ ); |
| 525 |
} |
| 526 |
|
| 527 |
return true; |
| 528 |
} // end of function |
| 529 |
|
| 530 |
/** |
| 531 |
* copy a subtree/node/... under a new parent or/and behind a given element |
| 532 |
* |
| 533 |
* |
| 534 |
* @access public |
| 535 |
* @author Wolfram Kriesing <wolfram@kriesing.de> |
| 536 |
* @param |
| 537 |
* @return |
| 538 |
*/ |
| 539 |
function copy( $id , $parentId=0 , $prevId=0 ) |
| 540 |
{ |
| 541 |
return $this->_throwError( 'copy-method is not implemented yet!' , __LINE__ ); |
| 542 |
// get element tree |
| 543 |
// $this->addTree |
| 544 |
} // end of function |
| 545 |
|
| 546 |
|
| 547 |
/** |
| 548 |
* get the root |
| 549 |
* |
| 550 |
* @access public |
| 551 |
* @version 2002/03/02 |
| 552 |
* @author Wolfram Kriesing <wolfram@kriesing.de> |
| 553 |
* @param |
| 554 |
* @return mixed either the data of the root element or an Tree_Error |
| 555 |
*/ |
| 556 |
function getRoot() |
| 557 |
{ |
| 558 |
$query = sprintf( 'SELECT * FROM %s WHERE%s %s=1', |
| 559 |
$this->table, |
| 560 |
$this->_getWhereAddOn(), |
| 561 |
$this->_getColName('left')); |
| 562 |
if( DB::isError( $res = $this->dbh->getRow($query) ) ) |
| 563 |
{ |
| 564 |
return $this->_throwError( $res->getMessage() , __LINE__ ); |
| 565 |
} |
| 566 |
return $this->_prepareResult( $res ); |
| 567 |
} // end of function |
| 568 |
|
| 569 |
/** |
| 570 |
* |
| 571 |
* |
| 572 |
* @access public |
| 573 |
* @version 2002/03/02 |
| 574 |
* @author Wolfram Kriesing <wolfram@kriesing.de> |
| 575 |
* @param |
| 576 |
* @return mixed either the data of the requested element or an Tree_Error |
| 577 |
*/ |
| 578 |
function getElement( $id ) |
| 579 |
{ |
| 580 |
$query = sprintf( 'SELECT * FROM %s WHERE%s %s=%s', |
| 581 |
$this->table, |
| 582 |
$this->_getWhereAddOn(), |
| 583 |
$this->_getColName('id'), |
| 584 |
$id); |
| 585 |
if( DB::isError( $res = $this->dbh->getRow($query) ) ) |
| 586 |
{ |
| 587 |
return $this->_throwError( $res->getMessage() , __LINE__ ); |
| 588 |
} |
| 589 |
if( !$res ) |
| 590 |
return $this->_throwError( "Element with id $id does not exist!" , __LINE__ ); |
| 591 |
|
| 592 |
return $this->_prepareResult( $res ); |
| 593 |
} // end of function |
| 594 |
|
| 595 |
/** |
| 596 |
* |
| 597 |
* |
| 598 |
* @access public |
| 599 |
* @version 2002/03/02 |
| 600 |
* @author Wolfram Kriesing <wolfram@kriesing.de> |
| 601 |
* @param |
| 602 |
* @return mixed either the data of the requested element or an Tree_Error |
| 603 |
*/ |
| 604 |
function getChild($id) |
| 605 |
{ |
| 606 |
// subqueries would be cool :-) |
| 607 |
$curElement = $this->getElement( $id ); |
| 608 |
if (PEAR::isError($curElement)) { |
| 609 |
return $curElement; |
| 610 |
} |
| 611 |
|
| 612 |
$query = sprintf( 'SELECT * FROM %s WHERE%s %s=%s', |
| 613 |
$this->table, |
| 614 |
$this->_getWhereAddOn(), |
| 615 |
$this->_getColName('left'), |
| 616 |
$curElement['left']+1 ); |
| 617 |
if (DB::isError( $res = $this->dbh->getRow($query))) { |
| 618 |
return $this->_throwError( $res->getMessage() , __LINE__ ); |
| 619 |
} |
| 620 |
return $this->_prepareResult( $res ); |
| 621 |
} |
| 622 |
|
| 623 |
/** |
| 624 |
* gets the path from the element with the given id down |
| 625 |
* to the root |
| 626 |
* the returned array is sorted to start at root |
| 627 |
* for simply walking through and retreiving the path |
| 628 |
* |
| 629 |
* @access public |
| 630 |
* @version 2002/03/02 |
| 631 |
* @author Wolfram Kriesing <wolfram@kriesing.de> |
| 632 |
* @param |
| 633 |
* @return mixed either the data of the requested elements or an Tree_Error |
| 634 |
*/ |
| 635 |
function getPath( $id ) |
| 636 |
{ |
| 637 |
// subqueries would be cool :-) |
| 638 |
$curElement = $this->getElement( $id ); |
| 639 |
|
| 640 |
$query = sprintf( 'SELECT * FROM %s WHERE%s %s<=%s AND %s>=%s ORDER BY %s', |
| 641 |
$this->table, |
| 642 |
$this->_getWhereAddOn(), |
| 643 |
$this->_getColName('left'), |
| 644 |
$curElement['left'], |
| 645 |
$this->_getColName('right'), |
| 646 |
$curElement['right'], |
| 647 |
$this->_getColName('left') ); |
| 648 |
if (DB::isError( $res = $this->dbh->getAll($query))) { |
| 649 |
return $this->_throwError( $res->getMessage() , __LINE__ ); |
| 650 |
} |
| 651 |
return $this->_prepareResults( $res ); |
| 652 |
} |
| 653 |
|
| 654 |
/** |
| 655 |
* gets the element to the left, the left visit |
| 656 |
* |
| 657 |
* @access public |
| 658 |
* @version 2002/03/07 |
| 659 |
* @author Wolfram Kriesing <wolfram@kriesing.de> |
| 660 |
* @param |
| 661 |
* @return mixed either the data of the requested element or an Tree_Error |
| 662 |
*/ |
| 663 |
function getLeft( $id ) |
| 664 |
{ |
| 665 |
$element = $this->getElement( $id ); |
| 666 |
if( PEAR::isError($element) ) |
| 667 |
return $element; |
| 668 |
|
| 669 |
$query = sprintf( 'SELECT * FROM %s WHERE%s (%s=%s OR %s=%s)', |
| 670 |
$this->table, |
| 671 |
$this->_getWhereAddOn(), |
| 672 |
$this->_getColName('right'),$element['left']-1, |
| 673 |
$this->_getColName('left'),$element['left']-1 ); |
| 674 |
if( DB::isError( $res = $this->dbh->getRow($query) ) ) |
| 675 |
{ |
| 676 |
return $this->_throwError( $res->getMessage() , __LINE__ ); |
| 677 |
} |
| 678 |
return $this->_prepareResult( $res ); |
| 679 |
} |
| 680 |
|
| 681 |
/** |
| 682 |
* gets the element to the right, the right visit |
| 683 |
* |
| 684 |
* @access public |
| 685 |
* @version 2002/03/07 |
| 686 |
* @author Wolfram Kriesing <wolfram@kriesing.de> |
| 687 |
* @param |
| 688 |
* @return mixed either the data of the requested element or an Tree_Error |
| 689 |
*/ |
| 690 |
function getRight( $id ) |
| 691 |
{ |
| 692 |
$element = $this->getElement( $id ); |
| 693 |
if( PEAR::isError($element) ) |
| 694 |
return $element; |
| 695 |
|
| 696 |
$query = sprintf( 'SELECT * FROM %s WHERE%s (%s=%s OR %s=%s)', |
| 697 |
$this->table, |
| 698 |
$this->_getWhereAddOn(), |
| 699 |
$this->_getColName('left'),$element['right']+1, |
| 700 |
$this->_getColName('right'),$element['right']+1); |
| 701 |
if( DB::isError( $res = $this->dbh->getRow($query) ) ) |
| 702 |
{ |
| 703 |
return $this->_throwError( $res->getMessage() , __LINE__ ); |
| 704 |
} |
| 705 |
return $this->_prepareResult( $res ); |
| 706 |
} |
| 707 |
|
| 708 |
/** |
| 709 |
* get the parent of the element with the given id |
| 710 |
* |
| 711 |
* @access public |
| 712 |
* @version 2002/04/15 |
| 713 |
* @author Wolfram Kriesing <wolfram@kriesing.de> |
| 714 |
* @param |
| 715 |
* @return mixed the array with the data of the parent element |
| 716 |
* or false, if there is no parent, if the element is the root |
| 717 |
* or an Tree_Error |
| 718 |
*/ |
| 719 |
function getParent( $id ) |
| 720 |
{ |
| 721 |
$query = sprintf( 'SELECT p.* FROM %s p,%s e WHERE%s e.%s=p.%s AND e.%s=%s', |
| 722 |
$this->table,$this->table, |
| 723 |
$this->_getWhereAddOn( ' AND ' , 'p' ), |
| 724 |
$this->_getColName('parentId'), |
| 725 |
$this->_getColName('id'), |
| 726 |
$this->_getColName('id'), |
| 727 |
$id); |
| 728 |
if( DB::isError( $res = $this->dbh->getRow($query) ) ) |
| 729 |
{ |
| 730 |
return $this->_throwError( $res->getMessage() , __LINE__ ); |
| 731 |
} |
| 732 |
return $this->_prepareResult( $res ); |
| 733 |
} |
| 734 |
|
| 735 |
/** |
| 736 |
* get the children of the given element |
| 737 |
* or if the parameter is an array, it gets the children of all |
| 738 |
* the elements given by their ids in the array |
| 739 |
* |
| 740 |
* @access public |
| 741 |
* @version 2002/04/15 |
| 742 |
* @author Wolfram Kriesing <wolfram@kriesing.de> |
| 743 |
* @param mixed (1) int the id of one element |
| 744 |
* (2) array an array of ids for which |
| 745 |
* the children will be returned |
| 746 |
* @param integer the children of how many levels shall be returned |
| 747 |
* @return mixed the array with the data of all children |
| 748 |
* or false, if there are none |
| 749 |
*/ |
| 750 |
function getChildren($ids,$levels=1) |
| 751 |
{ |
| 752 |
$res = array(); |
| 753 |
for( $i=1 ; $i<$levels+1 ; $i++ ) |
| 754 |
{ |
| 755 |
// if $ids is an array implode the values |
| 756 |
$getIds = is_array($ids) ? implode(',',$ids) : $ids; |
| 757 |
|
| 758 |
$query = sprintf( 'SELECT c.* FROM %s c,%s e WHERE%s e.%s=c.%s AND e.%s IN (%s) '. |
| 759 |
'ORDER BY c.%s', |
| 760 |
$this->table,$this->table, |
| 761 |
$this->_getWhereAddOn( ' AND ' , 'c' ), |
| 762 |
$this->_getColName('id'), |
| 763 |
$this->_getColName('parentId'), |
| 764 |
$this->_getColName('id'), |
| 765 |
$getIds, |
| 766 |
// order by left, so we have it in the order as it is in the tree |
| 767 |
// if no 'order'-option is given |
| 768 |
$this->getOption('order') ? $this->getOption('order') : $this->_getColName('left') |
| 769 |
); |
| 770 |
if (DB::isError($_res = $this->dbh->getAll($query))) { |
| 771 |
return $this->_throwError( $_res->getMessage() , __LINE__ ); |
| 772 |
} |
| 773 |
$_res = $this->_prepareResults( $_res ); |
| 774 |
|
| 775 |
// we use the id as the index, to make the use easier esp. for multiple return-values |
| 776 |
$tempRes = array(); |
| 777 |
foreach ($_res as $aRes) { |
| 778 |
$tempRes[$aRes[$this->_getColName('id')]] = $aRes; |
| 779 |
} |
| 780 |
$_res = $tempRes; |
| 781 |
|
| 782 |
// |
| 783 |
if ($levels>1) { |
| 784 |
$ids = array(); |
| 785 |
foreach( $_res as $aRes ) |
| 786 |
$ids[] = $aRes[$this->_getColName('id')]; |
| 787 |
} |
| 788 |
$res = array_merge($res,$_res); |
| 789 |
|
| 790 |
// quit the for-loop if there are no children in the current level |
| 791 |
if (!sizeof($ids)) { |
| 792 |
break; |
| 793 |
} |
| 794 |
} |
| 795 |
return $res; |
| 796 |
} |
| 797 |
|
| 798 |
/** |
| 799 |
* get the next element on the same level |
| 800 |
* if there is none return false |
| 801 |
* |
| 802 |
* @access public |
| 803 |
* @version 2002/04/15 |
| 804 |
* @author Wolfram Kriesing <wolfram@kriesing.de> |
| 805 |
* @param |
| 806 |
* @return mixed the array with the data of the next element |
| 807 |
* or false, if there is no next |
| 808 |
* or Tree_Error |
| 809 |
*/ |
| 810 |
function getNext( $id ) |
| 811 |
{ |
| 812 |
$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', |
| 813 |
$this->table,$this->table, |
| 814 |
$this->_getWhereAddOn( ' AND ' , 'n' ), |
| 815 |
$this->_getColName('right'), |
| 816 |
$this->_getColName('left'), |
| 817 |
$this->_getColName('parentId'), |
| 818 |
$this->_getColName('parentId'), |
| 819 |
$this->_getColName('id'), |
| 820 |
$id); |
| 821 |
if( DB::isError( $res = $this->dbh->getRow($query) ) ) |
| 822 |
{ |
| 823 |
return $this->_throwError( $res->getMessage() , __LINE__ ); |
| 824 |
} |
| 825 |
if( !$res ) |
| 826 |
return false; |
| 827 |
return $this->_prepareResult( $res ); |
| 828 |
} |
| 829 |
|
| 830 |
/** |
| 831 |
* get the previous element on the same level |
| 832 |
* if there is none return false |
| 833 |
* |
| 834 |
* @access public |
| 835 |
* @version 2002/04/15 |
| 836 |
* @author Wolfram Kriesing <wolfram@kriesing.de> |
| 837 |
* @param |
| 838 |
* @return mixed the array with the data of the previous element |
| 839 |
* or false, if there is no previous |
| 840 |
* or a Tree_Error |
| 841 |
*/ |
| 842 |
function getPrevious( $id ) |
| 843 |
{ |
| 844 |
$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', |
| 845 |
$this->table,$this->table, |
| 846 |
$this->_getWhereAddOn( ' AND ' , 'p' ), |
| 847 |
$this->_getColName('left'), |
| 848 |
$this->_getColName('right'), |
| 849 |
$this->_getColName('parentId'), |
| 850 |
$this->_getColName('parentId'), |
| 851 |
$this->_getColName('id'), |
| 852 |
$id); |
| 853 |
if( DB::isError( $res = $this->dbh->getRow($query) ) ) |
| 854 |
{ |
| 855 |
return $this->_throwError( $res->getMessage() , __LINE__ ); |
| 856 |
} |
| 857 |
if( !$res ) |
| 858 |
return false; |
| 859 |
return $this->_prepareResult( $res ); |
| 860 |
} |
| 861 |
|
| 862 |
/** |
| 863 |
* returns if $childId is a child of $id |
| 864 |
* |
| 865 |
* @abstract |
| 866 |
* @version 2002/04/29 |
| 867 |
* @access public |
| 868 |
* @author Wolfram Kriesing <wolfram@kriesing.de> |
| 869 |
* @param int id of the element |
| 870 |
* @param int id of the element to check if it is a child |
| 871 |
* @return boolean true if it is a child |
| 872 |
*/ |
| 873 |
function isChildOf( $id , $childId ) |
| 874 |
{ |
| 875 |
// check simply if the left and right of the child are within the |
| 876 |
// left and right of the parent, if so it definitly is a child :-) |
| 877 |
$parent = $this->getElement($id); |
| 878 |
$child = $this->getElement($childId); |
| 879 |
|
| 880 |
if( $parent['left'] < $child['left'] && |
| 881 |
$parent['right'] > $child['right'] ) |
| 882 |
{ |
| 883 |
return true; |
| 884 |
} |
| 885 |
|
| 886 |
return false; |
| 887 |
} // end of function |
| 888 |
|
| 889 |
/** |
| 890 |
* return the maximum depth of the tree |
| 891 |
* |
| 892 |
* @version 2003/02/25 |
| 893 |
* @access public |
| 894 |
* @author Wolfram Kriesing <wolfram@kriesing.de> |
| 895 |
* @return int the depth of the tree |
| 896 |
*/ |
| 897 |
function getDepth() |
| 898 |
{ |
| 899 |
// FIXXXME TODO!!! |
| 900 |
return $this->_throwError( 'not implemented yet' , __LINE__ ); |
| 901 |
} |
| 902 |
|
| 903 |
/** |
| 904 |
* Tells if the node with the given ID has children. |
| 905 |
* |
| 906 |
* @version 2003/03/04 |
| 907 |
* @access public |
| 908 |
* @author Wolfram Kriesing <wolfram@kriesing.de> |
| 909 |
* @param integer the ID of a node |
| 910 |
* @return boolean if the node with the given id has children |
| 911 |
*/ |
| 912 |
function hasChildren($id) |
| 913 |
{ |
| 914 |
$element = $this->getElement($id); |
| 915 |
return $element['right']-$element['left']>1; // if the diff between left and right>1 then there are children |
| 916 |
} |
| 917 |
|
| 918 |
|
| 919 |
// |
| 920 |
// PRIVATE METHODS |
| 921 |
// |
| 922 |
|
| 923 |
|
| 924 |
/** |
| 925 |
* |
| 926 |
* |
| 927 |
* @access private |
| 928 |
* @version 2002/04/20 |
| 929 |
* @author Wolfram Kriesing <wolfram@kriesing.de> |
| 930 |
* @param string the current where clause |
| 931 |
* @return |
| 932 |
*/ |
| 933 |
function _getWhereAddOn( $addAfter=' AND ' , $tableName='' ) |
| 934 |
{ |
| 935 |
if( $where=$this->getOption('whereAddOn') ) |
| 936 |
{ |
| 937 |
return ' '.($tableName?$tableName.'.':'')." $where$addAfter "; |
| 938 |
} |
| 939 |
return ''; |
| 940 |
} |
| 941 |
|
| 942 |
|
| 943 |
|
| 944 |
|
| 945 |
// for compatibility to Memory methods |
| 946 |
function getFirstRoot() |
| 947 |
{ |
| 948 |
return $this->getRoot(); |
| 949 |
} |
| 950 |
/** |
| 951 |
* gets the tree under the given element in one array, sorted |
| 952 |
* so you can go through the elements from begin to end and list them |
| 953 |
* as they are in the tree, where every child (until the deepest) is retreived |
| 954 |
* |
| 955 |
* @see &_getNode() |
| 956 |
* @access public |
| 957 |
* @version 2001/12/17 |
| 958 |
* @author Wolfram Kriesing <wolfram@kriesing.de> |
| 959 |
* @param integer $startId the id where to start walking |
| 960 |
* @param integer $depth this number says how deep into the structure the elements shall be retreived |
| 961 |
* @return array sorted as listed in the tree |
| 962 |
*/ |
| 963 |
function &getNode( $startId=0 , $depth=0 ) |
| 964 |
{ |
| 965 |
} |
| 966 |
|
| 967 |
} |
| 968 |
?> |