Subversion-Projekte lars-tiefland.php_share

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 1
<?php
2
/* vim: set expandtab tabstop=4 shiftwidth=4: */
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.39.2.2 2009/03/12 17:19:54 dufuz Exp $
20
 
21
require_once 'Tree/OptionsDB.php';
22
 
23
/**
24
 * This class implements methods to work on a tree saved using the nested
25
 * tree model.
26
 * explaination: http://research.calacademy.org/taf/proceedings/ballew/index.htm
27
 *
28
 * @access     public
29
 * @package    Tree
30
 */
31
class Tree_Dynamic_DBnested extends Tree_OptionsDB
32
{
33
 
34
    // {{{ properties
35
    var $debug = 0;
36
 
37
    var $options = array(
38
        // FIXXME to be implemented
39
        // add on for the where clause, this string is simply added
40
        // behind the WHERE in the select so you better make sure
41
        // its correct SQL :-), i.e. 'uid=3'
42
        // this is needed i.e. when you are saving many trees in one db-table
43
        'whereAddOn'=>'',
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
47
        // to the real column name using this array here, if it stays empty
48
        // the internal names are used, which are:
49
        // id, left, right
50
        'columnNameMaps'=>array(
51
                            // since mysql at least doesnt support 'left' ...
52
                            'left'      =>  'l',
53
                            // ...as a column name we set default to the first
54
                            //letter only
55
                            'right'     =>  'r',
56
                            // parent id
57
                            'parentId'  =>  'parent'
58
                       ),
59
        // needed for sorting the tree, currently only used in Memory_DBnested
60
        'order'    => ''
61
       );
62
 
63
    // }}}
64
    // {{{ __construct()
65
 
66
    // the defined methods here are proposals for the implementation,
67
    // they are named the same, as the methods in the "Memory" branch.
68
    // If possible it would be cool to keep the same naming. And
69
    // if the same parameters would be possible too then it would be
70
    // even better, so one could easily change from any kind
71
    // of tree-implementation to another, without changing the source
72
    // code, only the setupXXX would need to be changed
73
    /**
74
      *
75
      *
76
      * @access     public
77
      * @version    2002/03/02
78
      * @author     Wolfram Kriesing <wolfram@kriesing.de>
79
      * @param      string  the DSN for the DB connection
80
      * @return     void
81
      */
82
    function __construct($dsn, $options = array())
83
    {
84
        $this->Tree_Dynamic_DBnested($dsn, $options);
85
    }
86
 
87
    // }}}
88
    // {{{ Tree_Dynamic_DBnested()
89
 
90
    /**
91
     *
92
     *
93
     * @access     public
94
     * @version    2002/03/02
95
     * @author     Wolfram Kriesing <wolfram@kriesing.de>
96
     * @param      string  the DSN for the DB connection
97
     * @return     void
98
     */
99
    function Tree_Dynamic_DBnested($dsn, $options = array())
100
    {
101
        parent::Tree_OptionsDB($dsn, $options); // instanciate DB
102
        $this->table = $this->getOption('table');
103
    }
104
 
105
    // }}}
106
    // {{{ add()
107
 
108
    /**
109
     * add a new element to the tree
110
     * there are three ways to use this method
111
     * Method 1:
112
     * Give only the $parentId and the $newValues will be inserted
113
     * as the first child of this parent
114
     * <code>
115
     * // insert a new element under the parent with the ID=7
116
     * $tree->add(array('name'=>'new element name'), 7);
117
     * </code>
118
     *
119
     * Method 2:
120
     * Give the $prevId ($parentId will be dismissed) and the new element
121
     * will be inserted in the tree after the element with the ID=$prevId
122
     * the parentId is not necessary because the prevId defines exactly where
123
     * the new element has to be place in the tree, and the parent is
124
     * the same as for the element with the ID=$prevId
125
     * <code>
126
     * // insert a new element after the element with the ID=5
127
     * $tree->add(array('name'=>'new'), 0, 5);
128
     * </code>
129
     *
130
     * Method 3:
131
     * neither $parentId nor prevId is given, then the root element will be
132
     * inserted. This requires that programmer is responsible to confirm this.
133
     * This method does not yet check if there is already a root element saved!
134
     *
135
     * @access     public
136
     * @author     Wolfram Kriesing <wolfram@kriesing.de>
137
     * @param   array   $newValues  this array contains the values that shall
138
     *                              be inserted in the db-table
139
     * @param   integer $parentId   the id of the element which shall be
140
     *                              the parent of the new element
141
     * @param   integer $prevId     the id of the element which shall preceed
142
     *                              the one to be inserted use either
143
     *                              'parentId' or 'prevId'.
144
     * @return     integer the ID of the element that had been inserted
145
     */
146
    function add($newValues, $parentId = 0, $prevId = 0)
147
    {
148
        $lName = $this->_getColName('left');
149
        $rName = $this->_getColName('right');
150
        $prevVisited = 0;
151
 
152
        // check the DB-table if the columns which are given as keys
153
        // in the array $newValues do really exist, if not remove them
154
        // from the array
155
        // FIXXME do the above described
156
        // if no parent and no prevId is given the root shall be added
157
        if ($parentId || $prevId) {
158
            if ($prevId) {
159
                $element = $this->getElement($prevId);
160
                // we also need the parent id of the element
161
                // to write it in the db
162
                $parentId = $element['parentId'];
163
            } else {
164
                $element = $this->getElement($parentId);
165
            }
166
            $newValues['parentId'] = $parentId;
167
 
168
            if (Tree::isError($element)) {
169
                return $element;
170
            }
171
 
172
            // get the "visited"-value where to add the new element behind
173
            // if $prevId is given, we need to use the right-value
174
            // if only the $parentId is given we need to use the left-value
175
            // look at it graphically, that made me understand it :-)
176
            // See:
177
            // http://research.calacademy.org/taf/proceedings/ballew/sld034.htm
178
            $prevVisited = $prevId ? $element['right'] : $element['left'];
179
 
180
            // FIXXME start transaction here
181
            if (Tree::isError($err = $this->_add($prevVisited, 1))) {
182
                // FIXXME rollback
183
                //$this->dbh->rollback();
184
                return $err;
185
            }
186
        }
187
 
188
        // inserting _one_ new element in the tree
189
        $newData = array();
190
        // quote the values, as needed for the insert
191
        foreach ($newValues as $key => $value) {
192
            $newData[$this->_getColName($key)] = $this->dbh->quoteSmart($value);
193
        }
194
 
195
        // set the proper right and left values
196
        $newData[$lName] = $prevVisited + 1;
197
        $newData[$rName] = $prevVisited + 2;
198
 
199
        // use sequences to create a new id in the db-table
200
        $nextId = $this->dbh->nextId($this->table);
201
        $query = sprintf('INSERT INTO %s (%s,%s) VALUES (%s,%s)',
202
                            $this->table ,
203
                            $this->_getColName('id'),
204
                            implode(',', array_keys($newData)) ,
205
                            $nextId,
206
                            implode(',', $newData)
207
                        );
208
        if (DB::isError($res = $this->dbh->query($query))) {
209
            // rollback
210
            return $this->_throwError($res->getMessage(), __LINE__);
211
        }
212
        // commit here
213
 
214
        return $nextId;
215
    }
216
 
217
    // }}}
218
    // {{{ _add()
219
 
220
    /**
221
     * this method only updates the left/right values of all the
222
     * elements that are affected by the insertion
223
     * be sure to set the parentId of the element(s) you insert
224
     *
225
     * @param  int     this parameter is not the ID!!!
226
     *                 it is the previous visit number, that means
227
     *                 if you are inserting a child, you need to use the left-value
228
     *                 of the parent
229
     *                 if you are inserting a "next" element, on the same level
230
     *                 you need to give the right value !!
231
     * @param  int     the number of elements you plan to insert
232
     * @return mixed   either true on success or a Tree_Error on failure
233
     */
234
    function _add($prevVisited, $numberOfElements = 1)
235
    {
236
        $lName = $this->_getColName('left');
237
        $rName = $this->_getColName('right');
238
 
239
        // update the elements which will be affected by the new insert
240
        $query = sprintf('UPDATE %s SET %s=%s+%s WHERE%s %s>%s',
241
                            $this->table,
242
                            $lName,
243
                            $lName,
244
                            $numberOfElements*2,
245
                            $this->_getWhereAddOn(),
246
                            $lName,
247
                            $prevVisited);
248
        if (DB::isError($res = $this->dbh->query($query))) {
249
            // FIXXME rollback
250
            return $this->_throwError($res->getMessage(), __LINE__);
251
        }
252
 
253
        $query = sprintf('UPDATE %s SET %s=%s+%s WHERE%s %s>%s',
254
                            $this->table,
255
                            $rName,$rName,
256
                            $numberOfElements*2,
257
                            $this->_getWhereAddOn(),
258
                            $rName,
259
                            $prevVisited);
260
        if (DB::isError($res = $this->dbh->query($query))) {
261
            // FIXXME rollback
262
            return $this->_throwError($res->getMessage(), __LINE__);
263
        }
264
        return true;
265
    }
266
 
267
    // }}}
268
    // {{{ remove()
269
 
270
    /**
271
     * remove a tree element
272
     * this automatically remove all children and their children
273
     * if a node shall be removed that has children
274
     *
275
     * @access     public
276
     * @author     Wolfram Kriesing <wolfram@kriesing.de>
277
     * @param      integer $id the id of the element to be removed
278
     * @return     boolean returns either true or throws an error
279
     */
280
    function remove($id)
281
    {
282
        $element = $this->getElement($id);
283
        if (Tree::isError($element)) {
284
            return $element;
285
        }
286
 
287
        // FIXXME start transaction
288
        //$this->dbh->autoCommit(false);
289
        $query = sprintf('DELETE FROM %s WHERE%s %s BETWEEN %s AND %s',
290
                            $this->table,
291
                            $this->_getWhereAddOn(),
292
                            $this->_getColName('left'),
293
                            $element['left'],$element['right']);
294
        if (DB::isError($res = $this->dbh->query($query))) {
295
            // FIXXME rollback
296
            //$this->dbh->rollback();
297
            return $this->_throwError($res->getMessage(), __LINE__);
298
        }
299
 
300
        if (Tree::isError($err = $this->_remove($element))) {
301
            // FIXXME rollback
302
            //$this->dbh->rollback();
303
            return $err;
304
        }
305
        return true;
306
    }
307
 
308
    // }}}
309
    // {{{ _remove()
310
 
311
    /**
312
     * removes a tree element, but only updates the left/right values
313
     * to make it seem as if the given element would not exist anymore
314
     * it doesnt remove the row(s) in the db itself!
315
     *
316
     * @see        getElement()
317
     * @access     private
318
     * @author     Wolfram Kriesing <wolfram@kriesing.de>
319
     * @param      array   the entire element returned by "getElement"
320
     * @return     boolean returns either true or throws an error
321
     */
322
    function _remove($element)
323
    {
324
        $delta = $element['right'] - $element['left'] + 1;
325
        $lName = $this->_getColName('left');
326
        $rName = $this->_getColName('right');
327
 
328
        // update the elements which will be affected by the remove
329
        $query = sprintf("UPDATE
330
                                %s
331
                            SET
332
                                %s=%s-$delta,
333
                                %s=%s-$delta
334
                            WHERE%s %s>%s",
335
                            $this->table,
336
                            $lName,$lName,
337
                            $rName,$rName,
338
                            $this->_getWhereAddOn(),
339
                            $lName,$element['left']);
340
        if (DB::isError($res = $this->dbh->query($query))) {
341
            // the rollback shall be done by the method calling this one
342
            // since it is only private we can do that
343
            return $this->_throwError($res->getMessage(), __LINE__);
344
        }
345
 
346
        $query = sprintf("UPDATE
347
                                %s
348
                            SET %s=%s-$delta
349
                            WHERE
350
                                %s %s < %s
351
                                AND
352
                                %s>%s",
353
                            $this->table,
354
                            $rName,$rName,
355
                            $this->_getWhereAddOn(),
356
                            $lName,$element['left'],
357
                            $rName,$element['right']);
358
        if (DB::isError($res = $this->dbh->query($query))) {
359
            // the rollback shall be done by the method calling this one
360
            // since it is only private
361
            return $this->_throwError($res->getMessage(), __LINE__);
362
        }
363
        // FIXXME commit:
364
        // should that not also be done in the method calling this one?
365
        // like when an error occurs?
366
        //$this->dbh->commit();
367
        return true;
368
    }
369
 
370
    // }}}
371
    // {{{ move()
372
 
373
    /**
374
     * move an entry under a given parent or behind a given entry.
375
     * If a newPrevId is given the newParentId is dismissed!
376
     * call it either like this:
377
     *  $tree->move(x, y)
378
     *  to move the element (or entire tree) with the id x
379
     *  under the element with the id y
380
     * or
381
     *  $tree->move(x, 0, y);   // ommit the second parameter by setting
382
     *  it to 0
383
     *  to move the element (or entire tree) with the id x
384
     *  behind the element with the id y
385
     * or
386
     *  $tree->move(array(x1,x2,x3), ...
387
     *  the first parameter can also be an array of elements that shall
388
     *  be moved. the second and third para can be as described above.
389
     *
390
     * If you are using the Memory_DBnested then this method would be invain,
391
     * since Memory.php already does the looping through multiple elements.
392
     * But if Dynamic_DBnested is used we need to do the looping here
393
     *
394
     * @version    2002/06/08
395
     * @access     public
396
     * @author     Wolfram Kriesing <wolfram@kriesing.de>
397
     * @param      integer  the id(s) of the element(s) that shall be moved
398
     * @param      integer  the id of the element which will be the new parent
399
     * @param      integer  if prevId is given the element with the id idToMove
400
     *                      shall be moved _behind_ the element with id=prevId
401
     *                      if it is 0 it will be put at the beginning
402
     * @return     mixed    true for success, Tree_Error on failure
403
     */
404
    function move($idsToMove, $newParentId, $newPrevId = 0)
405
    {
406
        settype($idsToMove, 'array');
407
        $errors = array();
408
        foreach ($idsToMove as $idToMove) {
409
            $ret = $this->_move($idToMove, $newParentId, $newPrevId);
410
            if (Tree::isError($ret)) {
411
                $errors[] = $ret;
412
            }
413
        }
414
        // FIXXME the error in a nicer way, or even better
415
        // let the throwError method do it!!!
416
        if (sizeof($errors)) {
417
            return $this->_throwError(serialize($errors), __LINE__);
418
        }
419
        return true;
420
    }
421
 
422
    // }}}
423
    // {{{ _move()
424
 
425
    /**
426
     * this method moves one tree element
427
     *
428
     * @see     move()
429
     * @version 2002/04/29
430
     * @access  public
431
     * @author  Wolfram Kriesing <wolfram@kriesing.de>
432
     * @param   integer the id of the element that shall be moved
433
     * @param   integer the id of the element which will be the new parent
434
     * @param   integer if prevId is given the element with the id idToMove
435
     *                  shall be moved _behind_ the element with id=prevId
436
     *                  if it is 0 it will be put at the beginning
437
     * @return  mixed    true for success, Tree_Error on failure
438
     */
439
    function _move($idToMove, $newParentId, $newPrevId = 0)
440
    {
441
        // do some integrity checks first
442
        if ($newPrevId) {
443
            // dont let people move an element behind itself, tell it
444
            // succeeded, since it already is there :-)
445
            if ($newPrevId == $idToMove) {
446
                return true;
447
            }
448
            if (Tree::isError($newPrevious = $this->getElement($newPrevId))) {
449
                return $newPrevious;
450
            }
451
            $newParentId = $newPrevious['parentId'];
452
        } else {
453
            if ($newParentId == 0) {
454
                return $this->_throwError('no parent id given', __LINE__);
455
            }
456
            // if the element shall be moved under one of its children
457
            // return false
458
            if ($this->isChildOf($idToMove,$newParentId)) {
459
                return $this->_throwError(
460
                            'can not move an element under one of its children' ,
461
                            __LINE__
462
                        );
463
            }
464
            // dont do anything to let an element be moved under itself
465
            // which is bullshit
466
            if ($newParentId == $idToMove) {
467
                return true;
468
            }
469
            // try to retreive the data of the parent element
470
            if (Tree::isError($newParent=$this->getElement($newParentId))) {
471
                return $newParent;
472
            }
473
        }
474
        // get the data of the element itself
475
        if (Tree::isError($element=$this->getElement($idToMove))) {
476
            return $element;
477
        }
478
 
479
        $numberOfElements = ($element['right'] - $element['left'] + 1) / 2;
480
        $prevVisited = $newPrevId ? $newPrevious['right'] : $newParent['left'];
481
 
482
        // FIXXME start transaction
483
 
484
        // add the left/right values in the new parent, to have the space
485
        // to move the new values in
486
        $err = $this->_add($prevVisited, $numberOfElements);
487
        if (Tree::isError($err)) {
488
            // FIXXME rollback
489
            //$this->dbh->rollback();
490
            return $err;
491
        }
492
 
493
        // update the parentId of the element with $idToMove
494
        $err = $this->update($idToMove,array('parentId' => $newParentId));
495
        if (Tree::isError($err)) {
496
            // FIXXME rollback
497
            //$this->dbh->rollback();
498
            return $err;
499
        }
500
 
501
        // update the lefts and rights of those elements that shall be moved
502
 
503
        // first get the offset we need to add to the left/right values
504
        // if $newPrevId is given we need to get the right value,
505
        // otherwise the left since the left/right has changed
506
        // because we already updated it up there. We need to get them again.
507
        // We have to do that anyway, to have the proper new left/right values
508
        if ($newPrevId) {
509
            if (Tree::isError($temp = $this->getElement($newPrevId))) {
510
                // FIXXME rollback
511
                //$this->dbh->rollback();
512
                return $temp;
513
            }
514
            $calcWith = $temp['right'];
515
        } else {
516
            if (Tree::isError($temp=$this->getElement($newParentId))) {
517
                // FIXXME rollback
518
                //$this->dbh->rollback();
519
                return $temp;
520
            }
521
            $calcWith = $temp['left'];
522
        }
523
 
524
        // get the element that shall be moved again, since the left and
525
        // right might have changed by the add-call
526
        if (Tree::isError($element=$this->getElement($idToMove))) {
527
            return $element;
528
        }
529
        // calc the offset that the element to move has
530
        // to the spot where it should go
531
        $offset = $calcWith - $element['left'];
532
        // correct the offset by one, since it needs to go inbetween!
533
        $offset++;
534
 
535
        $lName = $this->_getColName('left');
536
        $rName = $this->_getColName('right');
537
        $query = sprintf("UPDATE
538
                                %s
539
                            SET
540
                                %s=%s+$offset,
541
                                %s=%s+$offset
542
                            WHERE
543
                                %s %s>%s
544
                                AND
545
                                %s < %s",
546
                            $this->table,
547
                            $rName,$rName,
548
                            $lName,$lName,
549
                            $this->_getWhereAddOn(),
550
                            $lName,$element['left']-1,
551
                            $rName,$element['right']+1);
552
        if (DB::isError($res=$this->dbh->query($query))) {
553
            // FIXXME rollback
554
            //$this->dbh->rollback();
555
            return $this->_throwError($res->getMessage(), __LINE__);
556
        }
557
 
558
        // remove the part of the tree where the element(s) was/were before
559
        if (Tree::isError($err = $this->_remove($element))) {
560
            // FIXXME rollback
561
            //$this->dbh->rollback();
562
            return $err;
563
        }
564
        // FIXXME commit all changes
565
        //$this->dbh->commit();
566
 
567
        return true;
568
    }
569
 
570
    // }}}
571
    // {{{ update()
572
 
573
    /**
574
     * update the tree element given by $id with the values in $newValues
575
     *
576
     * @access     public
577
     * @author     Wolfram Kriesing <wolfram@kriesing.de>
578
     * @param      int     the id of the element to update
579
     * @param      array   the new values, the index is the col name
580
     * @return     mixed   either true or an Tree_Error
581
     */
582
    function update($id, $newValues)
583
    {
584
        // just to be sure nothing gets screwed up :-)
585
        unset($newValues[$this->_getColName('left')]);
586
        unset($newValues[$this->_getColName('right')]);
587
        unset($newValues[$this->_getColName('parentId')]);
588
 
589
        // updating _one_ element in the tree
590
        $values = array();
591
        foreach ($newValues as $key => $value) {
592
            $values[] = $this->_getColName($key).'='.$this->dbh->quoteSmart($value);
593
        }
594
        $query = sprintf('UPDATE %s SET %s WHERE%s %s=%s',
595
                            $this->table,
596
                            implode(',',$values),
597
                            $this->_getWhereAddOn(),
598
                            $this->_getColName('id'),
599
                            $id);
600
        if (DB::isError($res=$this->dbh->query($query))) {
601
            return $this->_throwError($res->getMessage(), __LINE__);
602
        }
603
 
604
        return true;
605
    }
606
 
607
    // }}}
608
    // {{{ update()
609
 
610
    /**
611
     * copy a subtree/node/... under a new parent or/and behind a given element
612
     *
613
     *
614
     * @access     public
615
     * @author     Wolfram Kriesing <wolfram@kriesing.de>
616
     * @param      integer the ID of the node that shall be copied
617
     * @param      integer the new parent ID
618
     * @param      integer the new previous ID, if given parent ID will be omitted
619
     * @return     boolean true on success
620
     */
621
    function copy($id, $parentId = 0, $prevId = 0)
622
    {
623
        return $this->_throwError(
624
                'copy-method is not implemented yet!' ,
625
                __LINE__
626
           );
627
        // get element tree
628
        // $this->addTree
629
    }
630
 
631
    // }}}
632
    // {{{ getRoot()
633
 
634
    /**
635
     * get the root
636
     *
637
     * @access     public
638
     * @version    2002/03/02
639
     * @author     Wolfram Kriesing <wolfram@kriesing.de>
640
     * @return     mixed   either the data of the root element or an Tree_Error
641
     */
642
    function getRoot()
643
    {
644
        $query = sprintf('SELECT * FROM %s WHERE%s %s=1',
645
                            $this->table,
646
                            $this->_getWhereAddOn(),
647
                            $this->_getColName('left'));
648
        if (DB::isError($res = $this->dbh->getRow($query))) {
649
            return $this->_throwError($res->getMessage(), __LINE__);
650
        }
651
        return !$res ? false : $this->_prepareResult($res);
652
    }
653
 
654
    // }}}
655
    // {{{ getElement()
656
 
657
    /**
658
     *
659
     *
660
     * @access     public
661
     * @version    2002/03/02
662
     * @author     Wolfram Kriesing <wolfram@kriesing.de>
663
     * @param      integer  the ID of the element to return
664
     *
665
     * @return  mixed    either the data of the requested element
666
     *                      or an Tree_Error
667
     */
668
    function getElement($id)
669
    {
670
        $query = sprintf('SELECT * FROM %s WHERE %s %s=%s',
671
                            $this->table,
672
                            $this->_getWhereAddOn(),
673
                            $this->_getColName('id'),
674
                            $id);
675
        $res = $this->dbh->getRow($query);
676
        if (DB::isError($res)) {
677
            return $this->_throwError($res->getMessage(), __LINE__);
678
        }
679
        if (!$res) {
680
            return $this->_throwError("Element with id $id does not exist!" ,
681
                                        __LINE__);
682
        }
683
        return $this->_prepareResult($res);
684
    }
685
 
686
    // }}}
687
    // {{{ getChild()
688
 
689
    /**
690
     *
691
     *
692
     * @access     public
693
     * @version    2002/03/02
694
     * @author     Wolfram Kriesing <wolfram@kriesing.de>
695
     * @param      integer  the ID of the element for which the children
696
     *                      shall be returned
697
     * @return     mixed   either the data of the requested element or an Tree_Error
698
     */
699
    function getChild($id)
700
    {
701
        // subqueries would be cool :-)
702
        $curElement = $this->getElement($id);
703
        if (Tree::isError($curElement)) {
704
            return $curElement;
705
        }
706
 
707
        $query = sprintf('SELECT * FROM %s WHERE%s %s=%s',
708
                            $this->table,
709
                            $this->_getWhereAddOn(),
710
                            $this->_getColName('left'),
711
                            $curElement['left']+1);
712
        if (DB::isError($res = $this->dbh->getRow($query))) {
713
            return $this->_throwError($res->getMessage(), __LINE__);
714
        }
715
        return $this->_prepareResult($res);
716
    }
717
 
718
    // }}}
719
    // {{{ getPath()
720
 
721
    /**
722
     * gets the path from the element with the given id down
723
     * to the root. The returned array is sorted to start at root
724
     * for simply walking through and retreiving the path
725
     *
726
     * @access     public
727
     * @version    2002/03/02
728
     * @author     Wolfram Kriesing <wolfram@kriesing.de>
729
     * @param      integer  the ID of the element for which the path shall be
730
     *                      returned
731
     * @return     mixed    either the data of the requested elements
732
     *                      or an Tree_Error
733
     */
734
    function getPath($id)
735
    {
736
        $res = $this->dbh->getAll($this->_getPathQuery($id));
737
        if (DB::isError($res)) {
738
            return $this->_throwError($res->getMessage(), __LINE__);
739
        }
740
        return $this->_prepareResults($res);
741
    }
742
 
743
    // }}}
744
    // {{{ _getPathQuery()
745
 
746
    function _getPathQuery($id)
747
    {
748
        // subqueries would be cool :-)
749
        $curElement = $this->getElement($id);
750
        $query = sprintf('SELECT * FROM %s '.
751
                            'WHERE %s %s<=%s AND %s>=%s '.
752
                            'ORDER BY %s',
753
                            // set the FROM %s
754
                            $this->table,
755
                            // set the additional where add on
756
                            $this->_getWhereAddOn(),
757
                            // render 'left<=curLeft'
758
                            $this->_getColName('left'),$curElement['left'],
759
                            // render right>=curRight'
760
                            $this->_getColName('right'),$curElement['right'],
761
                            // set the order column
762
                            $this->_getColName('left'));
763
        return $query;
764
    }
765
 
766
    // }}}
767
    // {{{ getLevel()
768
 
769
    function getLevel($id)
770
    {
771
        $query = $this->_getPathQuery($id);
772
        // i know this is not really beautiful ...
773
        $query = preg_replace('/^select \* /i','SELECT COUNT(*) ',$query);
774
        if (DB::isError($res = $this->dbh->getOne($query))) {
775
            return $this->_throwError($res->getMessage(), __LINE__);
776
        }
777
        return $res-1;
778
    }
779
 
780
    // }}}
781
    // {{{ getLeft()
782
 
783
    /**
784
     * gets the element to the left, the left visit
785
     *
786
     * @access     public
787
     * @version    2002/03/07
788
     * @author     Wolfram Kriesing <wolfram@kriesing.de>
789
     * @param      integer  the ID of the element
790
     * @return     mixed    either the data of the requested element
791
     *                      or an Tree_Error
792
     */
793
    function getLeft($id)
794
    {
795
        $element = $this->getElement($id);
796
        if (Tree::isError($element)) {
797
            return $element;
798
        }
799
 
800
        $query = sprintf('SELECT * FROM %s WHERE%s (%s=%s OR %s=%s)',
801
                            $this->table,
802
                            $this->_getWhereAddOn(),
803
                            $this->_getColName('right'),$element['left']-1,
804
                            $this->_getColName('left'),$element['left']-1);
805
        if (DB::isError($res = $this->dbh->getRow($query))) {
806
            return $this->_throwError($res->getMessage(), __LINE__);
807
        }
808
        return $this->_prepareResult($res);
809
    }
810
 
811
    // }}}
812
    // {{{ getRight()
813
 
814
    /**
815
     * gets the element to the right, the right visit
816
     *
817
     * @access     public
818
     * @version    2002/03/07
819
     * @author     Wolfram Kriesing <wolfram@kriesing.de>
820
     * @param      integer  the ID of the element
821
     * @return     mixed    either the data of the requested element
822
     *                      or an Tree_Error
823
     */
824
    function getRight($id)
825
    {
826
        $element = $this->getElement($id);
827
        if (Tree::isError($element)) {
828
            return $element;
829
        }
830
        $query = sprintf('SELECT * FROM %s WHERE%s (%s=%s OR %s=%s)',
831
                            $this->table,
832
                            $this->_getWhereAddOn(),
833
                            $this->_getColName('left'),$element['right']+1,
834
                            $this->_getColName('right'),$element['right']+1);
835
        if (DB::isError($res = $this->dbh->getRow($query))) {
836
            return $this->_throwError($res->getMessage(), __LINE__);
837
        }
838
        return $this->_prepareResult($res);
839
    }
840
 
841
    // }}}
842
    // {{{ getParent()
843
 
844
    /**
845
     * get the parent of the element with the given id
846
     *
847
     * @access     public
848
     * @version    2002/04/15
849
     * @author     Wolfram Kriesing <wolfram@kriesing.de>
850
     * @param      integer the ID of the element
851
     * @return     mixed    the array with the data of the parent element
852
     *                      or false, if there is no parent, if the element is
853
     *                      the root or an Tree_Error
854
     */
855
    function getParent($id)
856
    {
857
        $query = sprintf('SELECT
858
                                p.*
859
                            FROM
860
                                %s p,%s e
861
                            WHERE
862
                                %s e.%s=p.%s
863
                                AND
864
                                e.%s=%s',
865
                            $this->table,$this->table,
866
                            $this->_getWhereAddOn(' AND ', 'p'),
867
                            $this->_getColName('parentId'),
868
                            $this->_getColName('id'),
869
                            $this->_getColName('id'),
870
                            $id);
871
        if (DB::isError($res = $this->dbh->getRow($query))) {
872
            return $this->_throwError($res->getMessage(), __LINE__);
873
        }
874
        return $this->_prepareResult($res);
875
    }
876
 
877
    // }}}
878
    // {{{ getChildren()
879
 
880
    /**
881
     * get the children of the given element or if the parameter is an array.
882
     * It gets the children of all the elements given by their ids
883
     * in the array.
884
     *
885
     * @access     public
886
     * @version    2002/04/15
887
     * @author     Wolfram Kriesing <wolfram@kriesing.de>
888
     * @param      mixed   (1) int     the id of one element
889
     *                     (2) array   an array of ids for which
890
     *                                 the children will be returned
891
     * @param      integer the children of how many levels shall be returned
892
     * @return     mixed   the array with the data of all children
893
     *                     or false, if there are none
894
     */
895
    function getChildren($ids, $levels = 1)
896
    {
897
        $res = array();
898
        for ($i = 1; $i < $levels + 1; $i++) {
899
            // if $ids is an array implode the values
900
            $getIds = is_array($ids) ? implode(',',$ids) : $ids;
901
 
902
            $query = sprintf('SELECT
903
                                    c.*
904
                                FROM
905
                                    %s c,%s e
906
                                WHERE
907
                                    %s e.%s=c.%s
908
                                    AND
909
                                    e.%s IN (%s) '.
910
                                'ORDER BY
911
                                    c.%s',
912
                                $this->table,$this->table,
913
                                $this->_getWhereAddOn(' AND ', 'c'),
914
                                $this->_getColName('id'),
915
                                $this->_getColName('parentId'),
916
                                $this->_getColName('id'),
917
                                $getIds,
918
                                // order by left, so we have it in the order
919
                                // as it is in the tree if no 'order'-option
920
                                // is given
921
                                $this->getOption('order')?
922
                                    $this->getOption('order')
923
                                    : $this->_getColName('left')
924
                       );
925
            if (DB::isError($_res = $this->dbh->getAll($query))) {
926
                return $this->_throwError($_res->getMessage(), __LINE__);
927
            }
928
 
929
            // Column names are now unmapped
930
            $_res = $this->_prepareResults($_res);
931
 
932
            // we use the id as the index, to make the use easier esp.
933
            // for multiple return-values
934
            $tempRes = array();
935
            foreach ($_res as $aRes) {
936
                $tempRes[$aRes['id']] = $aRes;
937
            }
938
            $_res = $tempRes;
939
 
940
            if ($levels>1) {
941
                $ids = array();
942
                foreach($_res as $aRes) {
943
                    $ids[] = $aRes[$this->_getColName('id')];
944
                }
945
            }
946
            $res = array_merge($res,$_res);
947
 
948
            // quit the for-loop if there are no children in the current level
949
            if (!sizeof($ids)) {
950
                break;
951
            }
952
        }
953
        return $res;
954
    }
955
 
956
    // }}}
957
    // {{{ getNext()
958
 
959
    /**
960
     * get the next element on the same level
961
     * if there is none return false
962
     *
963
     * @access     public
964
     * @version    2002/04/15
965
     * @author     Wolfram Kriesing <wolfram@kriesing.de>
966
     * @param      integer the ID of the element
967
     * @return     mixed   the array with the data of the next element
968
     *                     or false, if there is no next
969
     *                     or Tree_Error
970
     */
971
    function getNext($id)
972
    {
973
        $query = sprintf('SELECT
974
                                n.*
975
                            FROM
976
                                %s n,%s e
977
                            WHERE
978
                                %s e.%s=n.%s-1
979
                            AND
980
                                e.%s=n.%s
981
                            AND
982
                                e.%s=%s',
983
                            $this->table,$this->table,
984
                            $this->_getWhereAddOn(' AND ', 'n'),
985
                            $this->_getColName('right'),
986
                            $this->_getColName('left'),
987
                            $this->_getColName('parentId'),
988
                            $this->_getColName('parentId'),
989
                            $this->_getColName('id'),
990
                            $id);
991
        if (DB::isError($res = $this->dbh->getRow($query))) {
992
            return $this->_throwError($res->getMessage(), __LINE__);
993
        }
994
        return !$res ? false : $this->_prepareResult($res);
995
    }
996
 
997
    // }}}
998
    // {{{ getPrevious()
999
 
1000
    /**
1001
     * get the previous element on the same level
1002
     * if there is none return false
1003
     *
1004
     * @access     public
1005
     * @version    2002/04/15
1006
     * @author     Wolfram Kriesing <wolfram@kriesing.de>
1007
     * @param      integer the ID of the element
1008
     * @return     mixed   the array with the data of the previous element
1009
     *                     or false, if there is no previous
1010
     *                     or a Tree_Error
1011
     */
1012
    function getPrevious($id)
1013
    {
1014
        $query = sprintf('SELECT
1015
                                p.*
1016
                            FROM
1017
                                %s p,%s e
1018
                            WHERE
1019
                                %s e.%s=p.%s+1
1020
                                AND
1021
                                    e.%s=p.%s
1022
                                AND
1023
                                    e.%s=%s',
1024
                            $this->table,$this->table,
1025
                            $this->_getWhereAddOn(' AND ', 'p'),
1026
                            $this->_getColName('left'),
1027
                            $this->_getColName('right'),
1028
                            $this->_getColName('parentId'),
1029
                            $this->_getColName('parentId'),
1030
                            $this->_getColName('id'),
1031
                            $id);
1032
        if (DB::isError($res = $this->dbh->getRow($query))) {
1033
            return $this->_throwError($res->getMessage(), __LINE__);
1034
        }
1035
        return !$res ? false : $this->_prepareResult($res);
1036
    }
1037
 
1038
    // }}}
1039
    // {{{ isChildOf()
1040
 
1041
    /**
1042
     * returns if $childId is a child of $id
1043
     *
1044
     * @abstract
1045
     * @version    2002/04/29
1046
     * @access     public
1047
     * @author     Wolfram Kriesing <wolfram@kriesing.de>
1048
     * @param      int     id of the element
1049
     * @param      int     id of the element to check if it is a child
1050
     * @return     boolean true if it is a child
1051
     */
1052
    function isChildOf($id, $childId)
1053
    {
1054
        // check simply if the left and right of the child are within the
1055
        // left and right of the parent, if so it definitly is a child :-)
1056
        $parent = $this->getElement($id);
1057
        $child = $this->getElement($childId);
1058
 
1059
        if ($parent['left'] < $child['left']
1060
            && $parent['right'] > $child['right']) {
1061
            return true;
1062
        }
1063
        return false;
1064
    }
1065
 
1066
    // }}}
1067
    // {{{ getDepth()
1068
 
1069
    /**
1070
     * return the maximum depth of the tree
1071
     *
1072
     * @version 2003/02/25
1073
     * @access public
1074
     * @author "Denis Joloudov" <dan@aitart.ru>, Wolfram Kriesing <wolfram@kriesing.de>
1075
     * @return integer the depth of the tree
1076
     */
1077
    function getDepth()
1078
    {
1079
        // FIXXXME TODO!!!
1080
        $query = sprintf('SELECT COUNT(*) FROM %s p, %s e '.
1081
                            'WHERE %s (e.%s BETWEEN p.%s AND p.%s) AND '.
1082
                            '(e.%s BETWEEN p.%s AND p.%s)',
1083
                            $this-> table,$this->table,
1084
                            // first line in where
1085
                            $this->_getWhereAddOn(' AND ','p'),
1086
                            $this->_getColName('left'),$this->_getColName('left'),
1087
                            $this->_getColName('right'),
1088
                            // second where line
1089
                            $this->_getColName('right'),$this->_getColName('left'),
1090
                            $this->_getColName('right')
1091
                            );
1092
        if (DB::isError($res = $this->dbh->getOne($query))) {
1093
            return $this->_throwError($res->getMessage(), __LINE__);
1094
        }
1095
        if (!$res) {
1096
            return false;
1097
        }
1098
        return $this->_prepareResult($res);
1099
    }
1100
 
1101
    // }}}
1102
    // {{{ hasChildren()
1103
 
1104
    /**
1105
     * Tells if the node with the given ID has children.
1106
     *
1107
     * @version    2003/03/04
1108
     * @access     public
1109
     * @author     Wolfram Kriesing <wolfram@kriesing.de>
1110
     * @param      integer the ID of a node
1111
     * @return     boolean if the node with the given id has children
1112
     */
1113
    function hasChildren($id)
1114
    {
1115
        $element = $this->getElement($id);
1116
        // if the diff between left and right>1 then there are children
1117
        return ($element['right'] - $element['left']) > 1;
1118
    }
1119
 
1120
    // }}}
1121
    // {{{ getIdByPath()
1122
 
1123
    /**
1124
     * return the id of the element which is referenced by $path
1125
     * this is useful for xml-structures, like: getIdByPath('/root/sub1/sub2')
1126
     * this requires the structure to use each name uniquely
1127
     * if this is not given it will return the first proper path found
1128
     * i.e. there should only be one path /x/y/z
1129
     * experimental: the name can be non unique if same names are in different levels
1130
     *
1131
     * @version    2003/05/11
1132
     * @access     public
1133
     * @author     Pierre-Alain Joye <paj@pearfr.org>
1134
     * @param      string   $path       the path to search for
1135
     * @param      integer  $startId    the id where to start the search
1136
     * @param      string   $nodeName   the name of the key that contains
1137
     *                                  the node name
1138
     * @param      string   $seperator  the path seperator
1139
     * @return     integer  the id of the searched element
1140
     */
1141
    function getIdByPath($path, $startId = 0, $nodeName = 'name', $separator = '/')
1142
    // should this method be called getElementIdByPath ????
1143
    // Yes, with an optional private paramater to get the whole node
1144
    // in preference to only the id?
1145
    {
1146
        if ($separator == '') {
1147
            return $this->_throwError(
1148
                'getIdByPath: Empty separator not allowed', __LINE__);
1149
        }
1150
        if ($path == $separator) {
1151
            $root = $this->getRoot();
1152
            if (Tree::isError($root)) {
1153
                return $root;
1154
            }
1155
            return $root['id'];
1156
        }
1157
        if (!($colname=$this->_getColName($nodeName))) {
1158
            return $this->_throwError(
1159
                'getIdByPath: Invalid node name', __LINE__);
1160
        }
1161
        if ($startId != 0) {
1162
            // If the start node has no child, returns false
1163
            // hasChildren calls getElement. Not very good right
1164
            // now. See the TODO
1165
            $startElem = $this->getElement($startId);
1166
            if (!is_array($startElem) || Tree::isError($startElem)) {
1167
                return $startElem;
1168
            }
1169
            // No child? return
1170
            if (!is_array($startElem)) {
1171
                return null;
1172
            }
1173
            $rangeStart  = $startElem['left'];
1174
            $rangeEnd  = $startElem['right'];
1175
            // Not clean, we should call hasChildren, but I do not
1176
            // want to call getELement again :). See TODO
1177
            $startHasChild = ($rangeEnd-$rangeStart)>1?true:false;
1178
            $cwd = '/'.$this->getPathAsString($startId);
1179
        } else {
1180
            $cwd = '/';
1181
            $startHasChild = false;
1182
        }
1183
        $t = $this->_preparePath($path, $cwd, $separator);
1184
        if (Tree::isError($t)) {
1185
            return $t;
1186
        }
1187
        list($elems, $sublevels) = $t;
1188
        $cntElems = sizeof($elems);
1189
        $where = '';
1190
 
1191
        $query = 'SELECT '
1192
                .$this->_getColName('id')
1193
                .' FROM '
1194
                .$this->table
1195
                . ' WHERE '
1196
                .$colname;
1197
        if ($cntElems == 1) {
1198
            $query .= "='".$elems[0]."'";
1199
        } else {
1200
            $query .= "='".$elems[$cntElems-1]."'";
1201
        }
1202
        if ($startHasChild) {
1203
            $where  .= ' AND ('.
1204
                        $this->_getColName('left').'>'.$rangeStart.
1205
                        ' AND '.
1206
                        $this->_getColName('right').'<'.$rangeEnd.')';
1207
        }
1208
        $res = $this->dbh->getOne($query);
1209
        if (DB::isError($res)) {
1210
            return $this->_throwError($res->getMessage(),
1211
                        __LINE__);
1212
        }
1213
        return ($res ? (int)$res : false);
1214
    }
1215
 
1216
    // }}}
1217
 
1218
    //
1219
    //  PRIVATE METHODS
1220
    //
1221
 
1222
    // {{{ _getWhereAddOn()
1223
    /**
1224
     *
1225
     *
1226
     * @access     private
1227
     * @version    2002/04/20
1228
     * @author     Wolfram Kriesing <wolfram@kriesing.de>
1229
     * @param      string  the current where clause
1230
     * @return     string  the where clause we want to add to a query
1231
     */
1232
    function _getWhereAddOn($addAfter = ' AND ', $tableName = '')
1233
    {
1234
        if ($where = $this->getOption('whereAddOn')) {
1235
            return ' '.($tableName ? $tableName.'.' : '')." $where$addAfter ";
1236
        }
1237
        return '';
1238
    }
1239
 
1240
    // }}}
1241
    // {{{ getFirstRoot()
1242
 
1243
    // for compatibility to Memory methods
1244
    function getFirstRoot()
1245
    {
1246
        return $this->getRoot();
1247
    }
1248
 
1249
    // }}}
1250
    // {{{ getNode()
1251
 
1252
    /**
1253
     * gets the tree under the given element in one array, sorted
1254
     * so you can go through the elements from begin to end and list them
1255
     * as they are in the tree, where every child (until the deepest) is retreived
1256
     *
1257
     * @see        &_getNode()
1258
     * @access     public
1259
     * @version    2001/12/17
1260
     * @author     Wolfram Kriesing <wolfram@kriesing.de>
1261
     * @param      integer  $startId    the id where to start walking
1262
     * @param      integer  $depth      this number says how deep into
1263
     *                                  the structure the elements shall
1264
     *                                  be retreived
1265
     * @return     array    sorted as listed in the tree
1266
     */
1267
    function &getNode($startId = 0, $depth = 0)
1268
    {
1269
//FIXXXME use getChildren()
1270
        if ($startId) {
1271
            $startNode = $this->getElement($startId);
1272
            if (Tree::isError($startNode)) {
1273
                return $startNode;
1274
            }
1275
 
1276
        } else {
1277
        }
1278
    }
1279
}
1280
 
1281
/*
1282
 * Local Variables:
1283
 * mode: php
1284
 * tab-width: 4
1285
 * c-basic-offset: 4
1286
 * End:
1287
 */