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