Subversion Repositories Applications.papyrus

Rev

Rev 1688 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
1173 jp_milcent 1
<?php
2
//
3
// +----------------------------------------------------------------------+
4
// | PEAR :: DB_NestedSet                                                 |
5
// +----------------------------------------------------------------------+
6
// | Copyright (c) 1997-2003 The PHP Group                                |
7
// +----------------------------------------------------------------------+
8
// | This source file is subject to version 2.0 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   |f
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: Daniel Khan <dk@webcluster.at>                              |
17
// |          Jason Rust  <jason@rustyparts.com>                          |
18
// +----------------------------------------------------------------------+
19
// $Id: NestedSet.php,v 1.1 2006-12-14 15:04:28 jp_milcent Exp $
20
//
21
 
22
// CREDITS:
23
// --------
24
// - Thanks to Kristian Koehntopp for publishing an explanation of the Nested Set
25
//   technique and for the great work he did and does for the php community
26
// - Thanks to Daniel T. Gorski for his great tutorial on www.develnet.org
27
//
28
// - Thanks to my parents for ... just kidding :]
29
 
30
require_once 'PEAR.php';
31
 
32
// {{{ constants
33
 
34
// Error and message codes
35
define('NESE_ERROR_RECURSION',    'E100');
36
define('NESE_ERROR_NODRIVER',   'E200');
37
define('NESE_ERROR_NOHANDLER',    'E300');
38
define('NESE_ERROR_TBLOCKED',     'E010');
39
define('NESE_MESSAGE_UNKNOWN',    'E0');
40
define('NESE_ERROR_NOTSUPPORTED', 'E1');
41
define('NESE_ERROR_PARAM_MISSING','E400');
42
define('NESE_ERROR_NOT_FOUND',    'E500');
43
define('NESE_ERROR_WRONG_MPARAM', 'E2');
44
 
45
// for moving a node before another
46
define('NESE_MOVE_BEFORE', 'BE');
47
// for moving a node after another
48
define('NESE_MOVE_AFTER', 'AF');
49
// for moving a node below another
50
define('NESE_MOVE_BELOW', 'SUB');
51
 
52
 
53
// Sortorders
54
define('NESE_SORT_LEVEL', 'SLV');
55
define('NESE_SORT_PREORDER', 'SPO');
56
 
57
// }}}
58
// {{{ DB_NestedSet:: class
59
/**
60
* DB_NestedSet is a class for handling nested sets
61
*
62
* @author       Daniel Khan <dk@webcluster.at>
63
* @package      DB_NestedSet
64
* @version      $Revision: 1.1 $
65
* @access       public
66
*/
67
 
68
// }}}
69
class DB_NestedSet {
70
    // {{{ properties
71
 
72
    /**
73
    * @var array The field parameters of the table with the nested set. Format: 'realFieldName' => 'fieldId'
74
    * @access public
75
    */
76
    var $params = array(
77
    'STRID' => 'id',
78
    'ROOTID'=> 'rootid',
79
    'l'     => 'l',
80
    'r'     => 'r',
81
    'STREH' => 'norder',
82
    'LEVEL' => 'level',
83
    // 'parent'=>'parent', // Optional but very useful
84
    'STRNA' => 'name'
85
    );
86
 
87
    // To be used with 2.0 - would be an api break atm
88
    // var $quotedParams = array('name');
89
 
90
    /**
91
    * @var string The table with the actual tree data
92
    * @access public
93
    */
94
    var $node_table = 'tb_nodes';
95
 
96
    /**
97
    * @var string The table to handle locking
98
    * @access public
99
    */
100
    var $lock_table = 'tb_locks';
101
 
102
    /**
103
    * @var string The table used for sequences
104
    * @access public
105
    */
106
    var $sequence_table;
107
 
108
    /**
109
    * Secondary order field.  Normally this is the order field, but can be changed to
110
    * something else (i.e. the name field so that the tree can be shown alphabetically)
111
    *
112
    * @var string
113
    * @access public
114
    */
115
    var $secondarySort;
116
 
117
    /**
118
    * Used to store the secondary sort method set by the user while doing manipulative queries
119
    *
120
    * @var string
121
    * @access private
122
    */
123
    var $_userSecondarySort = false;
124
 
125
    /**
126
    * The default sorting field - will be set to the table column inside the constructor
127
    *
128
    * @var string
129
    * @access private
130
    */
131
    var $_defaultSecondarySort = 'norder';
132
 
133
    /**
134
    * @var int The time to live of the lock
135
    * @access public
136
    */
137
    var $lockTTL = 1;
138
 
139
    /**
140
    * @var bool Enable debugging statements?
141
    * @access public
142
    */
143
    var $debug = 0;
144
 
145
    /**
146
    * @var bool Lock the structure of the table?
147
    * @access private
148
    */
149
    var $_structureTableLock = false;
150
 
151
 
152
    /**
153
    * @var bool Don't allow unlocking (used inside of moves)
154
    * @access private
155
    */
156
    var $_lockExclusive     = false;
157
 
158
    /**
159
    * @var object cache Optional PEAR::Cache object
160
    * @access public
161
    */
162
    var $cache = false;
163
 
164
    /**
165
    * Specify the sortMode of the query methods
166
    * NESE_SORT_LEVEL is the 'old' sorting method and sorts a tree by level
167
    * all nodes of level 1, all nodes of level 2,...
168
    * NESE_SORT_PREORDER will sort doing a preorder walk.
169
    * So all children of node x will come right after it
170
    * Note that moving a node within it's siblings will obviously not change the output
171
    * in this mode
172
    *
173
    * @var constant Order method (NESE_SORT_LEVEL|NESE_SORT_PREORDER)
174
    * @access private
175
    */
176
    var $_sortMode   = NESE_SORT_LEVEL;
177
 
178
    /**
179
    * @var array Available sortModes
180
    * @access private
181
    */
182
    var $_sortModes = array(NESE_SORT_LEVEL, NESE_SORT_PREORDER);
183
 
184
    /**
185
    * @var array An array of field ids that must exist in the table
186
    * @access private
187
    */
188
    var $_requiredParams = array('id', 'rootid', 'l', 'r', 'norder', 'level');
189
 
190
    /**
191
    * @var bool Skip the callback events?
192
    * @access private
193
    */
194
    var $_skipCallbacks = false;
195
 
196
    /**
197
    * @var bool Do we want to use caching
198
    * @access private
199
    */
200
    var $_caching = false;
201
 
202
    /**
203
    * @var array The above parameters flipped for easy access
204
    * @access private
205
    */
206
    var $_flparams = array();
207
 
208
    /**
209
    *
210
    * @var bool Temporary switch for cache
211
    * @access private
212
    */
213
    var $_restcache = false;
214
 
215
    /**
216
    * Used to determine the presence of listeners for an event in triggerEvent()
217
    *
218
    * If any event listeners are registered for an event, the event name will
219
    * have a key set in this array, otherwise, it will not be set.
220
    * @see triggerEvent()
221
    * @var arrayg
222
    * @access private
223
    */
224
    var $_hasListeners = array();
225
 
226
 
227
    /**
228
    * @var string packagename
229
    * @access private
230
    */
231
    var $_packagename   = 'DB_NestedSet';
232
 
233
    /**
234
    * @var int Majorversion
235
    * @access private
236
    */
237
    var $_majorversion   = 1;
238
 
239
    /**
240
    * @var string Minorversion
241
    * @access private
242
    */
243
    var $_minorversion   = '3';
244
 
245
    /**
246
    * @var array Used for mapping a cloned tree to the real tree for move_* operations
247
    * @access private
248
    */
249
    var $_relations      = array();
250
 
251
    /**
252
    * Used for _internal_ tree conversion
253
    * @var bool Turn off user param verification and id generation
254
    * @access private
255
    */
256
    var $_dumbmode      = false;
257
 
258
    /**
259
    * @var array Map of error messages to their descriptions
260
    */
261
    var $messages = array(
262
    NESE_ERROR_RECURSION    => '%s: This operation would lead to a recursion',
263
    NESE_ERROR_TBLOCKED     => 'The structure Table is locked for another database operation, please retry.',
264
    NESE_ERROR_NODRIVER   => 'The selected database driver %s wasn\'t found',
265
    NESE_ERROR_NOTSUPPORTED => 'Method not supported yet',
266
    NESE_ERROR_NOHANDLER    => 'Event handler not found',
267
    NESE_ERROR_PARAM_MISSING=> 'Parameter missing',
268
    NESE_MESSAGE_UNKNOWN    => 'Unknown error or message',
269
    NESE_ERROR_NOT_FOUND    => '%s: Node %s not found',
270
    NESE_ERROR_WRONG_MPARAM        => '%s: %s'
271
    );
272
 
273
    /**
274
    * @var array The array of event listeners
275
    * @access private
276
    */
277
    var $eventListeners = array();
278
 
279
 
280
    // }}}
281
    // +---------------------------------------+
282
    // | Base methods                          |
283
    // +---------------------------------------+
284
    // {{{ constructor
285
 
286
    /**
287
    * Constructor
288
    *
289
    * @param array $params Database column fields which should be returned
290
    *
291
    * @access private
292
    * @return void
293
    */
294
    function DB_NestedSet($params) {
295
 
296
        if ($this->debug) {
297
            $this->_debugMessage('DB_NestedSet()');
298
        }
299
        if (is_array($params) && count($params) > 0) {
300
            $this->params = $params;
301
        }
302
 
303
        $this->_flparams = array_flip($this->params);
304
        $this->sequence_table = $this->node_table . '_' . $this->_flparams['id'];
305
        $this->secondarySort = $this->_flparams[$this->_defaultSecondarySort];
306
        register_shutdown_function(array(&$this,'_DB_NestedSet'));
307
    }
308
 
309
    // }}}
310
    // {{{ destructor
311
 
312
    /**
313
    * PEAR Destructor
314
    * Releases all locks
315
    * Closes open database connections
316
    *
317
    * @access private
318
    * @return void
319
    */
320
    function _DB_NestedSet() {
321
        if ($this->debug) {
322
            $this->_debugMessage('_DB_NestedSet()');
323
        }
324
        $this->_releaseLock(true);
325
    }
326
 
327
    // }}}
328
    // {{{ factory
329
 
330
    /**
331
    * Handles the returning of a concrete instance of DB_NestedSet based on the driver.
332
    * If the class given by $driver allready exists it will be used.
333
    * If not the driver will be searched inside the default path ./NestedSet/
334
    *
335
    * @param string $driver The driver, such as DB or MDB
336
    * @param string $dsn The dsn for connecting to the database
337
    * @param array $params The field name params for the node table
338
    *
339
    * @static
340
    * @access public
341
    * @return object The DB_NestedSet object
342
    */
343
    function & factory($driver, $dsn, $params = array()) {
344
 
345
        $classname = 'DB_NestedSet_' . $driver;
346
        if (!class_exists($classname)) {
347
            $driverpath = dirname(__FILE__).'/NestedSet/'.$driver.'.php';
348
            if(!file_exists($driverpath) || !$driver) {
349
                return PEAR::raiseError("factory(): The database driver '$driver' wasn't found", NESE_ERROR_NODRIVER, PEAR_ERROR_TRIGGER, E_USER_ERROR);
350
            }
351
            include_once($driverpath);
352
        }
353
        return new $classname($dsn, $params);
354
    }
355
 
356
    // }}}
357
 
358
 
359
    // }}}
360
    // +----------------------------------------------+
361
    // | NestedSet manipulation and query methods     |
362
    // |----------------------------------------------+
363
    // | Querying the tree                            |
364
    // +----------------------------------------------+
365
 
366
    // {{{ getAllNodes()
367
 
368
    /**
369
    * Fetch the whole NestedSet
370
    *
371
    * @param bool $keepAsArray (optional) Keep the result as an array or transform it into
372
    *             a set of DB_NestedSet_Node objects?
373
    * @param bool $aliasFields (optional) Should we alias the fields so they are the names
374
    *             of the parameter keys, or leave them as is?
375
    * @param array $addSQL (optional) Array of additional params to pass to the query.
376
    *
377
    * @access public
378
    * @return mixed False on error, or an array of nodes
379
    */
380
    function getAllNodes($keepAsArray = false, $aliasFields = true, $addSQL = array()) {
381
        if ($this->debug) {
382
            $this->_debugMessage('getAllNodes()');
383
        }
384
 
385
        if($this->_sortMode == NESE_SORT_LEVEL) {
386
            $sql = sprintf('SELECT %s %s FROM %s %s %s ORDER BY %s.%s, %s.%s ASC',
387
            $this->_getSelectFields($aliasFields),
388
            $this->_addSQL($addSQL, 'cols'),
389
            $this->node_table,
390
            $this->_addSQL($addSQL, 'join'),
391
            $this->_addSQL($addSQL, 'append'),
392
            $this->node_table,
393
            $this->_flparams['level'],
394
            $this->node_table,
395
            $this->secondarySort);
396
        } elseif ($this->_sortMode == NESE_SORT_PREORDER) {
397
            $nodeSet = array();
398
            $rootnodes = $this->getRootNodes(true);
399
            foreach($rootnodes AS $rid=>$rootnode) {
400
                $nodeSet = $nodeSet+$this->getBranch($rootnode, true);
401
            }
402
            return $nodeSet;
403
        }
404
 
405
        if (!$this->_caching) {
406
            $nodeSet = $this->_processResultSet($sql, $keepAsArray, $aliasFields);
407
        } else {
408
            $nodeSet = $this->cache->call('DB_NestedSet->_processResultSet', $sql, $keepAsArray, $aliasFields);
409
        }
410
 
411
        if (!$this->_skipCallbacks && isset($this->_hasListeners['nodeLoad'])) {
412
            // EVENT (nodeLoad)
413
            foreach (array_keys($nodeSet) as $key) {
414
                $this->triggerEvent('nodeLoad', $nodeSet[$key]);
415
            }
416
        }
417
        return $nodeSet;
418
    }
419
 
420
    // }}}
421
    // {{{ getRootNodes()
422
 
423
    /**
424
    * Fetches the first level (the rootnodes) of the NestedSet
425
    *
426
    * @param bool $keepAsArray (optional) Keep the result as an array or transform it into
427
    *             a set of DB_NestedSet_Node objects?
428
    * @param bool $aliasFields (optional) Should we alias the fields so they are the names
429
    *             of the parameter keys, or leave them as is?
430
    * @param array $addSQL (optional) Array of additional params to pass to the query.
431
    *
432
    * @see _addSQL()
433
    * @access public
434
    * @return mixed False on error, or an array of nodes
435
    */
436
    function getRootNodes($keepAsArray = false, $aliasFields = true, $addSQL = array()) {
437
        if ($this->debug) {
438
            $this->_debugMessage('getRootNodes()');
439
        }
440
        $sql = sprintf('SELECT %s %s FROM %s %s WHERE %s.%s=%s.%s %s ORDER BY %s.%s ASC',
441
        $this->_getSelectFields($aliasFields),
442
        $this->_addSQL($addSQL, 'cols'),
443
        $this->node_table,
444
        $this->_addSQL($addSQL, 'join'),
445
        $this->node_table,
446
        $this->_flparams['id'],
447
        $this->node_table,
448
        $this->_flparams['rootid'],
449
        $this->_addSQL($addSQL, 'append'),
450
        $this->node_table,
451
        $this->secondarySort);
452
 
453
        if (!$this->_caching) {
454
            $nodeSet = $this->_processResultSet($sql, $keepAsArray, $aliasFields);
455
        } else {
456
            $nodeSet = $this->cache->call('DB_NestedSet->_processResultSet', $sql, $keepAsArray, $aliasFields);
457
        }
458
 
459
        if (!$this->_skipCallbacks && isset($this->_hasListeners['nodeLoad'])) {
460
            // EVENT (nodeLoad)
461
            foreach (array_keys($nodeSet) as $key) {
462
                $this->triggerEvent('nodeLoad', $nodeSet[$key]);
463
            }
464
        }
465
        return $nodeSet;
466
    }
467
 
468
    // }}}
469
 
470
    // {{{ getBranch()
471
 
472
    /**
473
    * Fetch the whole branch where a given node id is in
474
    *
475
    * @param int  $id The node ID
476
    * @param bool $keepAsArray (optional) Keep the result as an array or transform it into
477
    *             a set of DB_NestedSet_Node objects?
478
    * @param bool $aliasFields (optional) Should we alias the fields so they are the names
479
    *             of the parameter keys, or leave them as is?
480
    * @param array $addSQL (optional) Array of additional params to pass to the query.
481
    *
482
    * @see _addSQL()
483
    * @access public
484
    * @return mixed False on error, or an array of nodes
485
    */
486
    function getBranch($id, $keepAsArray = false, $aliasFields = true, $addSQL = array()) {
487
        if ($this->debug) {
488
            $this->_debugMessage('getBranch($id)');
489
        }
490
        if (!($thisnode = $this->pickNode($id, true))) {
491
            $epr = array('getBranch()', $id);
492
            return $this->_raiseError(NESE_ERROR_NOT_FOUND, PEAR_ERROR_TRIGGER, E_USER_NOTICE, $epr);
493
        }
494
        if($this->_sortMode == NESE_SORT_LEVEL) {
495
            $firstsort = $this->_flparams['level'];
496
            $sql = sprintf('SELECT %s %s FROM %s %s WHERE %s.%s=%s %s ORDER BY %s.%s, %s.%s ASC',
497
            $this->_getSelectFields($aliasFields),
498
            $this->_addSQL($addSQL, 'cols'),
499
            $this->node_table,
500
            $this->_addSQL($addSQL, 'join'),
501
            $this->node_table,
502
            $this->_flparams['rootid'],
503
            $thisnode['rootid'],
504
            $this->_addSQL($addSQL, 'append'),
505
            $this->node_table,
506
            $firstsort,
507
            $this->node_table,
508
            $this->secondarySort);
509
        } elseif($this->_sortMode == NESE_SORT_PREORDER) {
510
            $firstsort = $this->_flparams['l'];
511
            $sql = sprintf('SELECT %s %s FROM %s %s WHERE %s.%s=%s %s ORDER BY %s.%s ASC',
512
            $this->_getSelectFields($aliasFields),
513
            $this->_addSQL($addSQL, 'cols'),
514
            $this->node_table,
515
            $this->_addSQL($addSQL, 'join'),
516
            $this->node_table,
517
            $this->_flparams['rootid'],
518
            $thisnode['rootid'],
519
            $this->_addSQL($addSQL, 'append'),
520
            $this->node_table,
521
            $firstsort);
522
        }
523
 
524
 
525
        if (!$this->_caching) {
526
            $nodeSet = $this->_processResultSet($sql, $keepAsArray, $aliasFields);
527
        } else {
528
            $nodeSet = $this->cache->call('DB_NestedSet->_processResultSet', $sql, $keepAsArray, $aliasFields);
529
        }
530
 
531
        if (!$this->_skipCallbacks && isset($this->_hasListeners['nodeLoad'])) {
532
            // EVENT (nodeLoad)
533
            foreach (array_keys($nodeSet) as $key) {
534
                $this->triggerEvent('nodeLoad', $nodeSet[$key]);
535
            }
536
        }
537
        if($this->_sortMode == NESE_SORT_PREORDER && ($this->params[$this->secondarySort] != $this->_defaultSecondarySort)) {
538
            uasort($nodeSet, array($this, '_secSort'));
539
        }
540
        return $nodeSet;
541
    }
542
 
543
    // }}}
544
    // {{{ getParents()
545
 
546
    /**
547
    * Fetch the parents of a node given by id
548
    *
549
    * @param int  $id The node ID
550
    * @param bool $keepAsArray (optional) Keep the result as an array or transform it into
551
    *             a set of DB_NestedSet_Node objects?
552
    * @param bool $aliasFields (optional) Should we alias the fields so they are the names
553
    *             of the parameter keys, or leave them as is?
554
    * @param array $addSQL (optional) Array of additional params to pass to the query.
555
    *
556
    * @see _addSQL()
557
    * @access public
558
    * @return mixed False on error, or an array of nodes
559
    */
560
    function getParents($id, $keepAsArray = false, $aliasFields = true, $addSQL = array()) {
561
        if ($this->debug) {
562
            $this->_debugMessage('getParents($id)');
563
        }
564
        if (!($child = $this->pickNode($id, true))) {
565
            $epr = array('getParents()', $id);
566
            return $this->_raiseError(NESE_ERROR_NOT_FOUND, PEAR_ERROR_TRIGGER, E_USER_NOTICE, $epr);
567
        }
568
 
569
        $sql = sprintf('SELECT %s %s FROM %s %s
570
                        WHERE %s.%s=%s AND %s.%s<%s AND %s.%s<%s AND %s.%s>%s %s
571
                        ORDER BY %s.%s ASC',
572
        $this->_getSelectFields($aliasFields),
573
        $this->_addSQL($addSQL, 'cols'),
574
        $this->node_table,
575
        $this->_addSQL($addSQL, 'join'),
576
        $this->node_table,
577
        $this->_flparams['rootid'],
578
        $child['rootid'],
579
        $this->node_table,
580
        $this->_flparams['level'],
581
        $child['level'],
582
        $this->node_table,
583
        $this->_flparams['l'],
584
        $child['l'],
585
        $this->node_table,
586
        $this->_flparams['r'],
587
        $child['r'],
588
        $this->_addSQL($addSQL, 'append'),
589
        $this->node_table,
590
        $this->_flparams['level']);
591
 
592
        if (!$this->_caching) {
593
            $nodeSet = $this->_processResultSet($sql, $keepAsArray, $aliasFields);
594
        } else {
595
            $nodeSet = $this->cache->call('DB_NestedSet->_processResultSet', $sql, $keepAsArray, $aliasFields);
596
        }
597
 
598
        if (!$this->_skipCallbacks && isset($this->_hasListeners['nodeLoad'])) {
599
            // EVENT (nodeLoad)
600
            foreach (array_keys($nodeSet) as $key) {
601
                $this->triggerEvent('nodeLoad', $nodeSet[$key]);
602
            }
603
        }
604
        return $nodeSet;
605
    }
606
 
607
    // }}}
608
    // {{{ getParent()
609
 
610
    /**
611
    * Fetch the immediate parent of a node given by id
612
    *
613
    * @param int  $id The node ID
614
    * @param bool $keepAsArray (optional) Keep the result as an array or transform it into
615
    *             a set of DB_NestedSet_Node objects?
616
    * @param bool $aliasFields (optional) Should we alias the fields so they are the names
617
    *             of the parameter keys, or leave them as is?
618
    * @param array $addSQL (optional) Array of additional params to pass to the query.
619
    *
620
    * @see _addSQL()
621
    * @access public
622
    * @return mixed False on error, or the parent node
623
    */
624
    function getParent($id, $keepAsArray = false, $aliasFields = true, $addSQL = array()) {
625
        if ($this->debug) {
626
            $this->_debugMessage('getParent($id)');
627
        }
628
        if (!($child = $this->pickNode($id, true))) {
629
            $epr = array('getParent()', $id);
630
            return $this->_raiseError(NESE_ERROR_NOT_FOUND, PEAR_ERROR_TRIGGER, E_USER_NOTICE, $epr);
631
        }
632
 
633
        if($child['id'] == $child['rootid']) {
634
            return false;
635
        }
636
 
637
        // If parent node is set inside the db simply return it
638
        if(isset($child['parent']) && !empty($child['parent'])) {
639
            return $this->pickNode($child['parent'], $keepAsArray, $aliasFields, 'id', $addSQL);
640
        }
641
 
642
        $addSQL['append'] = sprintf('AND %s.%s = %s',
643
        $this->node_table,
644
        $this->_flparams['level'],
645
        $child['level']-1);
646
 
647
        $nodeSet =  $this->getParents($id, $keepAsArray, $aliasFields, $addSQL);
648
 
649
        if(!empty($nodeSet)) {
650
            $keys = array_keys($nodeSet);
651
            return $nodeSet[$keys[0]];
652
        } else {
653
            return false;
654
        }
655
    }
656
 
657
    // }}}
658
    // {{{ getSiblings)
659
 
660
    /**
661
    * Fetch all siblings of the node given by id
662
    * Important: The node given by ID will also be returned
663
    * Do a unset($array[$id]) on the result if you don't want that
664
    *
665
    * @param int  $id The node ID
666
    * @param bool $keepAsArray (optional) Keep the result as an array or transform it into
667
    *             a set of DB_NestedSet_Node objects?
668
    * @param bool $aliasFields (optional) Should we alias the fields so they are the names
669
    *             of the parameter keys, or leave them as is?
670
    * @param array $addSQL (optional) Array of additional params to pass to the query.
671
    *
672
    * @see _addSQL()
673
    * @access public
674
    * @return mixed False on error, or the parent node
675
    */
676
    function getSiblings($id, $keepAsArray = false, $aliasFields = true, $addSQL = array()) {
677
        if ($this->debug) {
678
            $this->_debugMessage('getParents($id)');
679
        }
680
 
681
        if (!($sibling = $this->pickNode($id, true))) {
682
            $epr = array('getSibling()', $id);
683
            return $this->_raiseError(NESE_ERROR_NOT_FOUND, PEAR_ERROR_TRIGGER, E_USER_NOTICE, $epr);
684
        }
685
 
686
        $parent = $this->getParent($sibling, true);
687
 
688
        return $this->getChildren($parent, $keepAsArray, $aliasFields, $addSQL);
689
    }
690
 
691
    // }}}
692
    // {{{ getChildren()
693
 
694
    /**
695
    * Fetch the children _one level_ after of a node given by id
696
    *
697
    * @param int  $id The node ID
698
    * @param bool $keepAsArray (optional) Keep the result as an array or transform it into
699
    *             a set of DB_NestedSet_Node objects?
700
    * @param bool $aliasFields (optional) Should we alias the fields so they are the names
701
    *             of the parameter keys, or leave them as is?
702
    * @param bool $forceNorder (optional) Force the result to be ordered by the norder
703
    *             param (as opposed to the value of secondary sort).  Used by the move and
704
    *             add methods.
705
    * @param array $addSQL (optional) Array of additional params to pass to the query.
706
    *
707
    * @see _addSQL()
708
    * @access public
709
    * @return mixed False on error, or an array of nodes
710
    */
711
    function getChildren($id, $keepAsArray = false, $aliasFields = true, $forceNorder = false, $addSQL = array()) {
712
        if ($this->debug) {
713
            $this->_debugMessage('getChildren($id)');
714
        }
715
 
716
        if (!($parent = $this->pickNode($id, true))) {
717
            $epr = array('getChildren()', $id);
718
            return $this->_raiseError(NESE_ERROR_NOT_FOUND, PEAR_ERROR_TRIGGER, E_USER_NOTICE, $epr);
719
        }
720
        if (!$parent || $parent['l'] == ($parent['r'] - 1)) {
721
            return false;
722
        }
723
 
724
        $sql = sprintf('SELECT %s %s FROM %s %s
725
                        WHERE %s.%s=%s AND %s.%s=%s+1 AND %s.%s BETWEEN %s AND %s %s
726
                        ORDER BY %s.%s ASC',
727
        $this->_getSelectFields($aliasFields),
728
        $this->_addSQL($addSQL, 'cols'),
729
        $this->node_table,
730
        $this->_addSQL($addSQL, 'join'),
731
        $this->node_table,
732
        $this->_flparams['rootid'],
733
        $parent['rootid'],
734
        $this->node_table,
735
        $this->_flparams['level'],
736
        $parent['level'],
737
        $this->node_table,
738
        $this->_flparams['l'],
739
        $parent['l'],
740
        $parent['r'],
741
        $this->_addSQL($addSQL, 'append'),
742
        $this->node_table,
743
        $this->secondarySort);
744
 
745
        if (!$this->_caching) {
746
            $nodeSet = $this->_processResultSet($sql, $keepAsArray, $aliasFields);
747
        } else {
748
            $nodeSet = $this->cache->call('DB_NestedSet->_processResultSet', $sql, $keepAsArray, $aliasFields);
749
        }
750
 
751
        if (!$this->_skipCallbacks && isset($this->_hasListeners['nodeLoad'])) {
752
            // EVENT (nodeLoad)
753
            foreach (array_keys($nodeSet) as $key) {
754
                $this->triggerEvent('nodeLoad', $nodeSet[$key]);
755
            }
756
        }
757
        return $nodeSet;
758
    }
759
 
760
    // }}}
761
    // {{{ getSubBranch()
762
 
763
    /**
764
    * Fetch all the children of a node given by id
765
    *
766
    * getChildren only queries the immediate children
767
    * getSubBranch returns all nodes below the given node
768
    *
769
    * @param string  $id The node ID
770
    * @param bool $keepAsArray (optional) Keep the result as an array or transform it into
771
    *             a set of DB_NestedSet_Node objects?
772
    * @param bool $aliasFields (optional) Should we alias the fields so they are the names
773
    *             of the parameter keys, or leave them as is?
774
    * @param array $addSQL (optional) Array of additional params to pass to the query.
775
    *
776
    * @see _addSQL()
777
    * @access public
778
    * @return mixed False on error, or an array of nodes
779
    */
780
    function getSubBranch($id, $keepAsArray = false, $aliasFields = true, $addSQL = array()) {
781
 
782
        if ($this->debug) {
783
            $this->_debugMessage('getSubBranch($id)');
784
        }
785
        if (!($parent = $this->pickNode($id, true))) {
786
            $epr = array('getSubBranch()', $id);
787
            return $this->_raiseError(NESE_ERROR_NOT_FOUND, E_USER_NOTICE, $epr);
788
        }
789
        if($this->_sortMode == NESE_SORT_LEVEL) {
790
            $firstsort = $this->_flparams['level'];
791
            $sql = sprintf('SELECT %s %s FROM %s %s WHERE %s.%s BETWEEN %s AND %s AND %s.%s=%s AND %s.%s!=%s %s ORDER BY %s.%s, %s.%s ASC',
792
            $this->_getSelectFields($aliasFields),
793
            $this->_addSQL($addSQL, 'cols'),
794
            $this->node_table,
795
            $this->_addSQL($addSQL, 'join'),
796
            $this->node_table,
797
            $this->_flparams['l'],
798
            $parent['l'],
799
            $parent['r'],
800
            $this->node_table,
801
            $this->_flparams['rootid'],
802
            $parent['rootid'],
803
            $this->node_table,
804
            $this->_flparams['id'],
805
            $this->_addSQL($addSQL, 'append'),
806
            $id,
807
            $this->node_table,
808
            $firstsort,
809
            $this->node_table,
810
            $this->secondarySort
811
            );
812
        } elseif($this->_sortMode == NESE_SORT_PREORDER) {
813
            $firstsort = $this->_flparams['l'];
814
            $firstsort = $this->_flparams['level'];
815
            $sql = sprintf('SELECT %s %s FROM %s %s WHERE %s.%s BETWEEN %s AND %s AND %s.%s=%s AND %s.%s!=%s %s ORDER BY %s.%s  ASC',
816
            $this->_getSelectFields($aliasFields),
817
            $this->_addSQL($addSQL, 'cols'),
818
            $this->node_table,
819
            $this->_addSQL($addSQL, 'join'),
820
            $this->node_table,
821
            $this->_flparams['l'],
822
            $parent['l'],
823
            $parent['r'],
824
            $this->node_table,
825
            $this->_flparams['rootid'],
826
            $parent['rootid'],
827
            $this->node_table,
828
            $this->_flparams['id'],
829
            $this->_addSQL($addSQL, 'append'),
830
            $id,
831
            $this->node_table,
832
            $firstsort
833
            );
834
        }
835
 
836
        if (!$this->_caching) {
837
            $nodeSet = $this->_processResultSet($sql, $keepAsArray, $aliasFields);
838
        } else {
839
            $nodeSet = $this->cache->call('DB_NestedSet->_processResultSet', $sql, $keepAsArray, $aliasFields);
840
        }
841
 
842
        if (!$this->_skipCallbacks && isset($this->_hasListeners['nodeLoad'])) {
843
            // EVENT (nodeLoad)
844
            foreach (array_keys($nodeSet) as $key) {
845
                $this->triggerEvent('nodeLoad', $nodeSet[$key]);
846
            }
847
        }
848
        if($this->params[$this->secondarySort] != $this->_defaultSecondarySort) {
849
            uasort($nodeSet, array($this, '_secSort'));
850
        }
851
        return $nodeSet;
852
    }
853
 
854
    // }}}
855
    // {{{ pickNode()
856
 
857
    /**
858
    * Fetch the data of a node with the given id
859
    *
860
    * @param int  $id The node id of the node to fetch
861
    * @param bool $keepAsArray (optional) Keep the result as an array or transform it into
862
    *             a set of DB_NestedSet_Node objects?
863
    * @param bool $aliasFields (optional) Should we alias the fields so they are the names
864
    *             of the parameter keys, or leave them as is?
865
    * @param string $idfield (optional) Which field has to be compared with $id?
866
    *              This is can be used to pick a node by other values (e.g. it's name).
867
    * @param array $addSQL (optional) Array of additional params to pass to the query.
868
    *
869
    * @see _addSQL()
870
    * @access public
871
    * @return mixed False on error, or an array of nodes
872
    */
873
    function pickNode($id, $keepAsArray = false, $aliasFields = true, $idfield = 'id', $addSQL = array()) {
874
        if ($this->debug) {
875
            $this->_debugMessage('pickNode($id)');
876
        }
877
 
878
        if (is_object($id) && $id->id) {
879
            return $id;
880
        } elseif (is_array($id) && isset($id['id'])) {
881
            return $id;
882
        }
883
 
884
        if(!$id) {
885
            return false;
886
        }
887
 
888
        $sql = sprintf('SELECT %s %s FROM %s %s WHERE %s.%s=%s %s',
889
        $this->_getSelectFields($aliasFields),
890
        $this->_addSQL($addSQL, 'cols'),
891
        $this->node_table,
892
        $this->_addSQL($addSQL, 'join'),
893
        $this->node_table,
894
        $this->_flparams[$idfield],
895
        $id,
896
        $this->_addSQL($addSQL, 'append'));
897
 
898
        if (!$this->_caching) {
899
            $nodeSet = $this->_processResultSet($sql, $keepAsArray, $aliasFields);
900
        } else {
901
            $nodeSet = $this->cache->call('DB_NestedSet->_processResultSet', $sql, $keepAsArray, $aliasFields);
902
        }
903
 
904
        $nsKey = false;
905
 
906
        if (!$this->_skipCallbacks && isset($this->_hasListeners['nodeLoad'])) {
907
            // EVENT (nodeLoad)
908
            foreach (array_keys($nodeSet) as $key) {
909
                $this->triggerEvent('nodeLoad', $nodeSet[$key]);
910
                $nsKey = $key;
911
            }
912
        } else {
913
            foreach (array_keys($nodeSet) as $key) {
914
                $nsKey = $key;
915
            }
916
        }
917
 
918
        if (is_array($nodeSet) && $idfield != 'id') {
919
            $id = $nsKey;
920
        }
921
 
922
        return isset($nodeSet[$id]) ? $nodeSet[$id] : false;
923
    }
924
 
925
    // }}}
926
    // {{{ isParent()
927
 
928
    /**
929
    * See if a given node is a parent of another given node
930
    *
931
    * A node is considered to be a parent if it resides above the child
932
    * So it doesn't mean that the node has to be an immediate parent.
933
    * To get this information simply compare the levels of the two nodes
934
    * after you know that you have a parent relation.
935
    *
936
    * @param mixed  $parent The parent node as array or object
937
    * @param mixed  $child  The child node as array or object
938
    *
939
    * @access public
940
    * @return bool True if it's a parent
941
    */
942
    function isParent($parent, $child) {
943
 
944
        if ($this->debug) {
945
            $this->_debugMessage('isParent($parent, $child)');
946
        }
947
 
948
        if (!isset($parent)|| !isset($child)) {
949
            return false;
950
        }
951
 
952
        if (is_array($parent)) {
953
            $p_rootid   = $parent['rootid'];
954
            $p_l        = $parent['l'];
955
            $p_r        = $parent['r'];
956
 
957
        } elseif (is_object($parent)) {
958
            $p_rootid   = $parent->rootid;
959
            $p_l        = $parent->l;
960
            $p_r        = $parent->r;
961
        }
962
 
963
        if (is_array($child)) {
964
            $c_rootid   = $child['rootid'];
965
            $c_l        = $child['l'];
966
            $c_r        = $child['r'];
967
        } elseif (is_object($child)) {
968
            $c_rootid   = $child->rootid;
969
            $c_l        = $child->l;
970
            $c_r        = $child->r;
971
        }
972
 
973
        if (($p_rootid == $c_rootid) && ($p_l < $c_l && $p_r > $c_r)) {
974
            return true;
975
        }
976
 
977
        return false;
978
    }
979
 
980
 
981
    // }}}
982
    // +----------------------------------------------+
983
    // | NestedSet manipulation and query methods     |
984
    // |----------------------------------------------+
985
    // | insert / delete / update of nodes            |
986
    // +----------------------------------------------+
987
    // | [PUBLIC]                                     |
988
    // +----------------------------------------------+
989
    // {{{ createRootNode()
990
 
991
    /**
992
    * Creates a new root node
993
    * Optionally it deletes the whole tree and creates one initial rootnode
994
    *
995
    * <pre>
996
    * +-- root1 [target]
997
    * |
998
    * +-- root2 [new]
999
    * |
1000
    * +-- root3
1001
    * </pre>
1002
    *
1003
    * @param array    $values      Hash with param => value pairs of the node (see $this->params)
1004
    * @param integer  $id          ID of target node (the rootnode after which the node should be inserted)
1005
    * @param bool     $first       Danger: Deletes and (re)init's the hole tree - sequences are reset
1006
    *
1007
    * @access public
1008
    * @return mixed The node id or false on error
1009
    */
1010
    function createRootNode($values, $id = false, $first = false, $_pos = 'AF') {
1011
 
1012
        if ($this->debug) {
1013
            $this->_debugMessage('createRootNode($values, $id = false, $first = false, $_pos = \'AF\')');
1014
        }
1015
 
1016
        $this->_verifyUserValues('createRootNode()', $values);
1017
 
1018
        if(!$first && (!$id || !$parent = $this->pickNode($id, true))) {
1019
            $epr = array('createRootNode()', $id);
1020
            return $this->_raiseError(NESE_ERROR_NOT_FOUND, PEAR_ERROR_TRIGGER, E_USER_ERROR, $epr);
1021
        } elseif($first && $id) {
1022
 
1023
            // No notice for now.
1024
            // But tehese 2 params don't make sense together
1025
            $epr = array(
1026
            'createRootNode()',
1027
            '[id] AND [first] were passed - that doesn\'t make sense');
1028
            //$this->_raiseError(NESE_ERROR_WRONG_MPARAM, E_USER_WARNING, $epr);
1029
        }
1030
 
1031
        // Try to aquire a table lock
1032
        if(PEAR::isError($lock=$this->_setLock())) {
1033
            return $lock;
1034
        }
1035
 
1036
        $sql = array();
1037
        $addval = array();
1038
        $addval[$this->_flparams['level']] = 1;
1039
 
1040
        // Shall we delete the existing tree (reinit)
1041
        if ($first) {
1042
            $dsql = sprintf('DELETE FROM %s',
1043
            $this->node_table);
1044
            $this->db->query($dsql);
1045
            $this->db->dropSequence($this->sequence_table);
1046
            // New order of the new node will be 1
1047
            $addval[$this->_flparams['norder']] = 1;
1048
        } else {
1049
            // Let's open a gap for the new node
1050
            if($_pos == NESE_MOVE_AFTER) {
1051
                $addval[$this->_flparams['norder']] = $parent['norder'] + 1;
1052
                $sql[] = sprintf('UPDATE %s SET %s=%s+1 WHERE %s=%s AND %s > %s',
1053
                $this->node_table,
1054
                $this->_flparams['norder'],
1055
                $this->_flparams['norder'],
1056
                $this->_flparams['id'],
1057
                $this->_flparams['rootid'],
1058
                $this->_flparams['norder'],
1059
                $parent['norder']);
1060
            } elseif($_pos == NESE_MOVE_BEFORE) {
1061
                $addval[$this->_flparams['norder']] = $parent['norder'];
1062
                $sql[] = sprintf('UPDATE %s SET %s=%s+1 WHERE %s=%s AND %s >= %s',
1063
                $this->node_table,
1064
                $this->_flparams['norder'],
1065
                $this->_flparams['norder'],
1066
                $this->_flparams['id'],
1067
                $this->_flparams['rootid'],
1068
                $this->_flparams['norder'],
1069
                $parent['norder']);
1070
            }
1071
        }
1072
 
1073
        if(isset($this->_flparams['parent'])) {
1074
           $addval[$this->_flparams['parent']] = 0;
1075
        }
1076
        // Sequence of node id (equals to root id in this case
1077
 
1078
        if(!$this->_dumbmode || !$node_id=isset($values[$this->_flparams['id']]) || !isset($values[$this->_flparams['rootid']])) {
1079
            $addval[$this->_flparams['rootid']] = $node_id = $addval[$this->_flparams['id']] = $this->db->nextId($this->sequence_table);
1080
        } else {
1081
            $node_id = $values[$this->_flparams['id']];
1082
        }
1083
        // Left/Right values for rootnodes
1084
        $addval[$this->_flparams['l']] = 1;
1085
        $addval[$this->_flparams['r']] = 2;
1086
        // Transform the node data hash to a query
1087
        if (!$qr = $this->_values2Query($values, $addval)) {
1088
            $this->_releaseLock();
1089
            return false;
1090
        }
1091
 
1092
        // Insert the new node
1093
        $sql[] = sprintf('INSERT INTO %s SET %s',
1094
        $this->node_table,
1095
        $qr);
1096
 
1097
        for($i=0;$i<count($sql);$i++) {
1098
            $res = $this->db->query($sql[$i]);
1099
            $this->_testFatalAbort($res, __FILE__,  __LINE__);
1100
        }
1101
 
1102
        // EVENT (nodeCreate)
1103
 
1104
        if (!$this->_skipCallbacks && isset($this->_hasListeners['nodeCreate'])) {
1105
            $this->triggerEvent('nodeCreate', $this->pickNode($node_id));
1106
        }
1107
        $this->_releaseLock();
1108
        return $node_id;
1109
    }
1110
 
1111
    // }}}
1112
    // {{{ createSubNode()
1113
 
1114
    /**
1115
    * Creates a subnode
1116
    *
1117
    * <pre>
1118
    * +-- root1
1119
    * |
1120
    * +-\ root2 [target]
1121
    * | |
1122
    * | |-- subnode1 [new]
1123
    * |
1124
    * +-- root3
1125
    * </pre>
1126
    *
1127
    * @param integer    $id          Parent node ID
1128
    * @param array      $values      Hash with param => value pairs of the node (see $this->params)
1129
    *
1130
    * @access public
1131
    * @return mixed The node id or false on error
1132
    */
1133
    function createSubNode($id, $values) {
1134
        if ($this->debug) {
1135
            $this->_debugMessage('createSubNode($id, $values)');
1136
        }
1137
 
1138
        // invalid parent id, bail out
1139
        if (!($thisnode = $this->pickNode($id, true))) {
1140
            $epr = array('createSubNode()', $id);
1141
            return $this->_raiseError(NESE_ERROR_NOT_FOUND, PEAR_ERROR_TRIGGER, E_USER_ERROR, $epr);
1142
        }
1143
 
1144
        // Try to aquire a table lock
1145
        if(PEAR::isError($lock = $this->_setLock())) {
1146
            return $lock;
1147
        }
1148
 
1149
        $this->_verifyUserValues('createRootNode()', $values);
1150
 
1151
        // Get the children of the target node
1152
        $children = $this->getChildren($id, true);
1153
 
1154
        // We have children here
1155
        if ($thisnode['r']-1 != $thisnode['l']) {
1156
            // Get the last child
1157
            $last = array_pop($children);
1158
            // What we have to do is virtually an insert of a node after the last child
1159
            // So we don't have to proceed creating a subnode
1160
            $newNode = $this->createRightNode($last['id'], $values);
1161
            $this->_releaseLock();
1162
            return $newNode;
1163
        }
1164
 
1165
        $sql = array();
1166
        $sql[] = sprintf('
1167
                UPDATE %s SET
1168
                %s=IF(%s>=%s, %s+2, %s),
1169
                %s=IF(%s>=%s, %s+2, %s)
1170
                WHERE %s=%s',
1171
        $this->node_table,
1172
        $this->_flparams['l'],
1173
        $this->_flparams['l'],
1174
        $thisnode['r'],
1175
        $this->_flparams['l'],
1176
        $this->_flparams['l'],
1177
        $this->_flparams['r'],
1178
        $this->_flparams['r'],
1179
        $thisnode['r'],
1180
        $this->_flparams['r'],
1181
        $this->_flparams['r'],
1182
        $this->_flparams['rootid'],
1183
        $thisnode['rootid']
1184
        );
1185
 
1186
        $addval = array();
1187
        if(isset($this->_flparams['parent'])) {
1188
           $addval[$this->_flparams['parent']] = $thisnode['id'];
1189
        }
1190
 
1191
        $addval[$this->_flparams['l']] = $thisnode['r'];
1192
        $addval[$this->_flparams['r']] = $thisnode['r'] + 1;
1193
        $addval[$this->_flparams['rootid']] = $thisnode['rootid'];
1194
        $addval[$this->_flparams['norder']] = 1;
1195
        $addval[$this->_flparams['level']] = $thisnode['level'] + 1;
1196
 
1197
        if(!$this->_dumbmode || !$node_id=isset($values[$this->_flparams['id']])) {
1198
            $node_id = $addval[$this->_flparams['id']] = $this->db->nextId($this->sequence_table);
1199
        } else {
1200
            $node_id = $values[$this->_flparams['id']];
1201
        }
1202
        if (!$qr = $this->_values2Query($values, $addval)) {
1203
            $this->_releaseLock();
1204
            return false;
1205
        }
1206
 
1207
        $sql[] = sprintf('INSERT INTO %s SET %s',
1208
        $this->node_table,
1209
        $qr);
1210
        for($i=0;$i<count($sql);$i++) {
1211
            $res = $this->db->query($sql[$i]);
1212
            $this->_testFatalAbort($res, __FILE__,  __LINE__);
1213
        }
1214
 
1215
        // EVENT (NodeCreate)
1216
        if (!$this->_skipCallbacks && isset($this->_hasListeners['nodeCreate'])) {
1217
            $thisnode = $this->pickNode($node_id);
1218
            $this->triggerEvent('nodeCreate', $this->pickNode($id));
1219
        }
1220
        $this->_releaseLock();
1221
        return $node_id;
1222
    }
1223
 
1224
    // }}}
1225
    // {{{ createLeftNode()
1226
    /**
1227
    * Creates a node before a given node
1228
    * <pre>
1229
    * +-- root1
1230
    * |
1231
    * +-\ root2
1232
    * | |
1233
    * | |-- subnode2 [new]
1234
    * | |-- subnode1 [target]
1235
    * | |-- subnode3
1236
    * |
1237
    * +-- root3
1238
    * </pre>
1239
    *
1240
    * @param int   $id        Target node ID
1241
    * @param array      $values      Hash with param => value pairs of the node (see $this->params)
1242
    * @param bool       $returnID    Tell the method to return a node id instead of an object.
1243
    *                                ATTENTION: That the method defaults to return an object instead of the node id
1244
    *                                has been overseen and is basically a bug. We have to keep this to maintain BC.
1245
    *                                You will have to set $returnID to true to make it behave like the other creation methods.
1246
    *                                This flaw will get fixed with the next major version.
1247
    *
1248
    * @access public
1249
    * @return mixed The node id or false on error
1250
    */
1251
    function createLeftNode($id, $values) {
1252
 
1253
        if ($this->debug) {
1254
            $this->_debugMessage('createLeftNode($target, $values)');
1255
        }
1256
 
1257
        $this->_verifyUserValues('createLeftode()', $values);
1258
 
1259
        // invalid target node, bail out
1260
        if (!($thisnode = $this->pickNode($id, true))) {
1261
            $epr = array('createLeftNode()', $id);
1262
            return $this->_raiseError(NESE_ERROR_NOT_FOUND, PEAR_ERROR_TRIGGER, E_USER_ERROR, $epr);
1263
        }
1264
 
1265
        if(PEAR::isError($lock=$this->_setLock())) {
1266
            return $lock;
1267
        }
1268
 
1269
 
1270
        // If the target node is a rootnode we virtually want to create a new root node
1271
        if ($thisnode['rootid'] == $thisnode['id']) {
1272
            return $this->createRootNode($values, $id, false, NESE_MOVE_BEFORE);
1273
        }
1274
 
1275
 
1276
        $addval = array();
1277
        $parent = $this->getParent($id, true);
1278
        if(isset($this->_flparams['parent'])) {
1279
           $addval[$this->_flparams['parent']] = $parent['id'];
1280
        }
1281
 
1282
        $sql = array();
1283
 
1284
 
1285
        $sql[] = sprintf('UPDATE %s SET %s=%s+1
1286
                        WHERE
1287
                        %s=%s AND %s>=%s AND %s=%s AND %s BETWEEN %s AND %s',
1288
        $this->node_table,
1289
        $this->_flparams['norder'],
1290
        $this->_flparams['norder'],
1291
        $this->_flparams['rootid'],
1292
        $thisnode['rootid'],
1293
        $this->_flparams['norder'],
1294
        $thisnode['norder'],
1295
        $this->_flparams['level'],
1296
        $thisnode['level'],
1297
        $this->_flparams['l'],
1298
        $parent['l'],
1299
        $parent['r']);
1300
 
1301
 
1302
        // Update all nodes which have dependent left and right values
1303
        $sql[] = sprintf('
1304
                UPDATE %s SET
1305
                %s=IF(%s>=%s, %s+2, %s),
1306
                %s=IF(%s>=%s, %s+2, %s)
1307
                WHERE %s=%s',
1308
                $this->node_table,
1309
                $this->_flparams['l'],
1310
                $this->_flparams['l'],
1311
                $thisnode['l'],
1312
                $this->_flparams['l'],
1313
                $this->_flparams['l'],
1314
                $this->_flparams['r'],
1315
                $this->_flparams['r'],
1316
                $thisnode['r'],
1317
                $this->_flparams['r'],
1318
                $this->_flparams['r'],
1319
                $this->_flparams['rootid'],
1320
                $thisnode['rootid']
1321
        );
1322
 
1323
 
1324
 
1325
        $addval[$this->_flparams['norder']] = $thisnode['norder'];
1326
        $addval[$this->_flparams['l']] = $thisnode['l'];
1327
        $addval[$this->_flparams['r']] = $thisnode['l']+1;
1328
        $addval[$this->_flparams['rootid']] = $thisnode['rootid'];
1329
        $addval[$this->_flparams['level']] = $thisnode['level'];
1330
 
1331
        if(!$this->_dumbmode || !$node_id=isset($values[$this->_flparams['id']])) {
1332
            $node_id = $addval[$this->_flparams['id']] = $this->db->nextId($this->sequence_table);
1333
        } else {
1334
            $node_id = $values[$this->_flparams['id']];
1335
        }
1336
        if (!$qr = $this->_values2Query($values, $addval)) {
1337
            $this->_releaseLock();
1338
            return false;
1339
        }
1340
 
1341
        // Insert the new node
1342
        $sql[] = sprintf('INSERT INTO %s SET %s',
1343
        $this->node_table,
1344
        $qr);
1345
 
1346
        for($i=0;$i<count($sql);$i++) {
1347
            $res = $this->db->query($sql[$i]);
1348
            $this->_testFatalAbort($res, __FILE__,  __LINE__);
1349
        }
1350
 
1351
        // EVENT (NodeCreate)
1352
        if (!$this->_skipCallbacks && isset($this->_hasListeners['nodeCreate'])) {
1353
            $this->triggerEvent('nodeCreate', $this->pickNode($id));
1354
        }
1355
        $this->_releaseLock();
1356
        return $node_id;
1357
    }
1358
 
1359
    /**
1360
    * Creates a node after a given node
1361
    * <pre>
1362
    * +-- root1
1363
    * |
1364
    * +-\ root2
1365
    * | |
1366
    * | |-- subnode1 [target]
1367
    * | |-- subnode2 [new]
1368
    * | |-- subnode3
1369
    * |
1370
    * +-- root3
1371
    * </pre>
1372
    *
1373
    * @param int   $id        Target node ID
1374
    * @param array      $values      Hash with param => value pairs of the node (see $this->params)
1375
    * @param bool       $returnID    Tell the method to return a node id instead of an object.
1376
    *                                ATTENTION: That the method defaults to return an object instead of the node id
1377
    *                                has been overseen and is basically a bug. We have to keep this to maintain BC.
1378
    *                                You will have to set $returnID to true to make it behave like the other creation methods.
1379
    *                                This flaw will get fixed with the next major version.
1380
    *
1381
    * @access public
1382
    * @return mixed The node id or false on error
1383
    */
1384
    function createRightNode($id, $values) {
1385
 
1386
        if ($this->debug) {
1387
            $this->_debugMessage('createRightNode($target, $values)');
1388
        }
1389
 
1390
        $this->_verifyUserValues('createRootNode()', $values);
1391
 
1392
        // invalid target node, bail out
1393
        if (!($thisnode = $this->pickNode($id, true))) {
1394
            $epr = array('createRightNode()', $id);
1395
            return $this->_raiseError(NESE_ERROR_NOT_FOUND, PEAR_ERROR_TRIGGER, E_USER_ERROR, $epr);
1396
        }
1397
 
1398
        if(PEAR::isError($lock=$this->_setLock())) {
1399
            return $lock;
1400
        }
1401
 
1402
 
1403
        // If the target node is a rootnode we virtually want to create a new root node
1404
        if ($thisnode['rootid'] == $thisnode['id']) {
1405
 
1406
            $nid = $this->createRootNode($values, $id);
1407
            $this->_releaseLock();
1408
            return $nid;
1409
        }
1410
 
1411
 
1412
        $addval = array();
1413
        $parent = $this->getParent($id, true);
1414
        if(isset($this->_flparams['parent'])) {
1415
           $addval[$this->_flparams['parent']] = $parent['id'];
1416
        }
1417
 
1418
        $sql = array();
1419
 
1420
        $sql[] = sprintf('UPDATE %s SET %s=%s+1
1421
                        WHERE
1422
                        %s=%s AND %s>%s AND %s=%s AND %s BETWEEN %s AND %s',
1423
        $this->node_table,
1424
        $this->_flparams['norder'],
1425
        $this->_flparams['norder'],
1426
        $this->_flparams['rootid'],
1427
        $thisnode['rootid'],
1428
        $this->_flparams['norder'],
1429
        $thisnode['norder'],
1430
        $this->_flparams['level'],
1431
        $thisnode['level'],
1432
        $this->_flparams['l'],
1433
        $parent['l'],
1434
        $parent['r']);
1435
 
1436
 
1437
        // Update all nodes which have dependent left and right values
1438
 
1439
 
1440
        $sql[] = sprintf('
1441
                UPDATE %s SET
1442
                %s=IF(%s>%s, %s+2, %s),
1443
                %s=IF(%s>%s, %s+2, %s)
1444
                WHERE %s=%s',
1445
        $this->node_table,
1446
        $this->_flparams['l'],
1447
        $this->_flparams['l'],
1448
        $thisnode['r'],
1449
        $this->_flparams['l'],
1450
        $this->_flparams['l'],
1451
        $this->_flparams['r'],
1452
        $this->_flparams['r'],
1453
        $thisnode['r'],
1454
        $this->_flparams['r'],
1455
        $this->_flparams['r'],
1456
        $this->_flparams['rootid'],
1457
        $thisnode['rootid']
1458
        );
1459
 
1460
        $addval[$this->_flparams['norder']] = $thisnode['norder'] + 1;
1461
        $addval[$this->_flparams['l']] = $thisnode['r'] + 1;
1462
        $addval[$this->_flparams['r']] = $thisnode['r'] + 2;
1463
        $addval[$this->_flparams['rootid']] = $thisnode['rootid'];
1464
        $addval[$this->_flparams['level']] = $thisnode['level'];
1465
 
1466
        if(!$this->_dumbmode || !isset($values[$this->_flparams['id']])) {
1467
            $node_id = $addval[$this->_flparams['id']] = $this->db->nextId($this->sequence_table);
1468
        } else {
1469
            $node_id = $values[$this->_flparams['id']];
1470
        }
1471
        if (!$qr = $this->_values2Query($values, $addval)) {
1472
            $this->_releaseLock();
1473
            return false;
1474
        }
1475
 
1476
        // Insert the new node
1477
        $sql[] = sprintf('INSERT INTO %s SET %s', $this->node_table, $qr);
1478
 
1479
        for($i=0;$i<count($sql);$i++) {
1480
            $res = $this->db->query($sql[$i]);
1481
            $this->_testFatalAbort($res, __FILE__,  __LINE__);
1482
        }
1483
 
1484
        // EVENT (NodeCreate)
1485
        if (!$this->_skipCallbacks && isset($this->_hasListeners['nodeCreate'])) {
1486
            $this->triggerEvent('nodeCreate', $this->pickNode($id));
1487
        }
1488
        $this->_releaseLock();
1489
        return $node_id;
1490
    }
1491
 
1492
    // }}}
1493
    // {{{ deleteNode()
1494
 
1495
    /**
1496
    * Deletes a node
1497
    *
1498
    * @param int $id ID of the node to be deleted
1499
    *
1500
    * @access public
1501
    * @return bool True if the delete succeeds
1502
    */
1503
    function deleteNode($id) {
1504
 
1505
        if ($this->debug) {
1506
            $this->_debugMessage("deleteNode($id)");
1507
        }
1508
 
1509
        // invalid target node, bail out
1510
        if (!($thisnode = $this->pickNode($id, true))) {
1511
            $epr = array('deleteNode()', $id);
1512
            return $this->_raiseError(NESE_ERROR_NOT_FOUND, PEAR_ERROR_TRIGGER, E_USER_ERROR, $epr);
1513
        }
1514
 
1515
        if (PEAR::isError($lock = $this->_setLock())) {
1516
            return $lock;
1517
        }
1518
 
1519
        if (!$this->_skipCallbacks && isset($this->_hasListeners['nodeDelete'])) {
1520
            // EVENT (NodeDelete)
1521
            $this->triggerEvent('nodeDelete', $this->pickNode($id));
1522
        }
1523
 
1524
        $parent = $this->getParent($id, true);
1525
        $len = $thisnode['r'] - $thisnode['l'] + 1;
1526
 
1527
 
1528
        $sql = array();
1529
 
1530
        // Delete the node
1531
        $sql[] = sprintf('DELETE FROM %s WHERE %s BETWEEN %s AND %s AND %s=%s',
1532
        $this->node_table,
1533
        $this->_flparams['l'],
1534
        $thisnode['l'],
1535
        $thisnode['r'],
1536
        $this->_flparams['rootid'],
1537
        $thisnode['rootid']
1538
        );
1539
 
1540
        if ($thisnode['id'] != $thisnode['rootid']) {
1541
 
1542
            // The node isn't a rootnode so close the gap
1543
            $sql[] = sprintf('UPDATE %s SET
1544
                            %s=IF(%s>%s, %s-%s, %s),
1545
                            %s=IF(%s>%s, %s-%s, %s)
1546
                            WHERE %s=%s AND
1547
                            (%s>%s OR %s>%s)',
1548
            $this->node_table,
1549
            $this->_flparams['l'],
1550
            $this->_flparams['l'],
1551
            $thisnode['l'],
1552
            $this->_flparams['l'],
1553
            $len,
1554
            $this->_flparams['l'],
1555
            $this->_flparams['r'],
1556
            $this->_flparams['r'],
1557
            $thisnode['l'],
1558
            $this->_flparams['r'],
1559
            $len,
1560
            $this->_flparams['r'],
1561
            $this->_flparams['rootid'],
1562
            $thisnode['rootid'],
1563
            $this->_flparams['l'],
1564
            $thisnode['l'],
1565
            $this->_flparams['r'],
1566
            $thisnode['r']
1567
            );
1568
 
1569
            // Re-order
1570
 
1571
            $sql[] = sprintf('UPDATE %s SET %s=%s-1 WHERE %s=%s AND %s=%s AND %s>%s AND %s BETWEEN %s AND %s',
1572
            $this->node_table,
1573
            $this->_flparams['norder'],
1574
            $this->_flparams['norder'],
1575
            $this->_flparams['rootid'],
1576
            $thisnode['rootid'],
1577
            $this->_flparams['level'],
1578
            $thisnode['level'],
1579
            $this->_flparams['norder'],
1580
            $thisnode['norder'],
1581
            $this->_flparams['l'],
1582
            $parent['l'],
1583
            $parent['r']);
1584
 
1585
        } else {
1586
            // A rootnode was deleted and we only have to close the gap inside the order
1587
            $sql[] = sprintf('UPDATE %s SET %s=%s+1 WHERE %s=%s AND %s > %s',
1588
            $this->node_table,
1589
            $this->_flparams['norder'],
1590
            $this->_flparams['norder'],
1591
            $this->_flparams['rootid'],
1592
            $this->_flparams['id'],
1593
            $this->_flparams['norder'],
1594
            $thisnode['norder']);
1595
        }
1596
        for($i=0;$i<count($sql);$i++) {
1597
            $res = $this->db->query($sql[$i]);
1598
            $this->_testFatalAbort($res, __FILE__,  __LINE__);
1599
        }
1600
        $this->_releaseLock();
1601
        return true;
1602
    }
1603
 
1604
    // }}}
1605
    // {{{ updateNode()
1606
 
1607
    /**
1608
    * Changes the payload of a node
1609
    *
1610
    * @param int    $id Node ID
1611
    * @param array  $values Hash with param => value pairs of the node (see $this->params)
1612
    * @param bool  $_intermal Internal use only. Used to skip value validation. Leave this as it is.
1613
    *
1614
    * @access public
1615
    * @return bool True if the update is successful
1616
    */
1617
    function updateNode($id, $values, $_internal=false) {
1618
        if ($this->debug) {
1619
            $this->_debugMessage('updateNode($id, $values)');
1620
        }
1621
 
1622
        if (PEAR::isError($lock = $this->_setLock())) {
1623
            return $lock;
1624
        }
1625
 
1626
        if(!$_internal) {
1627
            $this->_verifyUserValues('createRootNode()', $values);
1628
        }
1629
 
1630
        $eparams = array('values' => $values);
1631
        if (!$this->_skipCallbacks && isset($this->_hasListeners['nodeUpdate'])) {
1632
            // EVENT (NodeUpdate)
1633
            $this->triggerEvent('nodeUpdate', $this->pickNode($id), $eparams);
1634
        }
1635
 
1636
        $addvalues = array();
1637
        if (!$qr = $this->_values2Query($values, $addvalues)) {
1638
            $this->_releaseLock();
1639
            return false;
1640
        }
1641
 
1642
        $sql = sprintf('UPDATE %s SET %s WHERE %s = %s',
1643
        $this->node_table,
1644
        $qr,
1645
        $this->_flparams['id'],
1646
        $id);
1647
        $res = $this->db->query($sql);
1648
        $this->_testFatalAbort($res, __FILE__,  __LINE__);
1649
        $this->_releaseLock();
1650
        return true;
1651
    }
1652
 
1653
 
1654
 
1655
    // }}}
1656
    // +----------------------------------------------+
1657
    // | Moving and copying                           |
1658
    // |----------------------------------------------+
1659
    // | [PUBLIC]                                     |
1660
    // +----------------------------------------------+
1661
    // {{{ moveTree()
1662
 
1663
    /**
1664
    * Wrapper for node moving and copying
1665
    *
1666
    * @param int    $id Source ID
1667
    * @param int    $target Target ID
1668
    * @param constant  $pos Position (use one of the NESE_MOVE_* constants)
1669
    * @param bool   $copy Shall we create a copy
1670
    *
1671
    * @see _moveInsideLevel
1672
    * @see _moveAcross
1673
    * @see _moveRoot2Root
1674
    * @access public
1675
    * @return int ID of the moved node or false on error
1676
    */
1677
    function moveTree($id, $targetid, $pos, $copy = false) {
1678
 
1679
        if ($this->debug) {
1680
            $this->_debugMessage('moveTree($id, $target, $pos, $copy = false)');
1681
        }
1682
        if($id == $targetid && !$copy) {
1683
            // TRIGGER BOGUS MESSAGE
1684
            return false;
1685
        }
1686
 
1687
        // Get information about source and target
1688
        if (!($source = $this->pickNode($id, true))) {
1689
            $epr = array('moveTree()', $id);
1690
            return $this->_raiseError(NESE_ERROR_NOT_FOUND, PEAR_ERROR_TRIGGER, E_USER_ERROR, $epr);
1691
        }
1692
 
1693
        if (!($target = $this->pickNode($targetid, true))) {
1694
            $epr = array('moveTree()', $targetid);
1695
            return $this->_raiseError(NESE_ERROR_NOT_FOUND, PEAR_ERROR_TRIGGER, E_USER_ERROR, $epr);
1696
        }
1697
 
1698
        if (PEAR::isError($lock = $this->_setLock(true))) {
1699
            return $lock;
1700
        }
1701
 
1702
        $this->_relations = array();
1703
        // This operations don't need callbacks except the copy handler
1704
        // which ignores this setting
1705
        $this->_skipCallbacks = true;
1706
 
1707
        if(!$copy) {
1708
            // We have a recursion - let's stop
1709
            if (($target['rootid'] == $source['rootid']) &&
1710
            (($source['l'] <= $target['l']) &&
1711
            ($source['r'] >= $target['r']))) {
1712
                $this->_releaseLock(true);
1713
                $epr = array('moveTree()');
1714
                return $this->_raiseError(NESE_ERROR_RECURSION, PEAR_ERROR_RETURN, E_USER_NOTICE, $epr);
1715
            }
1716
 
1717
            // Insert/move before or after
1718
 
1719
            if (($source['rootid'] == $source['id']) &&
1720
            ($target['rootid'] == $target['id'])) {
1721
                // We have to move a rootnode which is different from moving inside a tree
1722
                $nid = $this->_moveRoot2Root($source, $target, $pos, $copy);
1723
                $this->_releaseLock(true);
1724
                return $nid;
1725
            }
1726
        } elseif(($target['rootid'] == $source['rootid']) &&
1727
        (($source['l'] < $target['l']) &&
1728
        ($source['r'] > $target['r']))) {
1729
            $this->_releaseLock(true);
1730
            $epr = array('moveTree()');
1731
            return $this->_raiseError(NESE_ERROR_RECURSION, PEAR_ERROR_RETURN, E_USER_NOTICE, $epr);
1732
        }
1733
 
1734
 
1735
        // We have to move between different levels and maybe subtrees - let's rock ;)
1736
        $this->_moveAcross($source, $target, $pos);
1737
        $this->_moveCleanup($copy);
1738
        $this->_releaseLock(true);
1739
    }
1740
 
1741
    // }}}
1742
    // {{{ _moveAcross()
1743
 
1744
    /**
1745
    * Moves nodes and trees to other subtrees or levels
1746
    *
1747
    * <pre>
1748
    * [+] <--------------------------------+
1749
    * +-[\] root1 [target]                 |
1750
    *     <-------------------------+      |p
1751
    * +-\ root2                     |      |
1752
    * | |                           |      |
1753
    * | |-- subnode1 [target]       |      |B
1754
    * | |-- subnode2 [new]          |S     |E
1755
    * | |-- subnode3                |U     |F
1756
    * |                             |B     |O
1757
    * +-\ root3                     |      |R
1758
    *   |-- subnode 3.1             |      |E
1759
    *   |-\ subnode 3.2 [source] >--+------+
1760
    *     |-- subnode 3.2.1
1761
    *</pre>
1762
    *
1763
    * @param     object NodeCT $source   Source node
1764
    * @param     object NodeCT $target   Target node
1765
    * @param     string    $pos          Position [SUBnode/BEfore]
1766
    * @param     bool         $copy                Shall we create a copy
1767
    *
1768
    * @access    private
1769
    * @see        moveTree
1770
    * @see        _r_moveAcross
1771
    * @see        _moveCleanup
1772
    */
1773
    function _moveAcross($source, $target, $pos) {
1774
        if ($this->debug) {
1775
            $this->_debugMessage('_moveAcross($source, $target, $pos, $copy = false)');
1776
        }
1777
 
1778
        // Get the current data from a node and exclude the id params which will be changed
1779
        // because of the node move
1780
        $values = array();
1781
        foreach($this->params as $key => $val) {
1782
            if ($source[$val] && !in_array($val, $this->_requiredParams)) {
1783
                $values[$key] = trim($source[$val]);
1784
            }
1785
        }
1786
 
1787
        switch($pos) {
1788
 
1789
            case NESE_MOVE_BEFORE:
1790
            $clone_id = $this->createLeftNode($target['id'], $values);
1791
            break;
1792
 
1793
            case NESE_MOVE_AFTER:
1794
            $clone_id = $this->createRightNode($target['id'], $values);
1795
            break;
1796
 
1797
            case NESE_MOVE_BELOW:
1798
            $clone_id = $this->createSubNode($target['id'], $values);
1799
            break;
1800
        }
1801
 
1802
 
1803
        $children = $this->getChildren($source['id'], true, true, true);
1804
 
1805
 
1806
        if ($children) {
1807
            $pos = NESE_MOVE_BELOW;
1808
            $sclone_id = $clone_id;
1809
            // Recurse through the child nodes
1810
            foreach($children AS $cid => $child) {
1811
                $sclone = $this->pickNode($sclone_id, true);
1812
                $sclone_id = $this->_moveAcross($child, $sclone, $pos);
1813
 
1814
                $pos = NESE_MOVE_AFTER;
1815
            }
1816
        }
1817
 
1818
        $this->_relations[$source['id']] = $clone_id;
1819
        return $clone_id;
1820
    }
1821
 
1822
    // }}}
1823
    // {{{ _moveCleanup()
1824
 
1825
    /**
1826
    * Deletes the old subtree (node) and writes the node id's into the cloned tree
1827
    *
1828
    *
1829
    * @param     array    $relations        Hash in der Form $h[alteid]=neueid
1830
    * @param     array    $copy                     Are we in copy mode?
1831
    * @access    private
1832
    */
1833
    function _moveCleanup($copy = false) {
1834
 
1835
        $relations = $this->_relations;
1836
        if ($this->debug) {
1837
            $this->_debugMessage('_moveCleanup($relations, $copy = false)');
1838
        }
1839
 
1840
        $deletes = array();
1841
        $updates = array();
1842
        $tb = $this->node_table;
1843
        $fid = $this->_flparams['id'];
1844
        $froot = $this->_flparams['rootid'];
1845
        foreach($relations AS $key => $val) {
1846
            $clone = $this->pickNode($val);
1847
            if ($copy) {
1848
                // EVENT (NodeCopy)
1849
 
1850
                $eparams = array('clone' => $clone);
1851
 
1852
                if (!$this->_skipCallbacks && isset($this->_hasListeners['nodeCopy'])) {
1853
                    $this->triggerEvent('nodeCopy', $this->pickNode($key), $eparams);
1854
                }
1855
                continue;
1856
            }
1857
 
1858
            // No callbacks here because the node itself doesn't get changed
1859
            // Only it's position
1860
            // If one needs a callback here please let me know
1861
 
1862
            $deletes[] = $key;
1863
            // It's isn't a rootnode
1864
            if ($clone->id != $clone->rootid) {
1865
 
1866
 
1867
                $sql = sprintf('UPDATE %s SET %s=%s WHERE %s = %s',
1868
                $this->node_table,
1869
                $fid,
1870
                $key,
1871
                $fid,
1872
                $val);
1873
                $updates[] = $sql;
1874
            } else {
1875
                $sql = sprintf('UPDATE %s SET %s=%s, %s=%s WHERE %s=%s',
1876
                            $tb,
1877
                            $fid,
1878
                            $key,
1879
                            $froot,
1880
                            $val,
1881
                            $fid,
1882
                            $val);
1883
 
1884
                $updates[] = $sql;
1885
                $orootid = $clone->rootid;
1886
 
1887
                $sql = sprintf('UPDATE %s SET %s=%s WHERE %s=%s',
1888
                            $tb,
1889
                            $froot,
1890
                            $key,
1891
                            $froot,
1892
                            $orootid);
1893
                $updates[] = $sql;
1894
            }
1895
            $this->_skipCallbacks = false;
1896
        }
1897
 
1898
        if(!empty($deletes)) {
1899
            for($i=0;$i<count($deletes);$i++) {
1900
                $this->deleteNode($deletes[$i]);
1901
            }
1902
        }
1903
 
1904
        if(!empty($updates)) {
1905
            for($i=0;$i<count($updates);$i++) {
1906
                $res = $this->db->query($updates[$i]);
1907
                $this->_testFatalAbort($res, __FILE__, __LINE__);
1908
            }
1909
        }
1910
 
1911
        return true;
1912
    }
1913
 
1914
    // }}}
1915
    // {{{ _moveRoot2Root()
1916
 
1917
    /**
1918
    * Moves rootnodes
1919
    *
1920
    * <pre>
1921
    * +-- root1
1922
    * |
1923
    * +-\ root2
1924
    * | |
1925
    * | |-- subnode1 [target]
1926
    * | |-- subnode2 [new]
1927
    * | |-- subnode3
1928
    * |
1929
    * +-\ root3
1930
    *  [|]  <-----------------------+
1931
    *   |-- subnode 3.1 [target]    |
1932
    *   |-\ subnode 3.2 [source] >--+
1933
    *     |-- subnode 3.2.1
1934
    * </pre>
1935
    *
1936
    * @param     object NodeCT $source    Source
1937
    * @param     object NodeCT $target    Target
1938
    * @param     string $pos              BEfore | AFter
1939
    * @access    private
1940
    * @see        moveTree
1941
    */
1942
    function _moveRoot2Root($source, $target, $pos) {
1943
 
1944
        if ($this->debug) {
1945
            $this->_debugMessage('_moveRoot2Root($source, $target, $pos, $copy)');
1946
        }
1947
        if(PEAR::isError($lock=$this->_setLock())) {
1948
            return $lock;
1949
        }
1950
 
1951
        $tb = $this->node_table;
1952
        $fid = $this->_flparams['id'];
1953
        $froot = $this->_flparams['rootid'];
1954
        $freh = $this->_flparams['norder'];
1955
        $s_order = $source['norder'];
1956
        $t_order = $target['norder'];
1957
        $s_id = $source['id'];
1958
        $t_id = $target['id'];
1959
 
1960
 
1961
        if ($s_order < $t_order) {
1962
            if ($pos == NESE_MOVE_BEFORE) {
1963
                $sql = "UPDATE $tb SET $freh=$freh-1
1964
                        WHERE $freh BETWEEN $s_order AND $t_order AND
1965
                            $fid!=$t_id AND
1966
                            $fid!=$s_id AND
1967
                            $froot=$fid";
1968
                $res = $this->db->query($sql);
1969
                $this->_testFatalAbort($res, __FILE__, __LINE__);
1970
                $sql = "UPDATE $tb SET $freh=$t_order -1 WHERE $fid=$s_id";
1971
                $res = $this->db->query($sql);
1972
                $this->_testFatalAbort($res, __FILE__, __LINE__);
1973
            }
1974
            elseif($pos == NESE_MOVE_AFTER) {
1975
 
1976
                $sql = "UPDATE $tb SET $freh=$freh-1
1977
                        WHERE $freh BETWEEN $s_order AND $t_order AND
1978
                            $fid!=$s_id AND
1979
                            $froot=$fid";
1980
                $res = $this->db->query($sql);
1981
                $this->_testFatalAbort($res, __FILE__, __LINE__);
1982
 
1983
                $sql = "UPDATE $tb SET $freh=$t_order WHERE $fid=$s_id";
1984
                $res = $this->db->query($sql);
1985
                $this->_testFatalAbort($res, __FILE__, __LINE__);
1986
            }
1987
        }
1988
 
1989
        if ($s_order > $t_order) {
1990
            if ($pos == NESE_MOVE_BEFORE) {
1991
                $sql = "UPDATE $tb SET $freh=$freh+1
1992
                        WHERE $freh BETWEEN $t_order AND $s_order AND
1993
                            $fid != $s_id AND
1994
                            $froot=$fid";
1995
                $res = $this->db->query($sql);
1996
                $this->_testFatalAbort($res, __FILE__, __LINE__);
1997
 
1998
                $sql = "UPDATE $tb SET $freh=$t_order WHERE $fid=$s_id";
1999
                $res = $this->db->query($sql);
2000
                $this->_testFatalAbort($res, __FILE__, __LINE__);
2001
            }
2002
            elseif ($pos == NESE_MOVE_AFTER) {
2003
                $sql = "UPDATE $tb SET $freh=$freh+1
2004
                        WHERE $freh BETWEEN $t_order AND $s_order AND
2005
                        $fid!=$t_id AND
2006
                        $fid!=$s_id AND
2007
                        $froot=$fid";
2008
                $res = $this->db->query($sql);
2009
                $this->_testFatalAbort($res, __FILE__, __LINE__);
2010
 
2011
                $sql = "UPDATE $tb SET $freh=$t_order+1 WHERE $fid = $s_id";
2012
                $res = $this->db->query($sql);
2013
                $this->_testFatalAbort($res, __FILE__, __LINE__);
2014
            }
2015
        }
2016
        $this->_releaseLock();
2017
        return $source->id;
2018
    }
2019
 
2020
    // }}}
2021
    // +-----------------------+
2022
    // | Helper methods        |
2023
    // +-----------------------+
2024
 
2025
    // }}}
2026
    // {{{ _secSort()
2027
 
2028
    /**
2029
    * Callback for uasort used to sort siblings
2030
    *
2031
    * @access private
2032
    */
2033
    function _secSort($node1, $node2) {
2034
        // Within the same level?
2035
        if($node1['level'] != $node2['level']) {
2036
            return strnatcmp($node1['l'], $node2['l']);
2037
        }
2038
 
2039
        // Are they siblings?
2040
        $p1 = $this->getParent($node1);
2041
        $p2 = $this->getParent($node2);
2042
        if($p1['id'] != $p2['id']) {
2043
            return strnatcmp($node1['l'], $node2['l']);
2044
        }
2045
 
2046
        // Same field value? Use the lft value then
2047
        $field = $this->params[$this->secondarySort];
2048
        if($node1[$field] == $node2[$field]) {
2049
            return strnatcmp($node1['l'], $node2[l]);
2050
        }
2051
 
2052
        // Compare between siblings with different field value
2053
        return strnatcmp($node1[$field], $node2[$field]);
2054
    }
2055
 
2056
    // }}}
2057
    // {{{ _addSQL()
2058
 
2059
    /**
2060
    * Adds a specific type of SQL to a query string
2061
    *
2062
    * @param array $addSQL The array of SQL strings to add.  Example value:
2063
    *               $addSQL = array(
2064
    *               'cols' => 'tb2.col2, tb2.col3',         // Additional tables/columns
2065
    *               'join' => 'LEFT JOIN tb1 USING(STRID)', // Join statement
2066
    *               'append' => 'GROUP by tb1.STRID');      // Group condition
2067
    * @param string $type The type of SQL.  Can be 'cols', 'join', or 'append'.
2068
    *
2069
    * @access private
2070
    * @return string The SQL, properly formatted
2071
    */
2072
    function _addSQL($addSQL, $type) {
2073
        if (!isset($addSQL[$type])) {
2074
            return '';
2075
        }
2076
 
2077
        switch($type) {
2078
            case 'cols':
2079
            return ', ' . $addSQL[$type];
2080
            default:
2081
            return $addSQL[$type];
2082
        }
2083
    }
2084
 
2085
    // }}}
2086
    // {{{ _getSelectFields()
2087
 
2088
    /**
2089
    * Gets the select fields based on the params
2090
    *
2091
    * @param bool $aliasFields Should we alias the fields so they are the names of the
2092
    *             parameter keys, or leave them as is?
2093
    *
2094
    * @access private
2095
    * @return string A string of query fields to select
2096
    */
2097
    function _getSelectFields($aliasFields) {
2098
        $queryFields = array();
2099
        foreach ($this->params as $key => $val) {
2100
            $tmp_field = $this->node_table . '.' . $key;
2101
            if ($aliasFields) {
2102
                $tmp_field .= ' AS ' . $val;
2103
            }
2104
            $queryFields[] = $tmp_field;
2105
        }
2106
 
2107
        $fields = implode(', ', $queryFields);
2108
        return $fields;
2109
    }
2110
 
2111
    // }}}
2112
    // {{{ _processResultSet()
2113
 
2114
    /**
2115
    * Processes a DB result set by checking for a DB error and then transforming the result
2116
    * into a set of DB_NestedSet_Node objects or leaving it as an array.
2117
    *
2118
    * @param string $sql The sql query to be done
2119
    * @param bool $keepAsArray Keep the result as an array or transform it into a set of
2120
    *             DB_NestedSet_Node objects?
2121
    * @param bool $fieldsAreAliased Are the fields aliased?
2122
    *
2123
    * @access    private
2124
    * @return mixed False on error or the transformed node set.
2125
    */
2126
    function _processResultSet($sql, $keepAsArray, $fieldsAreAliased) {
2127
        $result = $this->db->getAll($sql);
2128
        if ($this->_testFatalAbort($result, __FILE__, __LINE__)) {
2129
            return false;
2130
        }
2131
 
2132
        $nodes = array();
2133
        $idKey = $fieldsAreAliased ? 'id' : $this->_flparams['id'];
2134
        foreach ($result as $row) {
2135
            $node_id = $row[$idKey];
2136
            if ($keepAsArray) {
2137
                $nodes[$node_id] = $row;
2138
            } else {
2139
                // Create an instance of the node container
2140
                $nodes[$node_id] =& new DB_NestedSet_Node($row);
2141
            }
2142
 
2143
        }
2144
        return $nodes;
2145
    }
2146
 
2147
    // }}}
2148
    // {{{ _testFatalAbort()
2149
 
2150
    /**
2151
    * Error Handler
2152
    *
2153
    * Tests if a given ressource is a PEAR error object
2154
    * ans raises a fatal error in case of an error object
2155
    *
2156
    * @param        object  PEAR::Error $errobj     The object to test
2157
    * @param        string  $file   The filename wher the error occured
2158
    * @param        int     $line   The line number of the error
2159
    * @return   void
2160
    * @access private
2161
    */
2162
    function _testFatalAbort($errobj, $file, $line) {
2163
        if (!$this->_isDBError($errobj)) {
2164
            return false;
2165
        }
2166
 
2167
        if ($this->debug) {
2168
            $this->_debugMessage('_testFatalAbort($errobj, $file, $line)');
2169
        }
2170
        if ($this->debug) {
2171
            $message = $errobj->getUserInfo();
2172
            $code = $errobj->getCode();
2173
            $msg = "$message ($code) in file $file at line $line";
2174
        } else {
2175
            $msg = $errobj->getMessage();
2176
            $code = $errobj->getCode();     }
2177
 
2178
            PEAR::raiseError($msg, $code, PEAR_ERROR_TRIGGER, E_USER_ERROR);
2179
    }
2180
 
2181
    // {{{ __raiseError()
2182
 
2183
    /**
2184
    * @access private
2185
    */
2186
    function _raiseError($code, $mode, $option, $epr=array()) {
2187
        $message = vsprintf($this->_getMessage($code), $epr);
2188
        return PEAR::raiseError($message, $code, $mode, $option);
2189
    }
2190
 
2191
    // }}}
2192
 
2193
    // {{{ addListener()
2194
 
2195
    /**
2196
    * Add an event listener
2197
    *
2198
    * Adds an event listener and returns an ID for it
2199
    *
2200
    * @param        string $event           The ivent name
2201
    * @param        string  $listener       The listener object
2202
    * @return   string
2203
    * @access public
2204
    */
2205
    function addListener($event, &$listener) {
2206
        $listenerID = uniqid('el');
2207
        $this->eventListeners[$event][$listenerID] =& $listener;
2208
        $this->_hasListeners[$event] = true;
2209
        return $listenerID;
2210
    }
2211
 
2212
    // }}}
2213
    // {{{ removeListener()
2214
 
2215
    /**
2216
    * Removes an event listener
2217
    *
2218
    * Removes the event listener with the given ID
2219
    *
2220
    * @param        string $event           The ivent name
2221
    * @param        string  $listenerID     The listener's ID
2222
    * @return   bool
2223
    * @access public
2224
    */
2225
    function removeListener($event, $listenerID) {
2226
        unset($this->eventListeners[$event][$listenerID]);
2227
        if (!isset($this->eventListeners[$event]) ||
2228
        !is_array($this->eventListeners[$event]) ||
2229
        count($this->eventListeners[$event]) == 0) {
2230
            unset($this->_hasListeners[$event]);
2231
        }
2232
        return true;
2233
    }
2234
 
2235
    // }}}
2236
    // {{{ triggerEvent()
2237
 
2238
    /**
2239
    * Triggers and event an calls the event listeners
2240
    *
2241
    * @param        string $event   The Event that occured
2242
    * @param        object node $node A Reference to the node object which was subject to changes
2243
    * @param        array $eparams  A associative array of params which may be needed by the handler
2244
    * @return   bool
2245
    * @access public
2246
    */
2247
    function triggerEvent($event, &$node, $eparams = false) {
2248
        if ($this->_skipCallbacks || !isset($this->_hasListeners[$event])) {
2249
            return false;
2250
        }
2251
 
2252
        foreach($this->eventListeners[$event] as $key => $val) {
2253
            if (!method_exists($val, 'callEvent')) {
2254
                return new PEAR_Error($this->_getMessage(NESE_ERROR_NOHANDLER), NESE_ERROR_NOHANDLER);
2255
            }
2256
 
2257
            $val->callEvent($event, $node, $eparams);
2258
        }
2259
 
2260
        return true;
2261
    }
2262
 
2263
    // }}}
2264
    // {{{ apiVersion()
2265
 
2266
    function apiVersion() {
2267
        return array(
2268
        'package:'=>$this->_packagename,
2269
        'majorversion'=>$this->_majorversion,
2270
        'minorversion'=>$this->_minorversion,
2271
        'version'=>sprintf('%s.%s',$this->_majorversion, $this->_minorversion),
2272
        'revision'=>str_replace('$', '',"$Revision: 1.1 $")
2273
        );
2274
    }
2275
 
2276
    // }}}
2277
    // {{{ setAttr()
2278
 
2279
    /**
2280
    * Sets an object attribute
2281
    *
2282
    * @param        array $attr     An associative array with attributes
2283
    *
2284
    * @return   bool
2285
    * @access public
2286
    */
2287
    function setAttr($attr) {
2288
        static $hasSetSequence;
2289
        if (!isset($hasSetSequence)) {
2290
            $hasSetSequence = false;
2291
        }
2292
 
2293
        if (!is_array($attr) || count($attr) == 0) {
2294
            return false;
2295
        }
2296
 
2297
        foreach ($attr as $key => $val) {
2298
            $this->$key = $val;
2299
            if ($key == 'sequence_table') {
2300
                $hasSetSequence = true;
2301
            }
2302
 
2303
            // only update sequence to reflect new table if they haven't set it manually
2304
            if (!$hasSetSequence && $key == 'node_table') {
2305
                $this->sequence_table = $this->node_table . '_' . $this->_flparams['id'];
2306
            }
2307
            if($key == 'cache' && is_object($val)) {
2308
                $this->_caching = true;
2309
                $GLOBALS['DB_NestedSet'] = & $this;
2310
            }
2311
        }
2312
 
2313
        return true;
2314
    }
2315
 
2316
    // }}}
2317
    // {{{ setsortMode()
2318
    /**
2319
    * This enables you to set specific options for each output method
2320
    *
2321
    * @param constant $sortMode
2322
    *
2323
    * @access public
2324
    * @return Current sortMode
2325
    */
2326
    function setsortMode($sortMode=false) {
2327
        if($sortMode && in_array($sortMode, $this->_sortModes)) {
2328
            $this->_sortMode = $sortMode;
2329
        } else {
2330
            return $this->_sortMode;
2331
        }
2332
        return $this->_sortMode;
2333
    }
2334
    // }}}
2335
    // {{{ setDbOption()
2336
 
2337
    /**
2338
    * Sets a db option.  Example, setting the sequence table format
2339
    *
2340
    * @var string $option The option to set
2341
    * @var string $val The value of the option
2342
    *
2343
    * @access public
2344
    * @return void
2345
    */
2346
    function setDbOption($option, $val) {
2347
        $this->db->setOption($option, $val);
2348
    }
2349
 
2350
    // }}}
2351
    // {{{ testLock()
2352
 
2353
    /**
2354
    * Tests if a database lock is set
2355
    *
2356
    * @access public
2357
    */
2358
    function testLock() {
2359
        if ($this->debug) {
2360
            $this->_debugMessage('testLock()');
2361
        }
2362
 
2363
        if($lockID = $this->_structureTableLock) {
2364
            return $lockID;
2365
        }
2366
        $this->_lockGC();
2367
        $sql = sprintf('SELECT lockID FROM %s WHERE lockTable=%s',
2368
                    $this->lock_table,
2369
                    $this->_quote($this->node_table)) ;
2370
 
2371
        $res = $this->db->query($sql);
2372
        $this->_testFatalAbort($res, __FILE__, __LINE__);
2373
 
2374
        if ($this->_numRows($res)) {
2375
            return new PEAR_Error($this->_getMessage(NESE_ERROR_TBLOCKED),NESE_ERROR_TBLOCKED);
2376
        }
2377
 
2378
        return false;
2379
    }
2380
 
2381
    // }}}
2382
    // {{{ _setLock()
2383
 
2384
    /**
2385
    * @access private
2386
    */
2387
    function _setLock($exclusive=false) {
2388
        $lock = $this->testLock();
2389
        if(PEAR::isError($lock)) {
2390
            return $lock;
2391
        }
2392
 
2393
        if ($this->debug) {
2394
            $this->_debugMessage('_setLock()');
2395
        }
2396
        if($this->_caching) {
2397
            @$this->cache->flush('function_cache');
2398
            $this->_caching = false;
2399
            $this->_restcache = true;
2400
        }
2401
 
2402
        if (!$lockID = $this->_structureTableLock) {
2403
            $lockID = $this->_structureTableLock = uniqid('lck-');
2404
 
2405
            $sql = sprintf('INSERT INTO %s SET lockID=%s, lockTable=%s, lockStamp=%s',
2406
            $this->lock_table,
2407
            $this->_quote($lockID),
2408
            $this->_quote($this->node_table),
2409
            time());
2410
 
2411
        } else {
2412
            $sql = sprintf('UPDATE %s set lockStamp=%s WHERE lockID=%s AND lockTable=%s',
2413
                        $this->lock_table,
2414
                        time(),
2415
                        $this->_quote($lockID),
2416
                        $this->_quote($this->node_table));
2417
        }
2418
        if($exclusive) {
2419
            $this->_lockExclusive = true;
2420
        }
2421
 
2422
        $res = $this->db->query($sql);
2423
        $this->_testFatalAbort($res, __FILE__, __LINE__);
2424
        return $lockID;
2425
    }
2426
 
2427
    // }}}
2428
    // {{{ _releaseLock()
2429
 
2430
    /**
2431
    * @access private
2432
    */
2433
    function _releaseLock($exclusive=false) {
2434
        if ($this->debug) {
2435
            $this->_debugMessage('_releaseLock()');
2436
        }
2437
 
2438
        if($exclusive) {
2439
            $this->_lockExclusive = false;
2440
        }
2441
 
2442
        if ((!$lockID = $this->_structureTableLock) || $this->_lockExclusive) {
2443
            return false;
2444
        }
2445
 
2446
        $tb = $this->lock_table;
2447
        $stb = $this->node_table;
2448
        $sql = "DELETE FROM $tb
2449
                WHERE lockTable=" . $this->_quote($stb) . " AND
2450
                    lockID=" . $this->_quote($lockID);
2451
 
2452
        $res = $this->db->query($sql);
2453
        $this->_testFatalAbort($res, __FILE__, __LINE__);
2454
        $this->_structureTableLock = false;
2455
        if($this->_restcache) {
2456
            $this->_caching = true;
2457
            $this->_restcache = false;
2458
        }
2459
        return true;
2460
    }
2461
 
2462
    // }}}
2463
    // {{{ _lockGC()
2464
 
2465
    /**
2466
    * @access private
2467
    */
2468
    function _lockGC() {
2469
        if ($this->debug) {
2470
            $this->_debugMessage('_lockGC()');
2471
        }
2472
        $tb = $this->lock_table;
2473
        $stb = $this->node_table;
2474
        $lockTTL = time() - $this->lockTTL;
2475
        $sql = "DELETE FROM $tb
2476
                WHERE lockTable=" . $this->_quote($stb) . " AND
2477
                    lockStamp < $lockTTL";
2478
 
2479
        $res = $this->db->query($sql);
2480
        $this->_testFatalAbort($res, __FILE__, __LINE__);
2481
    }
2482
 
2483
    // }}}
2484
    // {{{ _values2Query()
2485
 
2486
    /**
2487
    * @access private
2488
    */
2489
    function _values2Query($values, $addval = false) {
2490
 
2491
        if ($this->debug) {
2492
            $this->_debugMessage('_values2Query($values, $addval = false)');
2493
        }
2494
        if (is_array($addval)) {
2495
            $values = $values + $addval;
2496
        }
2497
 
2498
        $arq = array();
2499
        foreach($values AS $key => $val) {
2500
            $k = trim($key);
2501
            $v = trim($val);
2502
            if ($k) {
2503
                // To be used with the next mahor version
2504
                // $iv = in_array($this->params[$k], $this->_quotedParams) ? $this->_quote($v) : $v;
2505
                $iv = $this->_quote($v);
2506
                $arq[] = "$k=$iv";
2507
            }
2508
        }
2509
 
2510
        if (!is_array($arq) || count($arq) == 0) {
2511
            return false;
2512
        }
2513
 
2514
        $query = implode(', ', $arq);
2515
        return $query;
2516
    }
2517
 
2518
    // }}}
2519
    // {{{ _verifyUserValues()
2520
 
2521
    /**
2522
    * Clean values from protected or unknown columns
2523
    *
2524
    * @var string $caller The calling method
2525
    * @var string $values The values array
2526
    *
2527
    * @access private
2528
    * @return void
2529
    */
2530
    function _verifyUserValues($caller, &$values) {
2531
 
2532
        if($this->_dumbmode) {
2533
            return true;
2534
        }
2535
        foreach($values AS $field=>$value) {
2536
            if(!isset($this->params[$field])) {
2537
                $epr = array(
2538
                $caller,
2539
                sprintf('Unknown column/param \'%s\'', $field));
2540
                $this->_raiseError(NESE_ERROR_WRONG_MPARAM, PEAR_ERROR_RETURN, E_USER_NOTICE, $epr);
2541
                unset($values[$field]);
2542
            } else {
2543
                $flip = $this->params[$field];
2544
                if(in_array($flip, $this->_requiredParams)) {
2545
                    $epr = array(
2546
                    $caller,
2547
                    sprintf('\'%s\' is autogenerated and can\'t be passed - it will be ignored', $field));
2548
                    $this->_raiseError(NESE_ERROR_WRONG_MPARAM, PEAR_ERROR_RETURN, E_USER_NOTICE, $epr);
2549
                    unset($values[$field]);
2550
                }
2551
            }
2552
        }
2553
    }
2554
 
2555
    // }}}
2556
    // {{{ _debugMessage()
2557
 
2558
    /**
2559
    * @access private
2560
    */
2561
    function _debugMessage($msg) {
2562
        if ($this->debug) {
2563
            $time = $this->_getmicrotime();
2564
            echo "$time::Debug:: $msg<br />\n";
2565
        }
2566
    }
2567
 
2568
    // }}}
2569
    // {{{ _getMessage()
2570
 
2571
    /**
2572
    * @access private
2573
    */
2574
    function _getMessage($code) {
2575
        if ($this->debug) {
2576
            $this->_debugMessage('_getMessage($code)');
2577
        }
2578
        return isset($this->messages[$code]) ? $this->messages[$code] : $this->messages[NESE_MESSAGE_UNKNOWN];
2579
 
2580
    }
2581
 
2582
    // }}}
2583
    // {{{ _getmicrotime()
2584
 
2585
    /**
2586
    * @access private
2587
    */
2588
    function _getmicrotime() {
2589
        list($usec, $sec) = explode(' ', microtime());
2590
        return ((float)$usec + (float)$sec);
2591
    }
2592
 
2593
    // }}}
2594
    // {{{ convertTreeModel()
2595
 
2596
    /**
2597
    * Convert a <1.3 tree into a 1.3 tree format
2598
    *
2599
    * This will convert the tree into a format needed for some new features in
2600
    * 1.3. Your <1.3 tree will still work without converting but some new features
2601
    * like preorder sorting won't work as expected.
2602
    *
2603
    * <pre>
2604
    * Usage:
2605
    * - Create a new node table (tb_nodes2) from the current node table (tb_nodes1) (only copy the structure).
2606
    * - Create a nested set instance of the 'old' set (NeSe1) and one of the new set (NeSe2)
2607
    * - Now you have 2 identical objects where only node_table differs
2608
    * - Call DB_NestedSet::convertTreeModel(&$orig, &$copy);
2609
    * - After that you have a cleaned up copy of tb_nodes1 inside tb_nodes2
2610
    * </pre>
2611
    *
2612
    * @param object DB_NestedSet $orig  Nested set we want to copy
2613
    * @param object DB_NestedSet $copy  Object where the new tree is copied to
2614
    * @param integer $_parent           ID of the parent node (private)
2615
    *
2616
    * @static
2617
    * @access public
2618
    * @return bool True uns success
2619
    */
2620
    function convertTreeModel(&$orig, &$copy, $_parent=false) {
2621
 
2622
        static $firstSet;
2623
 
2624
        $isRoot = false;
2625
        if(!$_parent) {
2626
            if(!is_object($orig) || !is_object($copy)) {
2627
                return false;
2628
            }
2629
            if($orig->node_table == $copy->node_table) {
2630
             return false;
2631
            }
2632
            $copy->_dumbmode = true;
2633
            $orig->sortMode = NESE_SORT_LEVEL;
2634
            $copy->sortMode = NESE_SORT_LEVEL;
2635
            $sibl = $orig->getRootNodes(true);
2636
            $isRoot = true;
2637
        } else {
2638
            $sibl = $orig->getChildren($_parent, true);
2639
        }
2640
 
2641
        if(empty($sibl)) {
2642
            return false;
2643
        }
2644
 
2645
        foreach($sibl AS $sid=>$sibling) {
2646
            unset($sibling['l']);
2647
            unset($sibling['r']);
2648
            unset($sibling['norder']);
2649
 
2650
            $values = array();
2651
            foreach($sibling AS $key=>$val) {
2652
                if(!isset($copy->_flparams[$key])) {
2653
                    continue;
2654
                }
2655
                $values[$copy->_flparams[$key]] = $val;
2656
            }
2657
 
2658
            if(!$firstSet) {
2659
                $psid = $copy->createRootNode($values, false, true);
2660
                $firstSet = true;
2661
            } elseif($isRoot) {
2662
                $psid = $copy->createRightNode($psid, $values);
2663
            } else {
2664
                $copy->createSubNode($_parent, $values);
2665
            }
2666
 
2667
            DB_NestedSet::convertTreeModel($orig, $copy, $sid);
2668
        }
2669
        return true;
2670
    }
2671
    // }}}
2672
    // {{{ _numRows()
2673
    /**
2674
    * Fetches the number of rows the last query returned
2675
    * @access private
2676
    * @abstract
2677
    */
2678
    function _numRows($res) {
2679
    }
2680
    // }}}
2681
    // {{{ _isDBError()
2682
    /**
2683
    * Returns true if a db return value is an error object
2684
    * @access private
2685
    * @abstract
2686
    */
2687
    function _isDBError($err) {
2688
    }
2689
    // }}}
2690
    // {{{ quote()
2691
    /**
2692
    * Quotes a string to use it inside queries
2693
    * @access private
2694
    * @abstract
2695
    */
2696
    function _quote($str) {
2697
    }
2698
 
2699
}
2700
 
2701
// {{{ DB_NestedSet_Node:: class
2702
 
2703
/**
2704
* Generic class for node objects
2705
*
2706
* @autor Daniel Khan <dk@webcluster.at>;
2707
* @version $Revision: 1.1 $
2708
* @package DB_NestedSet
2709
*
2710
* @access private
2711
*/
2712
 
2713
class DB_NestedSet_Node {
2714
    // {{{ constructor
2715
 
2716
    /**
2717
    * Constructor
2718
    */
2719
    function DB_NestedSet_Node($data) {
2720
        if (!is_array($data) || count($data) == 0) {
2721
            return new PEAR_ERROR($data, NESE_ERROR_PARAM_MISSING);
2722
        }
2723
 
2724
        $this->setAttr($data);
2725
        return true;
2726
    }
2727
 
2728
    // }}}
2729
    // {{{ setAttr()
2730
 
2731
    function setAttr($data) {
2732
        if(!is_array($data) || count($data) == 0) {
2733
            return false;
2734
        }
2735
 
2736
        foreach ($data as $key => $val) {
2737
            $this->$key = $val;
2738
        }
2739
    }
2740
 
2741
    // }}}
2742
 
2743
}
2744
// }}}
2745
 
2746
 
2747
?>