Coverage for Doctrine_Node_NestedSet

Back to coverage report

1 <?php
2 /*
3  *    $Id: NestedSet.php 2702 2007-10-03 21:43:22Z Jonathan.Wage $
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.phpdoctrine.com>.
20  */
21 /**
22  * Doctrine_Node_NestedSet
23  *
24  * @package    Doctrine
25  * @subpackage Node
26  * @license    http://www.opensource.org/licenses/lgpl-license.php LGPL
27  * @link       www.phpdoctrine.com
28  * @since      1.0
29  * @version    $Revision: 2702 $
30  * @author     Joe Simms <joe.simms@websites4.com>
31  * @author     Roman Borschel <roman@code-factory.org>     
32  */
33 class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Interface
34 {
35     /**
36      * test if node has previous sibling
37      *
38      * @return bool            
39      */
40     public function hasPrevSibling()
41     {
42         return $this->isValidNode($this->getPrevSibling());        
43     }
44
45     /**
46      * test if node has next sibling
47      *
48      * @return bool            
49      */ 
50     public function hasNextSibling()
51     {
52         return $this->isValidNode($this->getNextSibling());        
53     }
54
55     /**
56      * test if node has children
57      *
58      * @return bool            
59      */
60     public function hasChildren()
61     {
62         return (($this->getRightValue() - $this->getLeftValue() ) >1 );        
63     }
64
65     /**
66      * test if node has parent
67      *
68      * @return bool            
69      */
70     public function hasParent()
71     {
72         return !$this->isRoot();
73     }
74
75     /**
76      * gets record of prev sibling or empty record
77      *
78      * @return object     Doctrine_Record            
79      */
80     public function getPrevSibling()
81     {
82         $baseAlias = $this->_tree->getBaseAlias();
83         $q = $this->_tree->getBaseQuery();
84         $q = $q->addWhere("$baseAlias.rgt = ?", $this->getLeftValue() - 1);
85         $q = $this->_tree->returnQueryWithRootId($q, $this->getRootValue());
86         $result = $q->execute();
87
88         if (count($result) <= 0) {
89             return false;
90         }
91         
92         if ($result instanceof Doctrine_Collection) {
93             $sibling = $result->getFirst();
94         } else if (is_array($result)) {
95             $sibling = array_shift($result);
96         }
97         
98         return $sibling;
99     }
100
101     /**
102      * gets record of next sibling or empty record
103      *
104      * @return object     Doctrine_Record            
105      */
106     public function getNextSibling()
107     {
108         $baseAlias = $this->_tree->getBaseAlias();
109         $q = $this->_tree->getBaseQuery();
110         $q = $q->addWhere("$baseAlias.lft = ?", $this->getRightValue() + 1);
111         $q = $this->_tree->returnQueryWithRootId($q, $this->getRootValue());
112         $result = $q->execute();
113
114         if (count($result) <= 0) {
115             return false;
116         }
117         
118         if ($result instanceof Doctrine_Collection) {
119             $sibling = $result->getFirst();
120         } else if (is_array($result)) {
121             $sibling = array_shift($result);
122         }
123         
124         return $sibling;
125     }
126
127     /**
128      * gets siblings for node
129      *
130      * @return array     array of sibling Doctrine_Record objects            
131      */
132     public function getSiblings($includeNode = false)
133     {
134         $parent = $this->getParent();
135         $siblings = array();
136         if ($parent->exists()) {
137             foreach ($parent->getNode()->getChildren() as $child) {
138                 if ($this->isEqualTo($child) && !$includeNode) {
139                     continue;
140                 }
141                 $siblings[] = $child;
142             }        
143         }
144         return $siblings;
145     }
146
147     /**
148      * gets record of first child or empty record
149      *
150      * @return object     Doctrine_Record            
151      */
152     public function getFirstChild()
153     {
154         $baseAlias = $this->_tree->getBaseAlias();
155         $q = $this->_tree->getBaseQuery();
156         $q->addWhere("$baseAlias.lft = ?", $this->getLeftValue() + 1);
157         $this->_tree->returnQueryWithRootId($q, $this->getRootValue());
158         $result = $q->execute();
159
160         if (count($result) <= 0) {
161             return false;
162         }
163         
164         if ($result instanceof Doctrine_Collection) {
165             $child = $result->getFirst();
166         } else if (is_array($result)) {
167             $child = array_shift($result);
168         }
169         
170         return $child;       
171     }
172
173     /**
174      * gets record of last child or empty record
175      *
176      * @return object     Doctrine_Record            
177      */
178     public function getLastChild()
179     {
180         $baseAlias = $this->_tree->getBaseAlias();
181         $q = $this->_tree->getBaseQuery();
182         $q->addWhere("$baseAlias.rgt = ?", $this->getRightValue() - 1);
183         $this->_tree->returnQueryWithRootId($q, $this->getRootValue());
184         $result = $q->execute();
185
186         if (count($result) <= 0) {
187             return false;
188         }
189         
190         if ($result instanceof Doctrine_Collection) {
191             $child = $result->getFirst();
192         } else if (is_array($result)) {
193             $child = array_shift($result);
194         }
195         
196         return $child;      
197     }
198
199     /**
200      * gets children for node (direct descendants only)
201      *
202      * @return mixed The children of the node or FALSE if the node has no children.               
203      */
204     public function getChildren()
205     { 
206         return $this->getDescendants(1);
207     }
208
209     /**
210      * gets descendants for node (direct descendants only)
211      *
212      * @return mixed  The descendants of the node or FALSE if the node has no descendants.
213      * @todo Currently all descendants are fetched, no matter the depth. Maybe there is a better
214      *       solution with less overhead.      
215      */
216     public function getDescendants($depth = null, $includeNode = false)
217     {
218         $baseAlias = $this->_tree->getBaseAlias();
219         $q = $this->_tree->getBaseQuery();
220         $params = array($this->record->get('lft'), $this->record->get('rgt'));
221         
222         if ($includeNode) {
223             $q->addWhere("$baseAlias.lft >= ? AND $baseAlias.rgt <= ?", $params)->addOrderBy("$baseAlias.lft asc");
224         } else {
225             $q->addWhere("$baseAlias.lft > ? AND $baseAlias.rgt < ?", $params)->addOrderBy("$baseAlias.lft asc");
226         }
227         
228         if ($depth !== null) {
229             $q->addWhere("$baseAlias.level <= ?", $this->record['level'] + $depth);
230         }
231         
232         $q = $this->_tree->returnQueryWithRootId($q, $this->getRootValue());
233         $result = $q->execute();
234
235         if (count($result) <= 0) {
236             return false;
237         }
238
239         return $result;
240     }
241
242     /**
243      * gets record of parent or empty record
244      *
245      * @return object     Doctrine_Record            
246      */
247     public function getParent()
248     {
249         $baseAlias = $this->_tree->getBaseAlias();
250         $q = $this->_tree->getBaseQuery();
251         $q->addWhere("$baseAlias.lft < ? AND $baseAlias.rgt > ?", array($this->getLeftValue(), $this->getRightValue()))
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         foreach ($ancestors as $ancestor) {
305             $path[] = $ancestor->__toString();
306         }
307         if ($includeRecord) {
308             $path[] = $this->getRecord()->__toString();
309         }
310             
311         return implode($seperator, $path);
312     }
313
314     /**
315      * gets number of children (direct descendants)
316      *
317      * @return int            
318      */     
319     public function getNumberChildren()
320     {
321         return count($this->getChildren());
322     }
323
324     /**
325      * gets number of descendants (children and their children)
326      *
327      * @return int            
328      */
329     public function getNumberDescendants()
330     {
331         return ($this->getRightValue() - $this->getLeftValue() - 1) / 2;
332     }
333
334     /**
335      * inserts node as parent of dest record
336      *
337      * @return bool
338      * @todo Wrap in transaction          
339      */
340     public function insertAsParentOf(Doctrine_Record $dest)
341     {
342         // cannot insert a node that has already has a place within the tree
343         if ($this->isValidNode()) {
344             return false;
345         }
346         // cannot insert as parent of root
347         if ($dest->getNode()->isRoot()) {
348             return false;
349         }
350         $newRoot = $dest->getNode()->getRootValue();
351         $this->shiftRLValues($dest->getNode()->getLeftValue(), 1, $newRoot);
352         $this->shiftRLValues($dest->getNode()->getRightValue() + 2, 1, $newRoot);
353         
354         $newLeft = $dest->getNode()->getLeftValue();
355         $newRight = $dest->getNode()->getRightValue() + 2;
356
357         $this->record['level'] = $dest['level'] - 1;
358         $this->insertNode($newLeft, $newRight, $newRoot);
359         
360         return true;
361     }
362
363     /**
364      * inserts node as previous sibling of dest record
365      *
366      * @return bool
367      * @todo Wrap in transaction       
368      */
369     public function insertAsPrevSiblingOf(Doctrine_Record $dest)
370     {
371         // cannot insert a node that has already has a place within the tree
372         if ($this->isValidNode())
373             return false;
374
375         $newLeft = $dest->getNode()->getLeftValue();
376         $newRight = $dest->getNode()->getLeftValue() + 1;
377         $newRoot = $dest->getNode()->getRootValue();
378         
379         $this->shiftRLValues($newLeft, 2, $newRoot);
380         $this->record['level'] = $dest['level'];
381         $this->insertNode($newLeft, $newRight, $newRoot);
382         // update destination left/right values to prevent a refresh
383         // $dest->getNode()->setLeftValue($dest->getNode()->getLeftValue() + 2);
384         // $dest->getNode()->setRightValue($dest->getNode()->getRightValue() + 2);
385                         
386         return true;
387     }
388
389     /**
390      * inserts node as next sibling of dest record
391      *
392      * @return bool
393      * @todo Wrap in transaction           
394      */    
395     public function insertAsNextSiblingOf(Doctrine_Record $dest)
396     {
397         // cannot insert a node that has already has a place within the tree
398         if ($this->isValidNode())
399             return false;
400
401         $newLeft = $dest->getNode()->getRightValue() + 1;
402         $newRight = $dest->getNode()->getRightValue() + 2;
403         $newRoot = $dest->getNode()->getRootValue();
404
405         $this->shiftRLValues($newLeft, 2, $newRoot);
406         $this->record['level'] = $dest['level'];
407         $this->insertNode($newLeft, $newRight, $newRoot);
408
409         // update destination left/right values to prevent a refresh
410         // no need, node not affected
411
412         return true;
413     }
414
415     /**
416      * inserts node as first child of dest record
417      *
418      * @return bool
419      * @todo Wrap in transaction         
420      */
421     public function insertAsFirstChildOf(Doctrine_Record $dest)
422     {
423         // cannot insert a node that has already has a place within the tree
424         if ($this->isValidNode())
425             return false;
426
427         $newLeft = $dest->getNode()->getLeftValue() + 1;
428         $newRight = $dest->getNode()->getLeftValue() + 2;
429         $newRoot = $dest->getNode()->getRootValue();
430
431         $this->shiftRLValues($newLeft, 2, $newRoot);
432         $this->record['level'] = $dest['level'] + 1;
433         $this->insertNode($newLeft, $newRight, $newRoot);
434         
435         // update destination left/right values to prevent a refresh
436         // $dest->getNode()->setRightValue($dest->getNode()->getRightValue() + 2);
437
438         return true;
439     }
440
441     /**
442      * inserts node as last child of dest record
443      *
444      * @return bool
445      * @todo Wrap in transaction            
446      */
447     public function insertAsLastChildOf(Doctrine_Record $dest)
448     {
449         // cannot insert a node that has already has a place within the tree
450         if ($this->isValidNode())
451             return false;
452
453         $newLeft = $dest->getNode()->getRightValue();
454         $newRight = $dest->getNode()->getRightValue() + 1;
455         $newRoot = $dest->getNode()->getRootValue();
456
457         $this->shiftRLValues($newLeft, 2, $newRoot);
458         $this->record['level'] = $dest['level'] + 1;
459         $this->insertNode($newLeft, $newRight, $newRoot);
460
461         // update destination left/right values to prevent a refresh
462         // $dest->getNode()->setRightValue($dest->getNode()->getRightValue() + 2);
463         
464         return true;
465     }
466     
467     /**
468      * Accomplishes moving of nodes between different trees.
469      * Used by the move* methods if the root values of the two nodes are different.
470      *
471      * @param Doctrine_Record $dest
472      * @param unknown_type $newLeftValue
473      * @param unknown_type $moveType
474      * @todo Better exception handling/wrapping
475      */
476     private function _moveBetweenTrees(Doctrine_Record $dest, $newLeftValue, $moveType)
477     {
478         $conn = $this->record->getTable()->getConnection();
479             
480             try {
481                 $conn->beginTransaction();
482                 
483                 // Move between trees: Detach from old tree & insert into new tree
484                 $newRoot = $dest->getNode()->getRootValue();
485                 $oldRoot = $this->getRootValue();
486                 $oldLft = $this->getLeftValue();
487                 $oldRgt = $this->getRightValue();
488                 $oldLevel = $this->record['level'];
489                 
490                 // Prepare target tree for insertion, make room
491                 $this->shiftRlValues($newLeftValue, $oldRgt - $oldLft - 1, $newRoot);
492                 
493                 // Set new root id for this node
494                 $this->setRootValue($newRoot);
495                 $this->record->save();
496                 
497                 // Close gap in old tree
498                 $first = $oldRgt + 1;
499                 $delta = $oldLft - $oldRgt - 1;
500                 $this->shiftRLValues($first, $delta, $oldRoot);
501                 
502                 // Insert this node as a new node
503                 $this->setRightValue(0);
504                 $this->setLeftValue(0);
505                 
506                 switch ($moveType) {
507                     case 'moveAsPrevSiblingOf':
508                         $this->insertAsPrevSiblingOf($dest);
509                     break;
510                     case 'moveAsFirstChildOf':
511                         $this->insertAsFirstChildOf($dest);
512                     break;
513                     case 'moveAsNextSiblingOf':
514                         $this->insertAsNextSiblingOf($dest);
515                     break;
516                     case 'moveAsLastChildOf':
517                         $this->insertAsLastChildOf($dest);
518                     break;
519                     default:
520                         throw new Exception("Unknown move operation: $moveType.");
521                 }
522                 
523                 $diff = $oldRgt - $oldLft;
524                 $this->setRightValue($this->getLeftValue() + ($oldRgt - $oldLft));
525                 $this->record->save();
526                 
527                 $newLevel = $this->record['level'];
528                 $levelDiff = $newLevel - $oldLevel;
529                 
530                 // Relocate descendants of the node
531                 $diff = $this->getLeftValue() - $oldLft;
532                 $componentName = $this->_tree->getBaseComponent();
533                 $rootColName = $this->record->getTable()->getTree()->getAttribute('rootColumnName');
534
535                 // Update lft/rgt/root/level for all descendants
536                 $q = new Doctrine_Query($conn);
537                 $q = $q->update($componentName)
538                         ->set($componentName . '.lft', 'lft + ?', $diff)
539                         ->set($componentName . '.rgt', 'rgt + ?', $diff)
540                         ->set($componentName . '.level', 'level + ?', $levelDiff)
541                         ->set($componentName . '.' . $rootColName, '?', $newRoot)
542                         ->where($componentName . '.lft > ? AND ' . $componentName . '.rgt < ?',
543                         array($oldLft, $oldRgt));
544                 $q = $this->_tree->returnQueryWithRootId($q, $oldRoot);
545                 $q->execute();
546                 
547                 $conn->commit();
548             } catch (Exception $e) {
549                 $conn->rollback();
550                 throw $e;
551             }
552     }
553     
554     /**
555      * moves node as prev sibling of dest record
556      * 
557      */     
558     public function moveAsPrevSiblingOf(Doctrine_Record $dest)
559     {
560         if ($dest->getNode()->getRootValue() != $this->getRootValue()) {
561             // Move between trees
562             $this->_moveBetweenTrees($dest, $dest->getNode()->getLeftValue(), __FUNCTION__);
563         } else {
564             // Move within the tree
565             $oldLevel = $this->record['level'];
566             $this->record['level'] = $dest['level'];
567             $this->updateNode($dest->getNode()->getLeftValue(), $this->record['level'] - $oldLevel);
568         }
569     }
570
571     /**
572      * moves node as next sibling of dest record
573      *        
574      */
575     public function moveAsNextSiblingOf(Doctrine_Record $dest)
576     {
577         if ($dest->getNode()->getRootValue() != $this->getRootValue()) {
578             // Move between trees
579             $this->_moveBetweenTrees($dest, $dest->getNode()->getRightValue() + 1, __FUNCTION__);
580         } else {
581             // Move within tree
582             $oldLevel = $this->record['level'];
583             $this->record['level'] = $dest['level'];
584             $this->updateNode($dest->getNode()->getRightValue() + 1, $this->record['level'] - $oldLevel);
585         }
586     }
587
588     /**
589      * moves node as first child of dest record
590      *            
591      */
592     public function moveAsFirstChildOf(Doctrine_Record $dest)
593     {
594         if ($dest->getNode()->getRootValue() != $this->getRootValue()) {
595             // Move between trees
596             $this->_moveBetweenTrees($dest, $dest->getNode()->getLeftValue() + 1, __FUNCTION__);
597         } else {
598             // Move within tree
599             $oldLevel = $this->record['level'];
600             $this->record['level'] = $dest['level'] + 1;
601             $this->updateNode($dest->getNode()->getLeftValue() + 1, $this->record['level'] - $oldLevel);
602         }
603     }
604
605     /**
606      * moves node as last child of dest record
607      *        
608      */
609     public function moveAsLastChildOf(Doctrine_Record $dest)
610     {
611         if ($dest->getNode()->getRootValue() != $this->getRootValue()) {
612             // Move between trees
613             $this->_moveBetweenTrees($dest, $dest->getNode()->getRightValue(), __FUNCTION__);
614         } else {
615             // Move within tree
616             $oldLevel = $this->record['level'];
617             $this->record['level'] = $dest['level'] + 1;
618             $this->updateNode($dest->getNode()->getRightValue(), $this->record['level'] - $oldLevel);
619         }
620     }
621     
622     /**
623      * Makes this node a root node. Only used in multiple-root trees.
624      *
625      * @todo Exception handling/wrapping
626      */
627     public function makeRoot($newRootId)
628     {
629         // TODO: throw exception instead?
630         if ($this->getLeftValue() == 1 || !$this->record->getTable()->getTree()->getAttribute('hasManyRoots')) {
631             return false;
632         }
633         
634         $oldRgt = $this->getRightValue();
635         $oldLft = $this->getLeftValue();
636         $oldRoot = $this->getRootValue();
637         $oldLevel = $this->record['level'];
638         
639         try {
640             $conn = $this->record->getTable()->getConnection();
641             $conn->beginTransaction();
642             
643             // Detach from old tree (close gap in old tree)
644             $first = $oldRgt + 1;
645             $delta = $oldLft - $oldRgt - 1;
646             $this->shiftRLValues($first, $delta, $this->getRootValue());
647             
648             // Set new lft/rgt/root/level values for root node
649             $this->setLeftValue(1);
650             $this->setRightValue($oldRgt - $oldLft + 1);
651             $this->setRootValue($newRootId);
652             $this->record['level'] = 0;
653             
654             // Update descendants lft/rgt/root/level values
655             $diff = 1 - $oldLft;
656             $newRoot = $newRootId;
657             $componentName = $this->_tree->getBaseComponent();
658             $rootColName = $this->record->getTable()->getTree()->getAttribute('rootColumnName');
659             $q = new Doctrine_Query($conn);
660             $q = $q->update($componentName)
661                     ->set($componentName . '.lft', 'lft + ?', $diff)
662                     ->set($componentName . '.rgt', 'rgt + ?', $diff)
663                     ->set($componentName . '.level', 'level - ?', $oldLevel)
664                     ->set($componentName . '.' . $rootColName, '?', $newRoot)
665                     ->where($componentName . '.lft > ? AND ' . $componentName . '.rgt < ?',
666                     array($oldLft, $oldRgt));
667             $q = $this->_tree->returnQueryWithRootId($q, $oldRoot);
668             $q->execute();
669             
670             $conn->commit();
671             
672         } catch (Exception $e) {
673             $conn->rollback();
674             throw $e;
675         }
676     }
677
678     /**
679      * adds node as last child of record
680      *        
681      */
682     public function addChild(Doctrine_Record $record)
683     {
684         $record->getNode()->insertAsLastChildOf($this->getRecord());
685     }
686
687     /**
688      * determines if node is leaf
689      *
690      * @return bool            
691      */
692     public function isLeaf()
693     {
694         return (($this->getRightValue() - $this->getLeftValue()) == 1);
695     }
696
697     /**
698      * determines if node is root
699      *
700      * @return bool            
701      */
702     public function isRoot()
703     {
704         return ($this->getLeftValue() == 1);
705     }
706
707     /**
708      * determines if node is equal to subject node
709      *
710      * @return bool            
711      */    
712     public function isEqualTo(Doctrine_Record $subj)
713     {
714         return (($this->getLeftValue() == $subj->getNode()->getLeftValue()) &&
715                 ($this->getRightValue() == $subj->getNode()->getRightValue()) && 
716                 ($this->getRootValue() == $subj->getNode()->getRootValue())
717                 );
718     }
719
720     /**
721      * determines if node is child of subject node
722      *
723      * @return bool
724      */
725     public function isDescendantOf(Doctrine_Record $subj)
726     {
727         return (($this->getLeftValue() > $subj->getNode()->getLeftValue()) &&
728                 ($this->getRightValue() < $subj->getNode()->getRightValue()) &&
729                 ($this->getRootValue() == $subj->getNode()->getRootValue()));
730     }
731
732     /**
733      * determines if node is child of or sibling to subject node
734      *
735      * @return bool            
736      */
737     public function isDescendantOfOrEqualTo(Doctrine_Record $subj)
738     {
739         return (($this->getLeftValue() >= $subj->getNode()->getLeftValue()) &&
740                 ($this->getRightValue() <= $subj->getNode()->getRightValue()) &&
741                 ($this->getRootValue() == $subj->getNode()->getRootValue()));
742     }
743
744     /**
745      * determines if node is valid
746      *
747      * @return bool            
748      */
749     public function isValidNode($record = null)
750     {
751         if ($record === null) {
752           return ($this->getRightValue() > $this->getLeftValue());
753         } else if ( $record instanceof Doctrine_Record ) {
754           return ($record->getNode()->getRightValue() > $record->getNode()->getLeftValue());
755         } else {
756           return false;
757         }
758     }
759
760     /**
761      * deletes node and it's descendants
762      * @todo Delete more efficiently. Wrap in transaction if needed.      
763      */
764     public function delete()
765     {
766         // TODO: add the setting whether or not to delete descendants or relocate children
767         $oldRoot = $this->getRootValue();
768         $q = $this->_tree->getBaseQuery();
769         
770         $baseAlias = $this->_tree->getBaseAlias();
771         $componentName = $this->_tree->getBaseComponent();
772
773         $q = $q->addWhere("$baseAlias.lft >= ? AND $baseAlias.rgt <= ?", array($this->getLeftValue(), $this->getRightValue()));
774
775         $q = $this->record->getTable()->getTree()->returnQueryWithRootId($q, $oldRoot);
776         
777         $coll = $q->execute();
778
779         $coll->delete();
780
781         $first = $this->getRightValue() + 1;
782         $delta = $this->getLeftValue() - $this->getRightValue() - 1;
783         $this->shiftRLValues($first, $delta, $oldRoot);
784         
785         return true; 
786     }
787
788     /**
789      * sets node's left and right values and save's it
790      *
791      * @param int     $destLeft     node left value
792      * @param int        $destRight    node right value
793      */    
794     private function insertNode($destLeft = 0, $destRight = 0, $destRoot = 1)
795     {
796         $this->setLeftValue($destLeft);
797         $this->setRightValue($destRight);
798         $this->setRootValue($destRoot);
799         $this->record->save();    
800     }
801
802     /**
803      * move node's and its children to location $destLeft and updates rest of tree
804      *
805      * @param int     $destLeft    destination left value
806      * @todo Wrap in transaction
807      */
808     private function updateNode($destLeft, $levelDiff)
809     { 
810         $componentName = $this->_tree->getBaseComponent();
811         $left = $this->getLeftValue();
812         $right = $this->getRightValue();
813         $rootId = $this->getRootValue();
814
815         $treeSize = $right - $left + 1;
816
817         // Make room in the new branch
818         $this->shiftRLValues($destLeft, $treeSize, $rootId);
819
820         if ($left >= $destLeft) { // src was shifted too?
821             $left += $treeSize;
822             $right += $treeSize;
823         }
824
825         // update level for descendants
826         $q = new Doctrine_Query();
827         $q = $q->update($componentName)
828                 ->set($componentName . '.level', 'level + ?')
829                 ->where($componentName . '.lft > ? AND ' . $componentName . '.rgt < ?',
830                         array($levelDiff, $left, $right));
831         $q = $this->_tree->returnQueryWithRootId($q, $rootId);
832         $q->execute();
833         
834         // now there's enough room next to target to move the subtree
835         $this->shiftRLRange($left, $right, $destLeft - $left, $rootId);
836
837         // correct values after source (close gap in old tree)
838         $this->shiftRLValues($right + 1, -$treeSize, $rootId);
839
840         $this->record->save();
841         $this->record->refresh();
842     }
843
844     /**
845      * adds '$delta' to all Left and Right values that are >= '$first'. '$delta' can also be negative.
846      *
847      * @param int $first         First node to be shifted
848      * @param int $delta         Value to be shifted by, can be negative
849      */    
850     private function shiftRlValues($first, $delta, $rootId = 1)
851     {
852         $qLeft  = new Doctrine_Query();
853         $qRight = new Doctrine_Query();
854
855         // shift left columns
856         $componentName = $this->_tree->getBaseComponent();
857         $qLeft = $qLeft->update($componentName)
858                                 ->set($componentName . '.lft', 'lft + ?')
859                                 ->where($componentName . '.lft >= ?', array($delta, $first));
860         
861         $qLeft = $this->record->getTable()->getTree()->returnQueryWithRootId($qLeft, $rootId);
862         
863         $resultLeft = $qLeft->execute();
864         
865         // shift right columns
866         $resultRight = $qRight->update($componentName)
867                                 ->set($componentName . '.rgt', 'rgt + ?')
868                                 ->where($componentName . '.rgt >= ?', array($delta, $first));
869
870         $qRight = $this->record->getTable()->getTree()->returnQueryWithRootId($qRight, $rootId);
871
872         $resultRight = $qRight->execute();
873     }
874
875     /**
876      * adds '$delta' to all Left and Right values that are >= '$first' and <= '$last'. 
877      * '$delta' can also be negative.
878      *
879      * @param int $first     First node to be shifted (L value)
880      * @param int $last     Last node to be shifted (L value)
881      * @param int $delta         Value to be shifted by, can be negative
882      */ 
883     private function shiftRlRange($first, $last, $delta, $rootId = 1)
884     {
885         $qLeft  = new Doctrine_Query();
886         $qRight = new Doctrine_Query();
887
888         // shift left column values
889         $componentName = $this->_tree->getBaseComponent();
890         $qLeft = $qLeft->update($componentName)
891                                 ->set($componentName . '.lft', 'lft + ?')
892                                 ->where($componentName . '.lft >= ? AND ' . $componentName . '.lft <= ?', array($delta, $first, $last));
893         
894         $qLeft = $this->record->getTable()->getTree()->returnQueryWithRootId($qLeft, $rootId);
895
896         $resultLeft = $qLeft->execute();
897         
898         // shift right column values
899         $qRight = $qRight->update($componentName)
900                                 ->set($componentName . '.rgt', 'rgt + ?')
901                                 ->where($componentName . '.rgt >= ? AND ' . $componentName . '.rgt <= ?', array($delta, $first, $last));
902
903         $qRight = $this->record->getTable()->getTree()->returnQueryWithRootId($qRight, $rootId);
904
905         $resultRight = $qRight->execute();
906     }
907     
908     /**
909      * gets record's left value
910      *
911      * @return int            
912      */     
913     public function getLeftValue()
914     {
915         return $this->record->get('lft');
916     }
917
918     /**
919      * sets record's left value
920      *
921      * @param int            
922      */     
923     public function setLeftValue($lft)
924     {
925         $this->record->set('lft', $lft);        
926     }
927
928     /**
929      * gets record's right value
930      *
931      * @return int            
932      */     
933     public function getRightValue()
934     {
935         return $this->record->get('rgt');        
936     }
937
938     /**
939      * sets record's right value
940      *
941      * @param int            
942      */    
943     public function setRightValue($rgt)
944     {
945         $this->record->set('rgt', $rgt);         
946     }
947
948     /**
949      * gets level (depth) of node in the tree
950      *
951      * @return int            
952      */    
953     public function getLevel()
954     {
955         if ( ! isset($this->record['level'])) {
956             $baseAlias = $this->_tree->getBaseAlias();
957             $componentName = $this->_tree->getBaseComponent();
958             $q = $this->_tree->getBaseQuery();
959             $q = $q->addWhere("$baseAlias.lft < ? AND $baseAlias.rgt > ?", array($this->getLeftValue(), $this->getRightValue()));
960
961             $q = $this->_tree->returnQueryWithRootId($q, $this->getRootValue());
962             
963             $coll = $q->execute();
964
965             $this->record['level'] = count($coll) ? count($coll) : 0;
966         }
967         return $this->record['level'];
968     }
969
970     /**
971      * get records root id value
972      *            
973      */     
974     public function getRootValue()
975     {
976         if ($this->_tree->getAttribute('hasManyRoots')) {
977             return $this->record->get($this->_tree->getAttribute('rootColumnName'));
978         }
979         return 1;
980     }
981
982     /**
983      * sets records root id value
984      *
985      * @param int            
986      */
987     public function setRootValue($value)
988     {
989         if ($this->_tree->getAttribute('hasManyRoots')) {
990             $this->record->set($this->_tree->getAttribute('rootColumnName'), $value);   
991         }    
992     }
993 }