DBnested.php 39 KB

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