Coverage for Doctrine_Node_NestedSet

Back to coverage report

1 <?php
2 /*
3  *    $Id: NestedSet.php 4089 2008-03-22 20:30:40Z romanb $
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.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.phpdoctrine.org
29  * @since      1.0
30  * @version    $Revision: 4089 $
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->isRoot();
74     }
75
76     /**
77      * gets record of prev sibling or empty record
78      *
79      * @return object     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 object     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->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 object     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 object     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      * @todo Currently all descendants are fetched, no matter the depth. Maybe there is a better
215      *       solution with less overhead.      
216      */
217     public function getDescendants($depth = null, $includeNode = false)
218     {
219         $baseAlias = $this->_tree->getBaseAlias();
220         $q = $this->_tree->getBaseQuery();
221         $params = array($this->record->get('lft'), $this->record->get('rgt'));
222         
223         if ($includeNode) {
224             $q->addWhere("$baseAlias.lft >= ? AND $baseAlias.rgt <= ?", $params)->addOrderBy("$baseAlias.lft asc");
225         } else {
226             $q->addWhere("$baseAlias.lft > ? AND $baseAlias.rgt < ?", $params)->addOrderBy("$baseAlias.lft asc");
227         }
228         
229         if ($depth !== null) {
230             $q->addWhere("$baseAlias.level <= ?", $this->record['level'] + $depth);
231         }
232         
233         $q = $this->_tree->returnQueryWithRootId($q, $this->getRootValue());
234         $result = $q->execute();
235
236         if (count($result) <= 0) {
237             return false;
238         }
239
240         return $result;
241     }
242
243     /**
244      * gets record of parent or empty record
245      *
246      * @return object     Doctrine_Record            
247      */
248     public function getParent()
249     {
250         $baseAlias = $this->_tree->getBaseAlias();
251         $q = $this->_tree->getBaseQuery();
252         $q->addWhere("$baseAlias.lft < ? AND $baseAlias.rgt > ?", array($this->getLeftValue(), $this->getRightValue()))
253                 ->addOrderBy("$baseAlias.rgt asc");
254         $q = $this->_tree->returnQueryWithRootId($q, $this->getRootValue());
255         $result = $q->execute();
256         
257         if (count($result) <= 0) {
258             return false;
259         }
260                
261         if ($result instanceof Doctrine_Collection) {
262             $parent = $result->getFirst();
263         } else if (is_array($result)) {
264             $parent = array_shift($result);
265         }
266         
267         return $parent;
268     }
269
270     /**
271      * gets ancestors for node
272      *
273      * @param integer $deth  The depth 'upstairs'.
274      * @return mixed  The ancestors of the node or FALSE if the node has no ancestors (this 
275      *                basically means it's a root node).                
276      */
277     public function getAncestors($depth = null)
278     {
279         $baseAlias = $this->_tree->getBaseAlias();
280         $q = $this->_tree->getBaseQuery();
281         $q->addWhere("$baseAlias.lft < ? AND $baseAlias.rgt > ?", array($this->getLeftValue(), $this->getRightValue()))
282                 ->addOrderBy("$baseAlias.lft asc");
283         if ($depth !== null) {
284             $q->addWhere("$baseAlias.level >= ?", $this->record['level'] - $depth);
285         }
286         $q = $this->_tree->returnQueryWithRootId($q, $this->getRootValue());
287         $ancestors = $q->execute();
288         if (count($ancestors) <= 0) {
289             return false;
290         }
291         return $ancestors;
292     }
293
294     /**
295      * gets path to node from root, uses record::toString() method to get node names
296      *
297      * @param string     $seperator     path seperator
298      * @param bool     $includeNode     whether or not to include node at end of path
299      * @return string     string representation of path                
300      */     
301     public function getPath($seperator = ' > ', $includeRecord = false)
302     {
303         $path = array();
304         $ancestors = $this->getAncestors();
305         if ($ancestors) {
306             foreach ($ancestors as $ancestor) {
307                 $path[] = $ancestor->__toString();
308             }
309         }
310         if ($includeRecord) {
311             $path[] = $this->getRecord()->__toString();
312         }
313             
314         return implode($seperator, $path);
315     }
316
317     /**
318      * gets number of children (direct descendants)
319      *
320      * @return int            
321      */     
322     public function getNumberChildren()
323     {
324         return count($this->getChildren());
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         $newRoot = $dest->getNode()->getRootValue();
354         $this->shiftRLValues($dest->getNode()->getLeftValue(), 1, $newRoot);
355         $this->shiftRLValues($dest->getNode()->getRightValue() + 2, 1, $newRoot);
356         
357         $newLeft = $dest->getNode()->getLeftValue();
358         $newRight = $dest->getNode()->getRightValue() + 2;
359
360         $this->record['level'] = $dest['level'] - 1;
361         $this->insertNode($newLeft, $newRight, $newRoot);
362         
363         return true;
364     }
365
366     /**
367      * inserts node as previous sibling of dest record
368      *
369      * @return bool
370      * @todo Wrap in transaction       
371      */
372     public function insertAsPrevSiblingOf(Doctrine_Record $dest)
373     {
374         // cannot insert a node that has already has a place within the tree
375         if ($this->isValidNode())
376             return false;
377
378         $newLeft = $dest->getNode()->getLeftValue();
379         $newRight = $dest->getNode()->getLeftValue() + 1;
380         $newRoot = $dest->getNode()->getRootValue();
381         
382         $this->shiftRLValues($newLeft, 2, $newRoot);
383         $this->record['level'] = $dest['level'];
384         $this->insertNode($newLeft, $newRight, $newRoot);
385         // update destination left/right values to prevent a refresh
386         // $dest->getNode()->setLeftValue($dest->getNode()->getLeftValue() + 2);
387         // $dest->getNode()->setRightValue($dest->getNode()->getRightValue() + 2);
388                         
389         return true;
390     }
391
392     /**
393      * inserts node as next sibling of dest record
394      *
395      * @return bool
396      * @todo Wrap in transaction           
397      */    
398     public function insertAsNextSiblingOf(Doctrine_Record $dest)
399     {
400         // cannot insert a node that has already has a place within the tree
401         if ($this->isValidNode())
402             return false;
403
404         $newLeft = $dest->getNode()->getRightValue() + 1;
405         $newRight = $dest->getNode()->getRightValue() + 2;
406         $newRoot = $dest->getNode()->getRootValue();
407
408         $this->shiftRLValues($newLeft, 2, $newRoot);
409         $this->record['level'] = $dest['level'];
410         $this->insertNode($newLeft, $newRight, $newRoot);
411
412         // update destination left/right values to prevent a refresh
413         // no need, node not affected
414
415         return true;
416     }
417
418     /**
419      * inserts node as first child of dest record
420      *
421      * @return bool
422      * @todo Wrap in transaction         
423      */
424     public function insertAsFirstChildOf(Doctrine_Record $dest)
425     {
426         // cannot insert a node that has already has a place within the tree
427         if ($this->isValidNode())
428             return false;
429
430         $newLeft = $dest->getNode()->getLeftValue() + 1;
431         $newRight = $dest->getNode()->getLeftValue() + 2;
432         $newRoot = $dest->getNode()->getRootValue();
433
434         $this->shiftRLValues($newLeft, 2, $newRoot);
435         $this->record['level'] = $dest['level'] + 1;
436         $this->insertNode($newLeft, $newRight, $newRoot);
437         
438         // update destination left/right values to prevent a refresh
439         // $dest->getNode()->setRightValue($dest->getNode()->getRightValue() + 2);
440
441         return true;
442     }
443
444     /**
445      * inserts node as last child of dest record
446      *
447      * @return bool
448      * @todo Wrap in transaction            
449      */
450     public function insertAsLastChildOf(Doctrine_Record $dest)
451     {
452         // cannot insert a node that has already has a place within the tree
453         if ($this->isValidNode())
454             return false;
455
456         $newLeft = $dest->getNode()->getRightValue();
457         $newRight = $dest->getNode()->getRightValue() + 1;
458         $newRoot = $dest->getNode()->getRootValue();
459
460         $this->shiftRLValues($newLeft, 2, $newRoot);
461         $this->record['level'] = $dest['level'] + 1;
462         $this->insertNode($newLeft, $newRight, $newRoot);
463
464         // update destination left/right values to prevent a refresh
465         // $dest->getNode()->setRightValue($dest->getNode()->getRightValue() + 2);
466         
467         return true;
468     }
469
470     /**
471      * Accomplishes moving of nodes between different trees.
472      * Used by the move* methods if the root values of the two nodes are different.
473      *
474      * @param Doctrine_Record $dest
475      * @param unknown_type $newLeftValue
476      * @param unknown_type $moveType
477      * @todo Better exception handling/wrapping
478      */
479     private function _moveBetweenTrees(Doctrine_Record $dest, $newLeftValue, $moveType)
480     {
481         $conn = $this->record->getTable()->getConnection();
482             
483             try {
484                 $conn->beginTransaction();
485                 
486                 // Move between trees: Detach from old tree & insert into new tree
487                 $newRoot = $dest->getNode()->getRootValue();
488                 $oldRoot = $this->getRootValue();
489                 $oldLft = $this->getLeftValue();
490                 $oldRgt = $this->getRightValue();
491                 $oldLevel = $this->record['level'];
492                 
493                 // Prepare target tree for insertion, make room
494                 $this->shiftRlValues($newLeftValue, $oldRgt - $oldLft - 1, $newRoot);
495                 
496                 // Set new root id for this node
497                 $this->setRootValue($newRoot);
498                 $this->record->save();
499                 
500                 // Close gap in old tree
501                 $first = $oldRgt + 1;
502                 $delta = $oldLft - $oldRgt - 1;
503                 $this->shiftRLValues($first, $delta, $oldRoot);
504                 
505                 // Insert this node as a new node
506                 $this->setRightValue(0);
507                 $this->setLeftValue(0);
508                 
509                 switch ($moveType) {
510                     case 'moveAsPrevSiblingOf':
511                         $this->insertAsPrevSiblingOf($dest);
512                     break;
513                     case 'moveAsFirstChildOf':
514                         $this->insertAsFirstChildOf($dest);
515                     break;
516                     case 'moveAsNextSiblingOf':
517                         $this->insertAsNextSiblingOf($dest);
518                     break;
519                     case 'moveAsLastChildOf':
520                         $this->insertAsLastChildOf($dest);
521                     break;
522                     default:
523                         throw new Exception("Unknown move operation: $moveType.");
524                 }
525                 
526                 $diff = $oldRgt - $oldLft;
527                 $this->setRightValue($this->getLeftValue() + ($oldRgt - $oldLft));
528                 $this->record->save();
529                 
530                 $newLevel = $this->record['level'];
531                 $levelDiff = $newLevel - $oldLevel;
532                 
533                 // Relocate descendants of the node
534                 $diff = $this->getLeftValue() - $oldLft;
535                 $componentName = $this->_tree->getBaseComponent();
536                 $rootColName = $this->_tree->getAttribute('rootColumnName');
537
538                 // Update lft/rgt/root/level for all descendants
539                 $q = new Doctrine_Query($conn);
540                 $q = $q->update($componentName)
541                         ->set($componentName . '.lft', 'lft + ?', $diff)
542                         ->set($componentName . '.rgt', 'rgt + ?', $diff)
543                         ->set($componentName . '.level', 'level + ?', $levelDiff)
544                         ->set($componentName . '.' . $rootColName, '?', $newRoot)
545                         ->where($componentName . '.lft > ? AND ' . $componentName . '.rgt < ?',
546                         array($oldLft, $oldRgt));
547                 $q = $this->_tree->returnQueryWithRootId($q, $oldRoot);
548                 $q->execute();
549                 
550                 $conn->commit();
551             } catch (Exception $e) {
552                 $conn->rollback();
553                 throw $e;
554             }
555     }
556
557     /**
558      * moves node as prev sibling of dest record
559      * 
560      */     
561     public function moveAsPrevSiblingOf(Doctrine_Record $dest)
562     {
563         if ($dest->getNode()->getRootValue() != $this->getRootValue()) {
564             // Move between trees
565             $this->_moveBetweenTrees($dest, $dest->getNode()->getLeftValue(), __FUNCTION__);
566         } else {
567             // Move within the tree
568             $oldLevel = $this->record['level'];
569             $this->record['level'] = $dest['level'];
570             $this->updateNode($dest->getNode()->getLeftValue(), $this->record['level'] - $oldLevel);
571         }
572     }
573
574     /**
575      * moves node as next sibling of dest record
576      *        
577      */
578     public function moveAsNextSiblingOf(Doctrine_Record $dest)
579     {
580         if ($dest->getNode()->getRootValue() != $this->getRootValue()) {
581             // Move between trees
582             $this->_moveBetweenTrees($dest, $dest->getNode()->getRightValue() + 1, __FUNCTION__);
583         } else {
584             // Move within tree
585             $oldLevel = $this->record['level'];
586             $this->record['level'] = $dest['level'];
587             $this->updateNode($dest->getNode()->getRightValue() + 1, $this->record['level'] - $oldLevel);
588         }
589     }
590
591     /**
592      * moves node as first child of dest record
593      *            
594      */
595     public function moveAsFirstChildOf(Doctrine_Record $dest)
596     {
597         if ($dest->getNode()->getRootValue() != $this->getRootValue()) {
598             // Move between trees
599             $this->_moveBetweenTrees($dest, $dest->getNode()->getLeftValue() + 1, __FUNCTION__);
600         } else {
601             // Move within tree
602             $oldLevel = $this->record['level'];
603             $this->record['level'] = $dest['level'] + 1;
604             $this->updateNode($dest->getNode()->getLeftValue() + 1, $this->record['level'] - $oldLevel);
605         }
606     }
607
608     /**
609      * moves node as last child of dest record
610      *        
611      */
612     public function moveAsLastChildOf(Doctrine_Record $dest)
613     {
614         if ($dest->getNode()->getRootValue() != $this->getRootValue()) {
615             // Move between trees
616             $this->_moveBetweenTrees($dest, $dest->getNode()->getRightValue(), __FUNCTION__);
617         } else {
618             // Move within tree
619             $oldLevel = $this->record['level'];
620             $this->record['level'] = $dest['level'] + 1;
621             $this->updateNode($dest->getNode()->getRightValue(), $this->record['level'] - $oldLevel);
622         }
623     }
624
625     /**
626      * Makes this node a root node. Only used in multiple-root trees.
627      *
628      * @todo Exception handling/wrapping
629      */
630     public function makeRoot($newRootId)
631     {
632         // TODO: throw exception instead?
633         if ($this->getLeftValue() == 1 || ! $this->_tree->getAttribute('hasManyRoots')) {
634             return false;
635         }
636         
637         $oldRgt = $this->getRightValue();
638         $oldLft = $this->getLeftValue();
639         $oldRoot = $this->getRootValue();
640         $oldLevel = $this->record['level'];
641         
642         try {
643             $conn = $this->record->getTable()->getConnection();
644             $conn->beginTransaction();
645             
646             // Detach from old tree (close gap in old tree)
647             $first = $oldRgt + 1;
648             $delta = $oldLft - $oldRgt - 1;
649             $this->shiftRLValues($first, $delta, $this->getRootValue());
650             
651             // Set new lft/rgt/root/level values for root node
652             $this->setLeftValue(1);
653             $this->setRightValue($oldRgt - $oldLft + 1);
654             $this->setRootValue($newRootId);
655             $this->record['level'] = 0;
656             
657             // Update descendants lft/rgt/root/level values
658             $diff = 1 - $oldLft;
659             $newRoot = $newRootId;
660             $componentName = $this->_tree->getBaseComponent();
661             $rootColName = $this->_tree->getAttribute('rootColumnName');
662             $q = new Doctrine_Query($conn);
663             $q = $q->update($componentName)
664                     ->set($componentName . '.lft', 'lft + ?', $diff)
665                     ->set($componentName . '.rgt', 'rgt + ?', $diff)
666                     ->set($componentName . '.level', 'level - ?', $oldLevel)
667                     ->set($componentName . '.' . $rootColName, '?', $newRoot)
668                     ->where($componentName . '.lft > ? AND ' . $componentName . '.rgt < ?',
669                     array($oldLft, $oldRgt));
670             $q = $this->_tree->returnQueryWithRootId($q, $oldRoot);
671             $q->execute();
672             
673             $conn->commit();
674             
675         } catch (Exception $e) {
676             $conn->rollback();
677             throw $e;
678         }
679     }
680
681     /**
682      * adds node as last child of record
683      *        
684      */
685     public function addChild(Doctrine_Record $record)
686     {
687         $record->getNode()->insertAsLastChildOf($this->getRecord());
688     }
689
690     /**
691      * determines if node is leaf
692      *
693      * @return bool            
694      */
695     public function isLeaf()
696     {
697         return (($this->getRightValue() - $this->getLeftValue()) == 1);
698     }
699
700     /**
701      * determines if node is root
702      *
703      * @return bool            
704      */
705     public function isRoot()
706     {
707         return ($this->getLeftValue() == 1);
708     }
709
710     /**
711      * determines if node is equal to subject node
712      *
713      * @return bool            
714      */    
715     public function isEqualTo(Doctrine_Record $subj)
716     {
717         return (($this->getLeftValue() == $subj->getNode()->getLeftValue()) &&
718                 ($this->getRightValue() == $subj->getNode()->getRightValue()) && 
719                 ($this->getRootValue() == $subj->getNode()->getRootValue())
720                 );
721     }
722
723     /**
724      * determines if node is child of subject node
725      *
726      * @return bool
727      */
728     public function isDescendantOf(Doctrine_Record $subj)
729     {
730         return (($this->getLeftValue() > $subj->getNode()->getLeftValue()) &&
731                 ($this->getRightValue() < $subj->getNode()->getRightValue()) &&
732                 ($this->getRootValue() == $subj->getNode()->getRootValue()));
733     }
734
735     /**
736      * determines if node is child of or sibling to subject node
737      *
738      * @return bool            
739      */
740     public function isDescendantOfOrEqualTo(Doctrine_Record $subj)
741     {
742         return (($this->getLeftValue() >= $subj->getNode()->getLeftValue()) &&
743                 ($this->getRightValue() <= $subj->getNode()->getRightValue()) &&
744                 ($this->getRootValue() == $subj->getNode()->getRootValue()));
745     }
746
747     /**
748      * determines if node is valid
749      *
750      * @return bool
751      */
752     public function isValidNode($record = null)
753     {
754         if ($record === null) {
755           return ($this->getRightValue() > $this->getLeftValue());
756         } else if ( $record instanceof Doctrine_Record ) {
757           return ($record->getNode()->getRightValue() > $record->getNode()->getLeftValue());
758         } else {
759           return false;
760         }
761     }
762     
763     /**
764      * Detaches the node from the tree by invalidating it's lft & rgt values
765      * (they're set to 0).
766      */
767     public function detach()
768     {
769         $this->setLeftValue(0);
770         $this->setRightValue(0);
771     }
772
773     /**
774      * deletes node and it's descendants
775      * @todo Delete more efficiently. Wrap in transaction if needed.      
776      */
777     public function delete()
778     {
779         // TODO: add the setting whether or not to delete descendants or relocate children
780         $oldRoot = $this->getRootValue();
781         $q = $this->_tree->getBaseQuery();
782         
783         $baseAlias = $this->_tree->getBaseAlias();
784         $componentName = $this->_tree->getBaseComponent();
785
786         $q = $q->addWhere("$baseAlias.lft >= ? AND $baseAlias.rgt <= ?", array($this->getLeftValue(), $this->getRightValue()));
787
788         $q = $this->_tree->returnQueryWithRootId($q, $oldRoot);
789         
790         $coll = $q->execute();
791
792         $coll->delete();
793
794         $first = $this->getRightValue() + 1;
795         $delta = $this->getLeftValue() - $this->getRightValue() - 1;
796         $this->shiftRLValues($first, $delta, $oldRoot);
797         
798         return true; 
799     }
800
801     /**
802      * sets node's left and right values and save's it
803      *
804      * @param int     $destLeft     node left value
805      * @param int        $destRight    node right value
806      */    
807     private function insertNode($destLeft = 0, $destRight = 0, $destRoot = 1)
808     {
809         $this->setLeftValue($destLeft);
810         $this->setRightValue($destRight);
811         $this->setRootValue($destRoot);
812         $this->record->save();    
813     }
814
815     /**
816      * move node's and its children to location $destLeft and updates rest of tree
817      *
818      * @param int     $destLeft    destination left value
819      * @todo Wrap in transaction
820      */
821     private function updateNode($destLeft, $levelDiff)
822     { 
823         $componentName = $this->_tree->getBaseComponent();
824         $left = $this->getLeftValue();
825         $right = $this->getRightValue();
826         $rootId = $this->getRootValue();
827
828         $treeSize = $right - $left + 1;
829
830         // Make room in the new branch
831         $this->shiftRLValues($destLeft, $treeSize, $rootId);
832
833         if ($left >= $destLeft) { // src was shifted too?
834             $left += $treeSize;
835             $right += $treeSize;
836         }
837
838         // update level for descendants
839         $q = new Doctrine_Query();
840         $q = $q->update($componentName)
841                 ->set($componentName . '.level', 'level + ?')
842                 ->where($componentName . '.lft > ? AND ' . $componentName . '.rgt < ?',
843                         array($levelDiff, $left, $right));
844         $q = $this->_tree->returnQueryWithRootId($q, $rootId);
845         $q->execute();
846         
847         // now there's enough room next to target to move the subtree
848         $this->shiftRLRange($left, $right, $destLeft - $left, $rootId);
849
850         // correct values after source (close gap in old tree)
851         $this->shiftRLValues($right + 1, -$treeSize, $rootId);
852
853         $this->record->save();
854         $this->record->refresh();
855     }
856
857     /**
858      * adds '$delta' to all Left and Right values that are >= '$first'. '$delta' can also be negative.
859      *
860      * @param int $first         First node to be shifted
861      * @param int $delta         Value to be shifted by, can be negative
862      */    
863     private function shiftRlValues($first, $delta, $rootId = 1)
864     {
865         $qLeft  = new Doctrine_Query();
866         $qRight = new Doctrine_Query();
867
868         // shift left columns
869         $componentName = $this->_tree->getBaseComponent();
870         $qLeft = $qLeft->update($componentName)
871                                 ->set($componentName . '.lft', 'lft + ?')
872                                 ->where($componentName . '.lft >= ?', array($delta, $first));
873         
874         $qLeft = $this->_tree->returnQueryWithRootId($qLeft, $rootId);
875         
876         $resultLeft = $qLeft->execute();
877         
878         // shift right columns
879         $resultRight = $qRight->update($componentName)
880                                 ->set($componentName . '.rgt', 'rgt + ?')
881                                 ->where($componentName . '.rgt >= ?', array($delta, $first));
882
883         $qRight = $this->_tree->returnQueryWithRootId($qRight, $rootId);
884
885         $resultRight = $qRight->execute();
886     }
887
888     /**
889      * adds '$delta' to all Left and Right values that are >= '$first' and <= '$last'. 
890      * '$delta' can also be negative.
891      *
892      * @param int $first     First node to be shifted (L value)
893      * @param int $last     Last node to be shifted (L value)
894      * @param int $delta         Value to be shifted by, can be negative
895      */ 
896     private function shiftRlRange($first, $last, $delta, $rootId = 1)
897     {
898         $qLeft  = new Doctrine_Query();
899         $qRight = new Doctrine_Query();
900
901         // shift left column values
902         $componentName = $this->_tree->getBaseComponent();
903         $qLeft = $qLeft->update($componentName)
904                                 ->set($componentName . '.lft', 'lft + ?')
905                                 ->where($componentName . '.lft >= ? AND ' . $componentName . '.lft <= ?', array($delta, $first, $last));
906         
907         $qLeft = $this->_tree->returnQueryWithRootId($qLeft, $rootId);
908
909         $resultLeft = $qLeft->execute();
910         
911         // shift right column values
912         $qRight = $qRight->update($componentName)
913                                 ->set($componentName . '.rgt', 'rgt + ?')
914                                 ->where($componentName . '.rgt >= ? AND ' . $componentName . '.rgt <= ?', array($delta, $first, $last));
915
916         $qRight = $this->_tree->returnQueryWithRootId($qRight, $rootId);
917
918         $resultRight = $qRight->execute();
919     }
920
921     /**
922      * gets record's left value
923      *
924      * @return int            
925      */     
926     public function getLeftValue()
927     {
928         return $this->record->get('lft');
929     }
930
931     /**
932      * sets record's left value
933      *
934      * @param int            
935      */     
936     public function setLeftValue($lft)
937     {
938         $this->record->set('lft', $lft);        
939     }
940
941     /**
942      * gets record's right value
943      *
944      * @return int            
945      */     
946     public function getRightValue()
947     {
948         return $this->record->get('rgt');        
949     }
950
951     /**
952      * sets record's right value
953      *
954      * @param int            
955      */    
956     public function setRightValue($rgt)
957     {
958         $this->record->set('rgt', $rgt);         
959     }
960
961     /**
962      * gets level (depth) of node in the tree
963      *
964      * @return int            
965      */    
966     public function getLevel()
967     {
968         if ( ! isset($this->record['level'])) {
969             $baseAlias = $this->_tree->getBaseAlias();
970             $componentName = $this->_tree->getBaseComponent();
971             $q = $this->_tree->getBaseQuery();
972             $q = $q->addWhere("$baseAlias.lft < ? AND $baseAlias.rgt > ?", array($this->getLeftValue(), $this->getRightValue()));
973
974             $q = $this->_tree->returnQueryWithRootId($q, $this->getRootValue());
975             
976             $coll = $q->execute();
977
978             $this->record['level'] = count($coll) ? count($coll) : 0;
979         }
980         return $this->record['level'];
981     }
982
983     /**
984      * get records root id value
985      *            
986      */     
987     public function getRootValue()
988     {
989         if ($this->_tree->getAttribute('hasManyRoots')) {
990             return $this->record->get($this->_tree->getAttribute('rootColumnName'));
991         }
992         return 1;
993     }
994
995     /**
996      * sets records root id value
997      *
998      * @param int            
999      */
1000     public function setRootValue($value)
1001     {
1002         if ($this->_tree->getAttribute('hasManyRoots')) {
1003             $this->record->set($this->_tree->getAttribute('rootColumnName'), $value);   
1004         }    
1005     }
1006 }