# 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.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: 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 | }