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: MDBnested.php,v 1.3.2.2 2009/03/12 17:19:55 dufuz Exp $
20
 
21
require_once 'Tree/OptionsMDB.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_MDBnested extends Tree_OptionsMDB
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_MDBnested($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_MDBnested($dsn, $options = array())
99
    {
100
        parent::Tree_OptionsMDB($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->getTextValue($value);
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->getIntegerValue($nextId),
207
                            implode(',', $newData)
208
                        );
209
        if (MDB::isError($res = $this->dbh->query($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 (MDB::isError($res = $this->dbh->query($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 (MDB::isError($res = $this->dbh->query($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 (MDB::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
     * @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 (MDB::isError($res = $this->dbh->query($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 (MDB::isError($res = $this->dbh->query($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 (MDB::isError($res=$this->dbh->query($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->getTextValue($value);
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 (MDB::isError($res=$this->dbh->query($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 (MDB::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
        if (MDB::isError($res = $this->dbh->getRow($query))) {
676
            return $this->_throwError($res->getMessage(), __LINE__);
677
        }
678
        if (!$res) {
679
            return $this->_throwError("Element with id $id does not exist!" ,
680
                                        __LINE__);
681
        }
682
        return $this->_prepareResult($res);
683
    }
684
 
685
    // }}}
686
    // {{{ getChild()
687
 
688
    /**
689
     *
690
     *
691
     * @access     public
692
     * @version    2002/03/02
693
     * @param      integer  the ID of the element for which the children
694
     *                      shall be returned
695
     * @return     mixed   either the data of the requested element or an Tree_Error
696
     */
697
    function getChild($id)
698
    {
699
        // subqueries would be cool :-)
700
        $curElement = $this->getElement($id);
701
        if (Tree::isError($curElement)) {
702
            return $curElement;
703
        }
704
 
705
        $query = sprintf('SELECT * FROM %s WHERE%s %s=%s',
706
                            $this->table,
707
                            $this->_getWhereAddOn(),
708
                            $this->_getColName('left'),
709
                            $curElement['left']+1);
710
        if (MDB::isError($res = $this->dbh->getRow($query))) {
711
            return $this->_throwError($res->getMessage(), __LINE__);
712
        }
713
        return $this->_prepareResult($res);
714
    }
715
 
716
    // }}}
717
    // {{{ getPath()
718
 
719
    /**
720
     * gets the path from the element with the given id down
721
     * to the root. The returned array is sorted to start at root
722
     * for simply walking through and retreiving the path
723
     *
724
     * @access public
725
     * @param integer the ID of the element for which the path shall be returned
726
     * @return mixed  either the data of the requested elements
727
     *                      or an Tree_Error
728
     */
729
    function getPath($id)
730
    {
731
        $res = $this->dbh->getAll($this->_getPathQuery($id));
732
        if (MDB::isError($res)) {
733
            return $this->_throwError($res->getMessage(), __LINE__);
734
        }
735
        return $this->_prepareResults($res);
736
    }
737
 
738
    // }}}
739
    // {{{ _getPathQuery()
740
 
741
    function _getPathQuery($id)
742
    {
743
        // subqueries would be cool :-)
744
        $curElement = $this->getElement($id);
745
        $query = sprintf('SELECT * FROM %s '.
746
                            'WHERE %s %s<=%s AND %s>=%s '.
747
                            'ORDER BY %s',
748
                            // set the FROM %s
749
                            $this->table,
750
                            // set the additional where add on
751
                            $this->_getWhereAddOn(),
752
                            // render 'left<=curLeft'
753
                            $this->_getColName('left'),  $curElement['left'],
754
                            // render right>=curRight'
755
                            $this->_getColName('right'), $curElement['right'],
756
                            // set the order column
757
                            $this->_getColName('left'));
758
        return $query;
759
    }
760
 
761
    // }}}
762
    // {{{ getLevel()
763
 
764
    function getLevel($id)
765
    {
766
        $query = $this->_getPathQuery($id);
767
        // i know this is not really beautiful ...
768
        $query = preg_replace('/^select \* /i','SELECT COUNT(*) ',$query);
769
        if (MDB::isError($res = $this->dbh->getOne($query))) {
770
            return $this->_throwError($res->getMessage(), __LINE__);
771
        }
772
        return $res-1;
773
    }
774
 
775
    // }}}
776
    // {{{ getLeft()
777
 
778
    /**
779
     * gets the element to the left, the left visit
780
     *
781
     * @access     public
782
     * @version    2002/03/07
783
     * @author     Wolfram Kriesing <wolfram@kriesing.de>
784
     * @param      integer  the ID of the element
785
     * @return     mixed    either the data of the requested element
786
     *                      or an Tree_Error
787
     */
788
    function getLeft($id)
789
    {
790
        $element = $this->getElement($id);
791
        if (Tree::isError($element)) {
792
            return $element;
793
        }
794
 
795
        $query = sprintf('SELECT * FROM %s WHERE%s (%s=%s OR %s=%s)',
796
                            $this->table,
797
                            $this->_getWhereAddOn(),
798
                            $this->_getColName('right'), $element['left'] - 1,
799
                            $this->_getColName('left'),  $element['left'] - 1);
800
        if (MDB::isError($res = $this->dbh->getRow($query))) {
801
            return $this->_throwError($res->getMessage(), __LINE__);
802
        }
803
        return $this->_prepareResult($res);
804
    }
805
 
806
    // }}}
807
    // {{{ getRight()
808
 
809
    /**
810
     * gets the element to the right, the right visit
811
     *
812
     * @access     public
813
     * @version    2002/03/07
814
     * @author     Wolfram Kriesing <wolfram@kriesing.de>
815
     * @param      integer  the ID of the element
816
     * @return     mixed    either the data of the requested element
817
     *                      or an Tree_Error
818
     */
819
    function getRight($id)
820
    {
821
        $element = $this->getElement($id);
822
        if (Tree::isError($element))
823
            return $element;
824
 
825
        $query = sprintf('SELECT * FROM %s WHERE%s (%s=%s OR %s=%s)',
826
                            $this->table,
827
                            $this->_getWhereAddOn(),
828
                            $this->_getColName('left'),  $element['right'] + 1,
829
                            $this->_getColName('right'), $element['right'] + 1);
830
        if (MDB::isError($res = $this->dbh->getRow($query))) {
831
            return $this->_throwError($res->getMessage(), __LINE__);
832
        }
833
        return $this->_prepareResult($res);
834
    }
835
 
836
    // }}}
837
    // {{{ getParent()
838
 
839
    /**
840
     * get the parent of the element with the given id
841
     *
842
     * @access     public
843
     * @version    2002/04/15
844
     * @author     Wolfram Kriesing <wolfram@kriesing.de>
845
     * @param      integer the ID of the element
846
     * @return     mixed    the array with the data of the parent element
847
     *                      or false, if there is no parent, if the element is
848
     *                      the root or an Tree_Error
849
     */
850
    function getParent($id)
851
    {
852
        $query = sprintf('SELECT
853
                                p.*
854
                            FROM
855
                                %s p,%s e
856
                            WHERE
857
                                %s e.%s=p.%s
858
                                AND
859
                                e.%s=%s',
860
                            $this->table,$this->table,
861
                            $this->_getWhereAddOn(' AND ', 'p'),
862
                            $this->_getColName('parentId'),
863
                            $this->_getColName('id'),
864
                            $this->_getColName('id'),
865
                            $id);
866
        if (MDB::isError($res = $this->dbh->getRow($query))) {
867
            return $this->_throwError($res->getMessage(), __LINE__);
868
        }
869
        return $this->_prepareResult($res);
870
    }
871
 
872
    // }}}
873
    // {{{ getChildren()
874
 
875
    /**
876
     * get the children of the given element or if the parameter is an array.
877
     * It gets the children of all the elements given by their ids
878
     * in the array.
879
     *
880
     * @access     public
881
     * @version    2002/04/15
882
     * @author     Wolfram Kriesing <wolfram@kriesing.de>
883
     * @param      mixed   (1) int     the id of one element
884
     *                     (2) array   an array of ids for which
885
     *                                 the children will be returned
886
     * @param      integer the children of how many levels shall be returned
887
     * @return     mixed   the array with the data of all children
888
     *                     or false, if there are none
889
     */
890
    function getChildren($ids, $levels = 1)
891
    {
892
        $res = array();
893
        for ($i = 1; $i < $levels + 1; $i++) {
894
            // if $ids is an array implode the values
895
            $getIds = is_array($ids) ? implode(',', $ids) : $ids;
896
 
897
            $query = sprintf('SELECT
898
                                    c.*
899
                                FROM
900
                                    %s c,%s e
901
                                WHERE
902
                                    %s e.%s=c.%s
903
                                    AND
904
                                    e.%s IN (%s) '.
905
                                'ORDER BY
906
                                    c.%s',
907
                                $this->table,$this->table,
908
                                $this->_getWhereAddOn(' AND ', 'c'),
909
                                $this->_getColName('id'),
910
                                $this->_getColName('parentId'),
911
                                $this->_getColName('id'),
912
                                $getIds,
913
                                // order by left, so we have it in the order
914
                                // as it is in the tree if no 'order'-option
915
                                // is given
916
                                $this->getOption('order')?
917
                                    $this->getOption('order')
918
                                    : $this->_getColName('left')
919
                       );
920
            if (MDB::isError($_res = $this->dbh->getAll($query))) {
921
                return $this->_throwError($_res->getMessage(), __LINE__);
922
            }
923
 
924
            // Column names are now unmapped
925
            $_res = $this->_prepareResults($_res);
926
 
927
            // we use the id as the index, to make the use easier esp.
928
            // for multiple return-values
929
            $tempRes = array();
930
            foreach ($_res as $aRes) {
931
                $tempRes[$aRes['id']] = $aRes;
932
            }
933
            $_res = $tempRes;
934
 
935
            if ($levels > 1) {
936
                $ids = array();
937
                foreach ($_res as $aRes) {
938
                    $ids[] = $aRes[$this->_getColName('id')];
939
                }
940
            }
941
            $res = array_merge($res, $_res);
942
 
943
            // quit the for-loop if there are no children in the current level
944
            if (!sizeof($ids)) {
945
                break;
946
            }
947
        }
948
        return $res;
949
    }
950
 
951
    // }}}
952
    // {{{ getNext()
953
 
954
    /**
955
     * get the next element on the same level
956
     * if there is none return false
957
     *
958
     * @access     public
959
     * @version    2002/04/15
960
     * @author     Wolfram Kriesing <wolfram@kriesing.de>
961
     * @param      integer the ID of the element
962
     * @return     mixed   the array with the data of the next element
963
     *                     or false, if there is no next
964
     *                     or Tree_Error
965
     */
966
    function getNext($id)
967
    {
968
        $query = sprintf('SELECT
969
                                n.*
970
                            FROM
971
                                %s n,%s e
972
                            WHERE
973
                                %s e.%s=n.%s-1
974
                            AND
975
                                e.%s=n.%s
976
                            AND
977
                                e.%s=%s',
978
                            $this->table, $this->table,
979
                            $this->_getWhereAddOn(' AND ', 'n'),
980
                            $this->_getColName('right'),
981
                            $this->_getColName('left'),
982
                            $this->_getColName('parentId'),
983
                            $this->_getColName('parentId'),
984
                            $this->_getColName('id'),
985
                            $id);
986
        if (MDB::isError($res = $this->dbh->getRow($query))) {
987
            return $this->_throwError($res->getMessage(), __LINE__);
988
        }
989
        return !$res ? false : $this->_prepareResult($res);
990
    }
991
 
992
    // }}}
993
    // {{{ getPrevious()
994
 
995
    /**
996
     * get the previous element on the same level
997
     * if there is none return false
998
     *
999
     * @access     public
1000
     * @version    2002/04/15
1001
     * @author     Wolfram Kriesing <wolfram@kriesing.de>
1002
     * @param      integer the ID of the element
1003
     * @return     mixed   the array with the data of the previous element
1004
     *                     or false, if there is no previous
1005
     *                     or a Tree_Error
1006
     */
1007
    function getPrevious($id)
1008
    {
1009
        $query = sprintf('SELECT
1010
                                p.*
1011
                            FROM
1012
                                %s p,%s e
1013
                            WHERE
1014
                                %s e.%s=p.%s+1
1015
                                AND
1016
                                    e.%s=p.%s
1017
                                AND
1018
                                    e.%s=%s',
1019
                            $this->table,$this->table,
1020
                            $this->_getWhereAddOn(' AND ', 'p'),
1021
                            $this->_getColName('left'),
1022
                            $this->_getColName('right'),
1023
                            $this->_getColName('parentId'),
1024
                            $this->_getColName('parentId'),
1025
                            $this->_getColName('id'),
1026
                            $id);
1027
        if (MDB::isError($res = $this->dbh->getRow($query))) {
1028
            return $this->_throwError($res->getMessage(), __LINE__);
1029
        }
1030
        return !$res ? false : $this->_prepareResult($res);
1031
    }
1032
 
1033
    // }}}
1034
    // {{{ isChildOf()
1035
 
1036
    /**
1037
     * returns if $childId is a child of $id
1038
     *
1039
     * @abstract
1040
     * @version    2002/04/29
1041
     * @access     public
1042
     * @author     Wolfram Kriesing <wolfram@kriesing.de>
1043
     * @param      int     id of the element
1044
     * @param      int     id of the element to check if it is a child
1045
     * @return     boolean true if it is a child
1046
     */
1047
    function isChildOf($id, $childId)
1048
    {
1049
        // check simply if the left and right of the child are within the
1050
        // left and right of the parent, if so it definitly is a child :-)
1051
        $parent = $this->getElement($id);
1052
        $child  = $this->getElement($childId);
1053
 
1054
        if ($parent['left'] < $child['left']
1055
            && $parent['right'] > $child['right'])
1056
        {
1057
            return true;
1058
        }
1059
        return false;
1060
    }
1061
 
1062
    // }}}
1063
    // {{{ getDepth()
1064
 
1065
    /**
1066
     * return the maximum depth of the tree
1067
     *
1068
     * @version    2003/02/25
1069
     * @access     public
1070
     * @author "Denis Joloudov" <dan@aitart.ru>, Wolfram Kriesing <wolfram@kriesing.de>
1071
     * @return integer the depth of the tree
1072
     */
1073
    function getDepth()
1074
    {
1075
        // FIXXXME TODO!!!
1076
        $query = sprintf('SELECT COUNT(*) FROM %s p, %s e '.
1077
                            'WHERE %s (e.%s BETWEEN p.%s AND p.%s) AND '.
1078
                            '(e.%s BETWEEN p.%s AND p.%s)',
1079
                            $this-> table,$this->table,
1080
                            // first line in where
1081
                            $this->_getWhereAddOn(' AND ','p'),
1082
                            $this->_getColName('left'),$this->_getColName('left'),
1083
                            $this->_getColName('right'),
1084
                            // second where line
1085
                            $this->_getColName('right'),$this->_getColName('left'),
1086
                            $this->_getColName('right')
1087
                            );
1088
        if (MDB::isError($res=$this->dbh->getOne($query))) {
1089
            return $this->_throwError($res->getMessage(), __LINE__);
1090
        }
1091
        if (!$res) {
1092
            return false;
1093
        }
1094
        return $this->_prepareResult($res);
1095
    }
1096
 
1097
    // }}}
1098
    // {{{ hasChildren()
1099
 
1100
    /**
1101
     * Tells if the node with the given ID has children.
1102
     *
1103
     * @version    2003/03/04
1104
     * @access     public
1105
     * @author     Wolfram Kriesing <wolfram@kriesing.de>
1106
     * @param      integer the ID of a node
1107
     * @return     boolean if the node with the given id has children
1108
     */
1109
    function hasChildren($id)
1110
    {
1111
        $element = $this->getElement($id);
1112
        // if the diff between left and right > 1 then there are children
1113
        return ($element['right'] - $element['left']) > 1;
1114
    }
1115
 
1116
    // }}}
1117
    // {{{ getIdByPath()
1118
 
1119
    /**
1120
     * return the id of the element which is referenced by $path
1121
     * this is useful for xml-structures, like: getIdByPath('/root/sub1/sub2')
1122
     * this requires the structure to use each name uniquely
1123
     * if this is not given it will return the first proper path found
1124
     * i.e. there should only be one path /x/y/z
1125
     * experimental: the name can be non unique if same names are in different levels
1126
     *
1127
     * @version    2003/05/11
1128
     * @access     public
1129
     * @author     Pierre-Alain Joye <paj@pearfr.org>
1130
     * @param      string   $path       the path to search for
1131
     * @param      integer  $startId    the id where to start the search
1132
     * @param      string   $nodeName   the name of the key that contains
1133
     *                                  the node name
1134
     * @param      string   $seperator  the path seperator
1135
     * @return     integer  the id of the searched element
1136
     */
1137
    function getIdByPath($path, $startId = 0, $nodeName = 'name', $separator = '/')
1138
    // should this method be called getElementIdByPath ????
1139
    // Yes, with an optional private paramater to get the whole node
1140
    // in preference to only the id?
1141
    {
1142
        if ($separator == '') {
1143
            return $this->_throwError(
1144
                'getIdByPath: Empty separator not allowed', __LINE__);
1145
        }
1146
        if ($path == $separator) {
1147
            $root = $this->getRoot();
1148
            if (Tree::isError($root)) {
1149
                return $root;
1150
            }
1151
            return $root['id'];
1152
        }
1153
        if (!($colname=$this->_getColName($nodeName))) {
1154
            return $this->_throwError(
1155
                'getIdByPath: Invalid node name', __LINE__);
1156
        }
1157
        if ($startId != 0) {
1158
            // If the start node has no child, returns false
1159
            // hasChildren calls getElement. Not very good right
1160
            // now. See the TODO
1161
            $startElem = $this->getElement($startId);
1162
            if (!is_array($startElem) || Tree::isError($startElem)) {
1163
                return $startElem;
1164
            }
1165
            // No child? return
1166
            if (!is_array($startElem)) {
1167
                return null;
1168
            }
1169
            $rangeStart = $startElem['left'];
1170
            $rangeEnd   = $startElem['right'];
1171
            // Not clean, we should call hasChildren, but I do not
1172
            // want to call getELement again :). See TODO
1173
            $startHasChild = ($rangeEnd-$rangeStart) > 1 ? true : false;
1174
            $cwd = '/'.$this->getPathAsString($startId);
1175
        } else {
1176
            $cwd = '/';
1177
            $startHasChild = false;
1178
        }
1179
        $t = $this->_preparePath($path, $cwd, $separator);
1180
        if (Tree::isError($t)) {
1181
            return $t;
1182
        }
1183
        list($elems, $sublevels) = $t;
1184
        $cntElems = sizeof($elems);
1185
        $where = '';
1186
 
1187
        $query = 'SELECT '
1188
                .$this->_getColName('id')
1189
                .' FROM '
1190
                .$this->table
1191
                .' WHERE '
1192
                .$colname;
1193
        if ($cntElems == 1) {
1194
            $query .= "='".$elems[0]."'";
1195
        } else {
1196
            $query .= "='".$elems[$cntElems-1]."'";
1197
        }
1198
        if ($startHasChild) {
1199
            $where  .= ' AND ('.
1200
                        $this->_getColName('left').'>'.$rangeStart.
1201
                        ' AND '.
1202
                        $this->_getColName('right').'<'.$rangeEnd.')';
1203
        }
1204
        $res = $this->dbh->getOne($query);
1205
        if (MDB::isError($res)) {
1206
            return $this->_throwError($res->getMessage(),
1207
                        __LINE__);
1208
        }
1209
        return ($res ? (int)$res : false);
1210
    }
1211
 
1212
    // }}}
1213
 
1214
    //
1215
    //  PRIVATE METHODS
1216
    //
1217
 
1218
    // {{{ _getWhereAddOn()
1219
    /**
1220
     *
1221
     *
1222
     * @access     private
1223
     * @version    2002/04/20
1224
     * @author     Wolfram Kriesing <wolfram@kriesing.de>
1225
     * @param      string  the current where clause
1226
     * @return     string  the where clause we want to add to a query
1227
     */
1228
    function _getWhereAddOn($addAfter = ' AND ', $tableName = '')
1229
    {
1230
        if ($where=$this->getOption('whereAddOn')) {
1231
            return ' '.($tableName ? $tableName.'.' : '')." $where$addAfter ";
1232
        }
1233
        return '';
1234
    }
1235
 
1236
    // }}}
1237
    // {{{ getFirstRoot()
1238
 
1239
    // for compatibility to Memory methods
1240
    function getFirstRoot()
1241
    {
1242
        return $this->getRoot();
1243
    }
1244
 
1245
    // }}}
1246
    // {{{ getNode()
1247
 
1248
    /**
1249
     * gets the tree under the given element in one array, sorted
1250
     * so you can go through the elements from begin to end and list them
1251
     * as they are in the tree, where every child (until the deepest) is retreived
1252
     *
1253
     * @see        &_getNode()
1254
     * @access     public
1255
     * @version    2001/12/17
1256
     * @author     Wolfram Kriesing <wolfram@kriesing.de>
1257
     * @param      integer  $startId    the id where to start walking
1258
     * @param      integer  $depth      this number says how deep into
1259
     *                                  the structure the elements shall
1260
     *                                  be retreived
1261
     * @return     array    sorted as listed in the tree
1262
     */
1263
    function &getNode($startId = 0, $depth = 0)
1264
    {
1265
//FIXXXME use getChildren()
1266
        if ($startId) {
1267
            $startNode = $this->getElement($startId);
1268
            if (Tree::isError($startNode)) {
1269
                return $startNode;
1270
            }
1271
 
1272
        } else {
1273
        }
1274
    }
1275
}
1276
 
1277
/*
1278
* Local Variables:
1279
* mode: php
1280
* tab-width: 4
1281
* c-basic-offset: 4
1282
* End:
1283
*/