Subversion-Projekte lars-tiefland.php_share

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 1
<?php
2
/*
3
 *    $Id: NestedSet.php 7490 2010-03-29 19:53:27Z jwage $
4
 *
5
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
6
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
7
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
8
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
9
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
10
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
11
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
12
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
13
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
14
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
15
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
16
 *
17
 * This software consists of voluntary contributions made by many individuals
18
 * and is licensed under the LGPL. For more information, see
19
 * <http://www.doctrine-project.org>.
20
 */
21
 
22
/**
23
 * Doctrine_Node_NestedSet
24
 *
25
 * @package    Doctrine
26
 * @subpackage Node
27
 * @license    http://www.opensource.org/licenses/lgpl-license.php LGPL
28
 * @link       www.doctrine-project.org
29
 * @since      1.0
30
 * @version    $Revision: 7490 $
31
 * @author     Joe Simms <joe.simms@websites4.com>
32
 * @author     Roman Borschel <roman@code-factory.org>
33
 */
34
class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Interface
35
{
36
    /**
37
     * test if node has previous sibling
38
     *
39
     * @return bool
40
     */
41
    public function hasPrevSibling()
42
    {
43
        return $this->isValidNode($this->getPrevSibling());
44
    }
45
 
46
    /**
47
     * test if node has next sibling
48
     *
49
     * @return bool
50
     */
51
    public function hasNextSibling()
52
    {
53
        return $this->isValidNode($this->getNextSibling());
54
    }
55
 
56
    /**
57
     * test if node has children
58
     *
59
     * @return bool
60
     */
61
    public function hasChildren()
62
    {
63
        return (($this->getRightValue() - $this->getLeftValue()) > 1);
64
    }
65
 
66
    /**
67
     * test if node has parent
68
     *
69
     * @return bool
70
     */
71
    public function hasParent()
72
    {
73
        return $this->isValidNode($this->getRecord()) && ! $this->isRoot();
74
    }
75
 
76
    /**
77
     * gets record of prev sibling or empty record
78
     *
79
     * @return Doctrine_Record
80
     */
81
    public function getPrevSibling()
82
    {
83
        $baseAlias = $this->_tree->getBaseAlias();
84
        $q = $this->_tree->getBaseQuery();
85
        $q = $q->addWhere("$baseAlias.rgt = ?", $this->getLeftValue() - 1);
86
        $q = $this->_tree->returnQueryWithRootId($q, $this->getRootValue());
87
        $result = $q->execute();
88
 
89
        if (count($result) <= 0) {
90
            return false;
91
        }
92
 
93
        if ($result instanceof Doctrine_Collection) {
94
            $sibling = $result->getFirst();
95
        } else if (is_array($result)) {
96
            $sibling = array_shift($result);
97
        }
98
 
99
        return $sibling;
100
    }
101
 
102
    /**
103
     * gets record of next sibling or empty record
104
     *
105
     * @return Doctrine_Record
106
     */
107
    public function getNextSibling()
108
    {
109
        $baseAlias = $this->_tree->getBaseAlias();
110
        $q = $this->_tree->getBaseQuery();
111
        $q = $q->addWhere("$baseAlias.lft = ?", $this->getRightValue() + 1);
112
        $q = $this->_tree->returnQueryWithRootId($q, $this->getRootValue());
113
        $result = $q->execute();
114
 
115
        if (count($result) <= 0) {
116
            return false;
117
        }
118
 
119
        if ($result instanceof Doctrine_Collection) {
120
            $sibling = $result->getFirst();
121
        } else if (is_array($result)) {
122
            $sibling = array_shift($result);
123
        }
124
 
125
        return $sibling;
126
    }
127
 
128
    /**
129
     * gets siblings for node
130
     *
131
     * @return array     array of sibling Doctrine_Record objects
132
     */
133
    public function getSiblings($includeNode = false)
134
    {
135
        $parent = $this->getParent();
136
        $siblings = array();
137
        if ($parent && $parent->exists()) {
138
            foreach ($parent->getNode()->getChildren() as $child) {
139
                if ($this->isEqualTo($child) && !$includeNode) {
140
                    continue;
141
                }
142
                $siblings[] = $child;
143
            }
144
        }
145
        return $siblings;
146
    }
147
 
148
    /**
149
     * gets record of first child or empty record
150
     *
151
     * @return Doctrine_Record
152
     */
153
    public function getFirstChild()
154
    {
155
        $baseAlias = $this->_tree->getBaseAlias();
156
        $q = $this->_tree->getBaseQuery();
157
        $q->addWhere("$baseAlias.lft = ?", $this->getLeftValue() + 1);
158
        $this->_tree->returnQueryWithRootId($q, $this->getRootValue());
159
        $result = $q->execute();
160
 
161
        if (count($result) <= 0) {
162
            return false;
163
        }
164
 
165
        if ($result instanceof Doctrine_Collection) {
166
            $child = $result->getFirst();
167
        } else if (is_array($result)) {
168
            $child = array_shift($result);
169
        }
170
 
171
        return $child;
172
    }
173
 
174
    /**
175
     * gets record of last child or empty record
176
     *
177
     * @return Doctrine_Record
178
     */
179
    public function getLastChild()
180
    {
181
        $baseAlias = $this->_tree->getBaseAlias();
182
        $q = $this->_tree->getBaseQuery();
183
        $q->addWhere("$baseAlias.rgt = ?", $this->getRightValue() - 1);
184
        $this->_tree->returnQueryWithRootId($q, $this->getRootValue());
185
        $result = $q->execute();
186
 
187
        if (count($result) <= 0) {
188
            return false;
189
        }
190
 
191
        if ($result instanceof Doctrine_Collection) {
192
            $child = $result->getFirst();
193
        } else if (is_array($result)) {
194
            $child = array_shift($result);
195
        }
196
 
197
        return $child;
198
    }
199
 
200
    /**
201
     * gets children for node (direct descendants only)
202
     *
203
     * @return mixed  The children of the node or FALSE if the node has no children.
204
     */
205
    public function getChildren()
206
    {
207
        return $this->getDescendants(1);
208
    }
209
 
210
    /**
211
     * gets descendants for node (direct descendants only)
212
     *
213
     * @return mixed  The descendants of the node or FALSE if the node has no descendants.
214
     */
215
    public function getDescendants($depth = null, $includeNode = false)
216
    {
217
        $baseAlias = $this->_tree->getBaseAlias();
218
        $q = $this->_tree->getBaseQuery();
219
        $params = array($this->record->get('lft'), $this->record->get('rgt'));
220
 
221
        if ($includeNode) {
222
            $q->addWhere("$baseAlias.lft >= ? AND $baseAlias.rgt <= ?", $params)->addOrderBy("$baseAlias.lft asc");
223
        } else {
224
            $q->addWhere("$baseAlias.lft > ? AND $baseAlias.rgt < ?", $params)->addOrderBy("$baseAlias.lft asc");
225
        }
226
 
227
        if ($depth !== null) {
228
            $q->addWhere("$baseAlias.level <= ?", $this->record['level'] + $depth);
229
        }
230
 
231
        $q = $this->_tree->returnQueryWithRootId($q, $this->getRootValue());
232
        $result = $q->execute();
233
 
234
        if (count($result) <= 0) {
235
            return false;
236
        }
237
 
238
        return $result;
239
    }
240
 
241
    /**
242
     * gets record of parent or empty record
243
     *
244
     * @return Doctrine_Record
245
     */
246
    public function getParent()
247
    {
248
        $baseAlias = $this->_tree->getBaseAlias();
249
        $q = $this->_tree->getBaseQuery();
250
        $q->addWhere("$baseAlias.lft < ? AND $baseAlias.rgt > ?", array($this->getLeftValue(), $this->getRightValue()))
251
          ->addWhere("$baseAlias.level >= ?", $this->record['level'] - 1)
252
          ->addOrderBy("$baseAlias.rgt asc");
253
        $q = $this->_tree->returnQueryWithRootId($q, $this->getRootValue());
254
        $result = $q->execute();
255
 
256
        if (count($result) <= 0) {
257
            return false;
258
        }
259
 
260
        if ($result instanceof Doctrine_Collection) {
261
            $parent = $result->getFirst();
262
        } else if (is_array($result)) {
263
            $parent = array_shift($result);
264
        }
265
 
266
        return $parent;
267
    }
268
 
269
    /**
270
     * gets ancestors for node
271
     *
272
     * @param integer $deth  The depth 'upstairs'.
273
     * @return mixed  The ancestors of the node or FALSE if the node has no ancestors (this
274
     *                basically means it's a root node).
275
     */
276
    public function getAncestors($depth = null)
277
    {
278
        $baseAlias = $this->_tree->getBaseAlias();
279
        $q = $this->_tree->getBaseQuery();
280
        $q->addWhere("$baseAlias.lft < ? AND $baseAlias.rgt > ?", array($this->getLeftValue(), $this->getRightValue()))
281
                ->addOrderBy("$baseAlias.lft asc");
282
        if ($depth !== null) {
283
            $q->addWhere("$baseAlias.level >= ?", $this->record['level'] - $depth);
284
        }
285
        $q = $this->_tree->returnQueryWithRootId($q, $this->getRootValue());
286
        $ancestors = $q->execute();
287
        if (count($ancestors) <= 0) {
288
            return false;
289
        }
290
        return $ancestors;
291
    }
292
 
293
    /**
294
     * gets path to node from root, uses record::toString() method to get node names
295
     *
296
     * @param string     $seperator     path seperator
297
     * @param bool     $includeNode     whether or not to include node at end of path
298
     * @return string     string representation of path
299
     */
300
    public function getPath($seperator = ' > ', $includeRecord = false)
301
    {
302
        $path = array();
303
        $ancestors = $this->getAncestors();
304
        if ($ancestors) {
305
            foreach ($ancestors as $ancestor) {
306
                $path[] = $ancestor->__toString();
307
            }
308
        }
309
        if ($includeRecord) {
310
            $path[] = $this->getRecord()->__toString();
311
        }
312
 
313
        return implode($seperator, $path);
314
    }
315
 
316
    /**
317
     * gets number of children (direct descendants)
318
     *
319
     * @return int
320
     */
321
    public function getNumberChildren()
322
    {
323
        $children = $this->getChildren();
324
        return $children === false ? 0 : count($children);
325
    }
326
 
327
    /**
328
     * gets number of descendants (children and their children)
329
     *
330
     * @return int
331
     */
332
    public function getNumberDescendants()
333
    {
334
        return ($this->getRightValue() - $this->getLeftValue() - 1) / 2;
335
    }
336
 
337
    /**
338
     * inserts node as parent of dest record
339
     *
340
     * @return bool
341
     * @todo Wrap in transaction
342
     */
343
    public function insertAsParentOf(Doctrine_Record $dest)
344
    {
345
        // cannot insert a node that has already has a place within the tree
346
        if ($this->isValidNode()) {
347
            return false;
348
        }
349
        // cannot insert as parent of root
350
        if ($dest->getNode()->isRoot()) {
351
            return false;
352
        }
353
 
354
        // cannot insert as parent of itself
355
        if (
356
		    $dest === $this->record ||
357
			($dest->exists() && $this->record->exists() && $dest->identifier() === $this->record->identifier())
358
		) {
359
            throw new Doctrine_Tree_Exception("Cannot insert node as parent of itself");
360
 
361
            return false;
362
        }
363
 
364
        $newLeft  = $dest->getNode()->getLeftValue();
365
        $newRight = $dest->getNode()->getRightValue() + 2;
366
        $newRoot  = $dest->getNode()->getRootValue();
367
		$newLevel = $dest->getNode()->getLevel();
368
 
369
		$conn = $this->record->getTable()->getConnection();
370
		try {
371
		    $conn->beginInternalTransaction();
372
 
373
		    // Make space for new node
374
            $this->shiftRLValues($dest->getNode()->getRightValue() + 1, 2, $newRoot);
375
 
376
            // Slide child nodes over one and down one to allow new parent to wrap them
377
    		$componentName = $this->_tree->getBaseComponent();
378
            $q = Doctrine_Core::getTable($componentName)
379
                ->createQuery()
380
                ->update();
381
            $q->set("$componentName.lft", "$componentName.lft + 1");
382
            $q->set("$componentName.rgt", "$componentName.rgt + 1");
383
            $q->set("$componentName.level", "$componentName.level + 1");
384
            $q->where("$componentName.lft >= ? AND $componentName.rgt <= ?", array($newLeft, $newRight));
385
    		$q = $this->_tree->returnQueryWithRootId($q, $newRoot);
386
    		$q->execute();
387
 
388
            $this->record['level'] = $newLevel;
389
    		$this->insertNode($newLeft, $newRight, $newRoot);
390
 
391
    		$conn->commit();
392
		} catch (Exception $e) {
393
		    $conn->rollback();
394
		    throw $e;
395
		}
396
 
397
        return true;
398
    }
399
 
400
    /**
401
     * inserts node as previous sibling of dest record
402
     *
403
     * @return bool
404
     * @todo Wrap in transaction
405
     */
406
    public function insertAsPrevSiblingOf(Doctrine_Record $dest)
407
    {
408
        // cannot insert a node that has already has a place within the tree
409
        if ($this->isValidNode()) {
410
            return false;
411
        }
412
        // cannot insert as sibling of itself
413
        if (
414
		    $dest === $this->record ||
415
			($dest->exists() && $this->record->exists() && $dest->identifier() === $this->record->identifier())
416
		) {
417
            throw new Doctrine_Tree_Exception("Cannot insert node as previous sibling of itself");
418
 
419
            return false;
420
        }
421
 
422
        $newLeft = $dest->getNode()->getLeftValue();
423
        $newRight = $dest->getNode()->getLeftValue() + 1;
424
        $newRoot = $dest->getNode()->getRootValue();
425
 
426
        $conn = $this->record->getTable()->getConnection();
427
        try {
428
            $conn->beginInternalTransaction();
429
 
430
            $this->shiftRLValues($newLeft, 2, $newRoot);
431
            $this->record['level'] = $dest['level'];
432
            $this->insertNode($newLeft, $newRight, $newRoot);
433
            // update destination left/right values to prevent a refresh
434
            // $dest->getNode()->setLeftValue($dest->getNode()->getLeftValue() + 2);
435
            // $dest->getNode()->setRightValue($dest->getNode()->getRightValue() + 2);
436
 
437
            $conn->commit();
438
        } catch (Exception $e) {
439
            $conn->rollback();
440
            throw $e;
441
        }
442
 
443
        return true;
444
    }
445
 
446
    /**
447
     * inserts node as next sibling of dest record
448
     *
449
     * @return bool
450
     * @todo Wrap in transaction
451
     */
452
    public function insertAsNextSiblingOf(Doctrine_Record $dest)
453
    {
454
        // cannot insert a node that has already has a place within the tree
455
        if ($this->isValidNode()) {
456
            return false;
457
        }
458
        // cannot insert as sibling of itself
459
        if (
460
		    $dest === $this->record ||
461
			($dest->exists() && $this->record->exists() && $dest->identifier() === $this->record->identifier())
462
		) {
463
            throw new Doctrine_Tree_Exception("Cannot insert node as next sibling of itself");
464
 
465
            return false;
466
        }
467
 
468
        $newLeft = $dest->getNode()->getRightValue() + 1;
469
        $newRight = $dest->getNode()->getRightValue() + 2;
470
        $newRoot = $dest->getNode()->getRootValue();
471
 
472
        $conn = $this->record->getTable()->getConnection();
473
        try {
474
            $conn->beginInternalTransaction();
475
 
476
            $this->shiftRLValues($newLeft, 2, $newRoot);
477
            $this->record['level'] = $dest['level'];
478
            $this->insertNode($newLeft, $newRight, $newRoot);
479
            // update destination left/right values to prevent a refresh
480
            // no need, node not affected
481
 
482
            $conn->commit();
483
        } catch (Exception $e) {
484
            $conn->rollback();
485
            throw $e;
486
        }
487
 
488
        return true;
489
    }
490
 
491
    /**
492
     * inserts node as first child of dest record
493
     *
494
     * @return bool
495
     * @todo Wrap in transaction
496
     */
497
    public function insertAsFirstChildOf(Doctrine_Record $dest)
498
    {
499
        // cannot insert a node that has already has a place within the tree
500
        if ($this->isValidNode()) {
501
            return false;
502
        }
503
        // cannot insert as child of itself
504
        if (
505
		    $dest === $this->record ||
506
			($dest->exists() && $this->record->exists() && $dest->identifier() === $this->record->identifier())
507
		) {
508
            throw new Doctrine_Tree_Exception("Cannot insert node as first child of itself");
509
 
510
            return false;
511
        }
512
 
513
        $newLeft = $dest->getNode()->getLeftValue() + 1;
514
        $newRight = $dest->getNode()->getLeftValue() + 2;
515
        $newRoot = $dest->getNode()->getRootValue();
516
 
517
        $conn = $this->record->getTable()->getConnection();
518
        try {
519
            $conn->beginInternalTransaction();
520
 
521
            $this->shiftRLValues($newLeft, 2, $newRoot);
522
            $this->record['level'] = $dest['level'] + 1;
523
            $this->insertNode($newLeft, $newRight, $newRoot);
524
 
525
            // update destination left/right values to prevent a refresh
526
            // $dest->getNode()->setRightValue($dest->getNode()->getRightValue() + 2);
527
 
528
            $conn->commit();
529
        } catch (Exception $e) {
530
            $conn->rollback();
531
            throw $e;
532
        }
533
 
534
        return true;
535
    }
536
 
537
    /**
538
     * inserts node as last child of dest record
539
     *
540
     * @return bool
541
     * @todo Wrap in transaction
542
     */
543
    public function insertAsLastChildOf(Doctrine_Record $dest)
544
    {
545
        // cannot insert a node that has already has a place within the tree
546
        if ($this->isValidNode()) {
547
            return false;
548
        }
549
        // cannot insert as child of itself
550
        if (
551
		    $dest === $this->record ||
552
			($dest->exists() && $this->record->exists() && $dest->identifier() === $this->record->identifier())
553
		) {
554
            throw new Doctrine_Tree_Exception("Cannot insert node as last child of itself");
555
 
556
            return false;
557
        }
558
 
559
        $newLeft = $dest->getNode()->getRightValue();
560
        $newRight = $dest->getNode()->getRightValue() + 1;
561
        $newRoot = $dest->getNode()->getRootValue();
562
 
563
        $conn = $this->record->getTable()->getConnection();
564
        try {
565
            $conn->beginInternalTransaction();
566
 
567
            $this->shiftRLValues($newLeft, 2, $newRoot);
568
            $this->record['level'] = $dest['level'] + 1;
569
            $this->insertNode($newLeft, $newRight, $newRoot);
570
 
571
            // update destination left/right values to prevent a refresh
572
            // $dest->getNode()->setRightValue($dest->getNode()->getRightValue() + 2);
573
 
574
            $conn->commit();
575
        } catch (Exception $e) {
576
            $conn->rollback();
577
            throw $e;
578
        }
579
 
580
        return true;
581
    }
582
 
583
    /**
584
     * Accomplishes moving of nodes between different trees.
585
     * Used by the move* methods if the root values of the two nodes are different.
586
     *
587
     * @param Doctrine_Record $dest
588
     * @param unknown_type $newLeftValue
589
     * @param unknown_type $moveType
590
     * @todo Better exception handling/wrapping
591
     */
592
    private function _moveBetweenTrees(Doctrine_Record $dest, $newLeftValue, $moveType)
593
    {
594
        $conn = $this->record->getTable()->getConnection();
595
 
596
        try {
597
            $conn->beginInternalTransaction();
598
 
599
            // Move between trees: Detach from old tree & insert into new tree
600
            $newRoot = $dest->getNode()->getRootValue();
601
            $oldRoot = $this->getRootValue();
602
            $oldLft = $this->getLeftValue();
603
            $oldRgt = $this->getRightValue();
604
            $oldLevel = $this->record['level'];
605
 
606
            // Prepare target tree for insertion, make room
607
            $this->shiftRlValues($newLeftValue, $oldRgt - $oldLft - 1, $newRoot);
608
 
609
            // Set new root id for this node
610
            $this->setRootValue($newRoot);
611
            $this->record->save();
612
 
613
            // Insert this node as a new node
614
            $this->setRightValue(0);
615
            $this->setLeftValue(0);
616
 
617
            switch ($moveType) {
618
                case 'moveAsPrevSiblingOf':
619
                    $this->insertAsPrevSiblingOf($dest);
620
                break;
621
                case 'moveAsFirstChildOf':
622
                    $this->insertAsFirstChildOf($dest);
623
                break;
624
                case 'moveAsNextSiblingOf':
625
                    $this->insertAsNextSiblingOf($dest);
626
                break;
627
                case 'moveAsLastChildOf':
628
                    $this->insertAsLastChildOf($dest);
629
                break;
630
                default:
631
                    throw new Doctrine_Node_Exception("Unknown move operation: $moveType.");
632
            }
633
 
634
            $diff = $oldRgt - $oldLft;
635
            $this->setRightValue($this->getLeftValue() + ($oldRgt - $oldLft));
636
            $this->record->save();
637
 
638
            $newLevel = $this->record['level'];
639
            $levelDiff = $newLevel - $oldLevel;
640
 
641
            // Relocate descendants of the node
642
            $diff = $this->getLeftValue() - $oldLft;
643
            $componentName = $this->_tree->getBaseComponent();
644
            $rootColName = $this->_tree->getAttribute('rootColumnName');
645
 
646
            // Update lft/rgt/root/level for all descendants
647
            $q = Doctrine_Core::getTable($componentName)
648
                ->createQuery()
649
                ->update()
650
                ->set($componentName . '.lft', $componentName.'.lft + ?', $diff)
651
                ->set($componentName . '.rgt', $componentName.'.rgt + ?', $diff)
652
                ->set($componentName . '.level', $componentName.'.level + ?', $levelDiff)
653
                ->set($componentName . '.' . $rootColName, '?', $newRoot)
654
                ->where($componentName . '.lft > ? AND ' . $componentName . '.rgt < ?', array($oldLft, $oldRgt));
655
            $q = $this->_tree->returnQueryWithRootId($q, $oldRoot);
656
            $q->execute();
657
 
658
            // Close gap in old tree
659
            $first = $oldRgt + 1;
660
            $delta = $oldLft - $oldRgt - 1;
661
            $this->shiftRLValues($first, $delta, $oldRoot);
662
 
663
            $conn->commit();
664
 
665
	        return true;
666
        } catch (Exception $e) {
667
            $conn->rollback();
668
            throw $e;
669
        }
670
 
671
        return false;
672
    }
673
 
674
    /**
675
     * moves node as prev sibling of dest record
676
     *
677
     */
678
    public function moveAsPrevSiblingOf(Doctrine_Record $dest)
679
    {
680
        if (
681
		    $dest === $this->record ||
682
			($dest->exists() && $this->record->exists() && $dest->identifier() === $this->record->identifier())
683
		) {
684
            throw new Doctrine_Tree_Exception("Cannot move node as previous sibling of itself");
685
 
686
			return false;
687
		}
688
 
689
        if ($dest->getNode()->getRootValue() != $this->getRootValue()) {
690
            // Move between trees
691
            return $this->_moveBetweenTrees($dest, $dest->getNode()->getLeftValue(), __FUNCTION__);
692
        } else {
693
            // Move within the tree
694
            $oldLevel = $this->record['level'];
695
            $this->record['level'] = $dest['level'];
696
            $this->updateNode($dest->getNode()->getLeftValue(), $this->record['level'] - $oldLevel);
697
        }
698
 
699
        return true;
700
    }
701
 
702
    /**
703
     * moves node as next sibling of dest record
704
     *
705
     */
706
    public function moveAsNextSiblingOf(Doctrine_Record $dest)
707
    {
708
        if (
709
		    $dest === $this->record ||
710
			($dest->exists() && $this->record->exists() && $dest->identifier() === $this->record->identifier())
711
		) {
712
            throw new Doctrine_Tree_Exception("Cannot move node as next sibling of itself");
713
 
714
            return false;
715
        }
716
 
717
        if ($dest->getNode()->getRootValue() != $this->getRootValue()) {
718
            // Move between trees
719
            return $this->_moveBetweenTrees($dest, $dest->getNode()->getRightValue() + 1, __FUNCTION__);
720
        } else {
721
            // Move within tree
722
            $oldLevel = $this->record['level'];
723
            $this->record['level'] = $dest['level'];
724
            $this->updateNode($dest->getNode()->getRightValue() + 1, $this->record['level'] - $oldLevel);
725
        }
726
 
727
        return true;
728
    }
729
 
730
    /**
731
     * moves node as first child of dest record
732
     *
733
     */
734
    public function moveAsFirstChildOf(Doctrine_Record $dest)
735
    {
736
        if (
737
		    $dest === $this->record || $this->isAncestorOf($dest) ||
738
			($dest->exists() && $this->record->exists() && $dest->identifier() === $this->record->identifier())
739
		) {
740
            throw new Doctrine_Tree_Exception("Cannot move node as first child of itself or into a descendant");
741
 
742
			return false;
743
		}
744
 
745
		if ($dest->getNode()->getRootValue() != $this->getRootValue()) {
746
            // Move between trees
747
            return $this->_moveBetweenTrees($dest, $dest->getNode()->getLeftValue() + 1, __FUNCTION__);
748
        } else {
749
            // Move within tree
750
            $oldLevel = $this->record['level'];
751
            $this->record['level'] = $dest['level'] + 1;
752
            $this->updateNode($dest->getNode()->getLeftValue() + 1, $this->record['level'] - $oldLevel);
753
        }
754
 
755
        return true;
756
    }
757
 
758
    /**
759
     * moves node as last child of dest record
760
     *
761
     */
762
    public function moveAsLastChildOf(Doctrine_Record $dest)
763
    {
764
        if (
765
		    $dest === $this->record || $this->isAncestorOf($dest) ||
766
			($dest->exists() && $this->record->exists() && $dest->identifier() === $this->record->identifier())
767
		) {
768
            throw new Doctrine_Tree_Exception("Cannot move node as last child of itself or into a descendant");
769
 
770
			return false;
771
		}
772
 
773
        if ($dest->getNode()->getRootValue() != $this->getRootValue()) {
774
            // Move between trees
775
            return $this->_moveBetweenTrees($dest, $dest->getNode()->getRightValue(), __FUNCTION__);
776
        } else {
777
            // Move within tree
778
            $oldLevel = $this->record['level'];
779
            $this->record['level'] = $dest['level'] + 1;
780
            $this->updateNode($dest->getNode()->getRightValue(), $this->record['level'] - $oldLevel);
781
        }
782
 
783
        return true;
784
    }
785
 
786
    /**
787
     * Makes this node a root node. Only used in multiple-root trees.
788
     *
789
     * @todo Exception handling/wrapping
790
     */
791
    public function makeRoot($newRootId)
792
    {
793
        // TODO: throw exception instead?
794
        if ($this->getLeftValue() == 1 || ! $this->_tree->getAttribute('hasManyRoots')) {
795
            return false;
796
        }
797
 
798
        $oldRgt = $this->getRightValue();
799
        $oldLft = $this->getLeftValue();
800
        $oldRoot = $this->getRootValue();
801
        $oldLevel = $this->record['level'];
802
 
803
        $conn = $this->record->getTable()->getConnection();
804
        try {
805
            $conn->beginInternalTransaction();
806
 
807
            // Update descendants lft/rgt/root/level values
808
            $diff = 1 - $oldLft;
809
            $newRoot = $newRootId;
810
            $componentName = $this->_tree->getBaseComponent();
811
            $rootColName = $this->_tree->getAttribute('rootColumnName');
812
            $q = Doctrine_Core::getTable($componentName)
813
                ->createQuery()
814
                ->update()
815
                ->set($componentName . '.lft', $componentName.'.lft + ?', array($diff))
816
                ->set($componentName . '.rgt', $componentName.'.rgt + ?', array($diff))
817
                ->set($componentName . '.level', $componentName.'.level - ?', array($oldLevel))
818
                ->set($componentName . '.' . $rootColName, '?', array($newRoot))
819
                ->where($componentName . '.lft > ? AND ' . $componentName . '.rgt < ?', array($oldLft, $oldRgt));
820
            $q = $this->_tree->returnQueryWithRootId($q, $oldRoot);
821
            $q->execute();
822
 
823
            // Detach from old tree (close gap in old tree)
824
            $first = $oldRgt + 1;
825
            $delta = $oldLft - $oldRgt - 1;
826
            $this->shiftRLValues($first, $delta, $this->getRootValue());
827
 
828
            // Set new lft/rgt/root/level values for root node
829
            $this->setLeftValue(1);
830
            $this->setRightValue($oldRgt - $oldLft + 1);
831
            $this->setRootValue($newRootId);
832
            $this->record['level'] = 0;
833
 
834
            $this->record->save();
835
 
836
            $conn->commit();
837
 
838
            return true;
839
        } catch (Exception $e) {
840
            $conn->rollback();
841
            throw $e;
842
        }
843
 
844
        return false;
845
    }
846
 
847
    /**
848
     * adds node as last child of record
849
     *
850
     */
851
    public function addChild(Doctrine_Record $record)
852
    {
853
        $record->getNode()->insertAsLastChildOf($this->getRecord());
854
    }
855
 
856
    /**
857
     * determines if node is leaf
858
     *
859
     * @return bool
860
     */
861
    public function isLeaf()
862
    {
863
        return (($this->getRightValue() - $this->getLeftValue()) == 1);
864
    }
865
 
866
    /**
867
     * determines if node is root
868
     *
869
     * @return bool
870
     */
871
    public function isRoot()
872
    {
873
        return ($this->getLeftValue() == 1);
874
    }
875
 
876
    /**
877
     * determines if node is equal to subject node
878
     *
879
     * @return bool
880
     */
881
    public function isEqualTo(Doctrine_Record $subj)
882
    {
883
        return (($this->getLeftValue() == $subj->getNode()->getLeftValue()) &&
884
                ($this->getRightValue() == $subj->getNode()->getRightValue()) &&
885
                ($this->getRootValue() == $subj->getNode()->getRootValue())
886
                );
887
    }
888
 
889
    /**
890
     * determines if node is child of subject node
891
     *
892
     * @return bool
893
     */
894
    public function isDescendantOf(Doctrine_Record $subj)
895
    {
896
        return (($this->getLeftValue() > $subj->getNode()->getLeftValue()) &&
897
                ($this->getRightValue() < $subj->getNode()->getRightValue()) &&
898
                ($this->getRootValue() == $subj->getNode()->getRootValue()));
899
    }
900
 
901
    /**
902
     * determines if node is child of or sibling to subject node
903
     *
904
     * @return bool
905
     */
906
    public function isDescendantOfOrEqualTo(Doctrine_Record $subj)
907
    {
908
        return (($this->getLeftValue() >= $subj->getNode()->getLeftValue()) &&
909
                ($this->getRightValue() <= $subj->getNode()->getRightValue()) &&
910
                ($this->getRootValue() == $subj->getNode()->getRootValue()));
911
    }
912
 
913
    /**
914
     * determines if node is ancestor of subject node
915
     *
916
     * @return bool
917
     */
918
    public function isAncestorOf(Doctrine_Record $subj)
919
    {
920
        return (($subj->getNode()->getLeftValue() > $this->getLeftValue()) &&
921
                ($subj->getNode()->getRightValue() < $this->getRightValue()) &&
922
                ($subj->getNode()->getRootValue() == $this->getRootValue()));
923
    }
924
 
925
    /**
926
     * determines if node is valid
927
     *
928
     * @return bool
929
     */
930
    public function isValidNode($record = null)
931
    {
932
        if ($record === null) {
933
            return ($this->getRightValue() > $this->getLeftValue());
934
        } else if ( $record instanceof Doctrine_Record ) {
935
            return ($record->getNode()->getRightValue() > $record->getNode()->getLeftValue());
936
        } else {
937
            return false;
938
        }
939
    }
940
 
941
    /**
942
     * Detaches the node from the tree by invalidating it's lft & rgt values
943
     * (they're set to 0).
944
     */
945
    public function detach()
946
    {
947
        $this->setLeftValue(0);
948
        $this->setRightValue(0);
949
    }
950
 
951
    /**
952
     * deletes node and it's descendants
953
     * @todo Delete more efficiently. Wrap in transaction if needed.
954
     */
955
    public function delete()
956
    {
957
        $conn = $this->record->getTable()->getConnection();
958
        try {
959
            $conn->beginInternalTransaction();
960
 
961
            // TODO: add the setting whether or not to delete descendants or relocate children
962
            $oldRoot = $this->getRootValue();
963
            $q = $this->_tree->getBaseQuery();
964
 
965
            $baseAlias = $this->_tree->getBaseAlias();
966
            $componentName = $this->_tree->getBaseComponent();
967
 
968
            $q = $q->addWhere("$baseAlias.lft >= ? AND $baseAlias.rgt <= ?", array($this->getLeftValue(), $this->getRightValue()));
969
 
970
            $q = $this->_tree->returnQueryWithRootId($q, $oldRoot);
971
 
972
            $coll = $q->execute();
973
 
974
            $coll->delete();
975
 
976
            $first = $this->getRightValue() + 1;
977
            $delta = $this->getLeftValue() - $this->getRightValue() - 1;
978
            $this->shiftRLValues($first, $delta, $oldRoot);
979
 
980
            $conn->commit();
981
        } catch (Exception $e) {
982
            $conn->rollback();
983
            throw $e;
984
        }
985
 
986
        return true;
987
    }
988
 
989
    /**
990
     * sets node's left and right values and save's it
991
     *
992
     * @param int     $destLeft     node left value
993
     * @param int        $destRight    node right value
994
     */
995
    private function insertNode($destLeft = 0, $destRight = 0, $destRoot = 1)
996
    {
997
        $this->setLeftValue($destLeft);
998
        $this->setRightValue($destRight);
999
        $this->setRootValue($destRoot);
1000
        $this->record->save();
1001
    }
1002
 
1003
    /**
1004
     * move node's and its children to location $destLeft and updates rest of tree
1005
     *
1006
     * @param int     $destLeft    destination left value
1007
     * @todo Wrap in transaction
1008
     */
1009
    private function updateNode($destLeft, $levelDiff)
1010
    {
1011
        $componentName = $this->_tree->getBaseComponent();
1012
        $left = $this->getLeftValue();
1013
        $right = $this->getRightValue();
1014
        $rootId = $this->getRootValue();
1015
 
1016
        $treeSize = $right - $left + 1;
1017
 
1018
        $conn = $this->record->getTable()->getConnection();
1019
        try {
1020
            $conn->beginInternalTransaction();
1021
 
1022
            // Make room in the new branch
1023
            $this->shiftRLValues($destLeft, $treeSize, $rootId);
1024
 
1025
            if ($left >= $destLeft) { // src was shifted too?
1026
                $left += $treeSize;
1027
                $right += $treeSize;
1028
            }
1029
 
1030
            // update level for descendants
1031
            $q = Doctrine_Core::getTable($componentName)
1032
                ->createQuery()
1033
                ->update()
1034
                ->set($componentName . '.level', $componentName.'.level + ?', array($levelDiff))
1035
                ->where($componentName . '.lft > ? AND ' . $componentName . '.rgt < ?', array($left, $right));
1036
            $q = $this->_tree->returnQueryWithRootId($q, $rootId);
1037
            $q->execute();
1038
 
1039
            // now there's enough room next to target to move the subtree
1040
            $this->shiftRLRange($left, $right, $destLeft - $left, $rootId);
1041
 
1042
            // correct values after source (close gap in old tree)
1043
            $this->shiftRLValues($right + 1, -$treeSize, $rootId);
1044
 
1045
            $this->record->save();
1046
            $this->record->refresh();
1047
 
1048
            $conn->commit();
1049
        } catch (Exception $e) {
1050
            $conn->rollback();
1051
            throw $e;
1052
        }
1053
 
1054
        return true;
1055
    }
1056
 
1057
    /**
1058
     * adds '$delta' to all Left and Right values that are >= '$first'. '$delta' can also be negative.
1059
     *
1060
     * Note: This method does wrap its database queries in a transaction. This should be done
1061
     * by the invoking code.
1062
     *
1063
     * @param int $first         First node to be shifted
1064
     * @param int $delta         Value to be shifted by, can be negative
1065
     */
1066
    private function shiftRlValues($first, $delta, $rootId = 1)
1067
    {
1068
        // shift left columns
1069
        $componentName = $this->_tree->getBaseComponent();
1070
 
1071
        $qLeft  = Doctrine_Core::getTable($componentName)
1072
            ->createQuery()
1073
            ->update($componentName);
1074
 
1075
        $qRight = Doctrine_Core::getTable($componentName)
1076
            ->createQuery()
1077
            ->update($componentName);
1078
 
1079
        $qLeft = $qLeft->set($componentName . '.lft', $componentName.'.lft + ?', $delta)
1080
                       ->where($componentName . '.lft >= ?', $first);
1081
        $qLeft = $this->_tree->returnQueryWithRootId($qLeft, $rootId);
1082
 
1083
        $resultLeft = $qLeft->execute();
1084
 
1085
        // shift right columns
1086
        $qRight = $qRight->set($componentName . '.rgt', $componentName.'.rgt + ?', $delta)
1087
                         ->where($componentName . '.rgt >= ?', $first);
1088
 
1089
        $qRight = $this->_tree->returnQueryWithRootId($qRight, $rootId);
1090
 
1091
        $resultRight = $qRight->execute();
1092
    }
1093
 
1094
    /**
1095
     * adds '$delta' to all Left and Right values that are >= '$first' and <= '$last'.
1096
     * '$delta' can also be negative.
1097
     *
1098
     * Note: This method does wrap its database queries in a transaction. This should be done
1099
     * by the invoking code.
1100
     *
1101
     * @param int $first     First node to be shifted (L value)
1102
     * @param int $last     Last node to be shifted (L value)
1103
     * @param int $delta         Value to be shifted by, can be negative
1104
     */
1105
    private function shiftRlRange($first, $last, $delta, $rootId = 1)
1106
    {
1107
        $componentName = $this->_tree->getBaseComponent();
1108
 
1109
        $qLeft  = Doctrine_Core::getTable($componentName)
1110
            ->createQuery()
1111
            ->update();
1112
 
1113
        $qRight = Doctrine_Core::getTable($componentName)
1114
            ->createQuery()
1115
            ->update();
1116
 
1117
        // shift left column values
1118
        $qLeft = $qLeft->set($componentName . '.lft', $componentName.'.lft + ?', $delta)
1119
                       ->where($componentName . '.lft >= ? AND ' . $componentName . '.lft <= ?', array($first, $last));
1120
 
1121
        $qLeft = $this->_tree->returnQueryWithRootId($qLeft, $rootId);
1122
 
1123
        $resultLeft = $qLeft->execute();
1124
 
1125
        // shift right column values
1126
        $qRight = $qRight->set($componentName . '.rgt', $componentName.'.rgt + ?', $delta)
1127
                        ->where($componentName . '.rgt >= ? AND ' . $componentName . '.rgt <= ?', array($first, $last));
1128
 
1129
        $qRight = $this->_tree->returnQueryWithRootId($qRight, $rootId);
1130
 
1131
        $resultRight = $qRight->execute();
1132
    }
1133
 
1134
    /**
1135
     * gets record's left value
1136
     *
1137
     * @return int
1138
     */
1139
    public function getLeftValue()
1140
    {
1141
        return $this->record->get('lft');
1142
    }
1143
 
1144
    /**
1145
     * sets record's left value
1146
     *
1147
     * @param int
1148
     */
1149
    public function setLeftValue($lft)
1150
    {
1151
        $this->record->set('lft', $lft);
1152
    }
1153
 
1154
    /**
1155
     * gets record's right value
1156
     *
1157
     * @return int
1158
     */
1159
    public function getRightValue()
1160
    {
1161
        return $this->record->get('rgt');
1162
    }
1163
 
1164
    /**
1165
     * sets record's right value
1166
     *
1167
     * @param int
1168
     */
1169
    public function setRightValue($rgt)
1170
    {
1171
        $this->record->set('rgt', $rgt);
1172
    }
1173
 
1174
    /**
1175
     * gets level (depth) of node in the tree
1176
     *
1177
     * @return int
1178
     */
1179
    public function getLevel()
1180
    {
1181
        if ( ! isset($this->record['level'])) {
1182
            $baseAlias = $this->_tree->getBaseAlias();
1183
            $componentName = $this->_tree->getBaseComponent();
1184
            $q = $this->_tree->getBaseQuery();
1185
            $q = $q->addWhere("$baseAlias.lft < ? AND $baseAlias.rgt > ?", array($this->getLeftValue(), $this->getRightValue()));
1186
 
1187
            $q = $this->_tree->returnQueryWithRootId($q, $this->getRootValue());
1188
 
1189
            $coll = $q->execute();
1190
 
1191
            $this->record['level'] = count($coll) ? count($coll) : 0;
1192
        }
1193
        return $this->record['level'];
1194
    }
1195
 
1196
    /**
1197
     * get records root id value
1198
     *
1199
     */
1200
    public function getRootValue()
1201
    {
1202
        if ($this->_tree->getAttribute('hasManyRoots')) {
1203
            return $this->record->get($this->_tree->getAttribute('rootColumnName'));
1204
        }
1205
        return 1;
1206
    }
1207
 
1208
    /**
1209
     * sets records root id value
1210
     *
1211
     * @param int
1212
     */
1213
    public function setRootValue($value)
1214
    {
1215
        if ($this->_tree->getAttribute('hasManyRoots')) {
1216
            $this->record->set($this->_tree->getAttribute('rootColumnName'), $value);
1217
        }
1218
    }
1219
}