Coverage for Doctrine_Node_NestedSet

Back to coverage report

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