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