Source for file AkActsAsNestedSet.php

Documentation is available at AkActsAsNestedSet.php

  1. <?php
  2. /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
  3.  
  4. // +----------------------------------------------------------------------+
  5. // | Akelos Framework - http://www.akelos.org                             |
  6. // +----------------------------------------------------------------------+
  7. // | Copyright (c) 2002-2006, Akelos Media, S.L.  & Bermi Ferrer Martinez |
  8. // | Released under the GNU Lesser General Public License, see LICENSE.txt|
  9. // +----------------------------------------------------------------------+
  10.  
  11. /**
  12. * @package ActiveRecord
  13. * @subpackage Behaviours
  14.  * @author Bermi Ferrer <bermi a.t akelos c.om>
  15.  * @author Jean-Christophe Michel, Symétrie
  16.  * @copyright Copyright (c) 2002-2006, Akelos Media, S.L. http://www.akelos.org
  17.  * @license GNU Lesser General Public License <http://www.gnu.org/copyleft/lesser.html>
  18.  */
  19.  
  20. require_once(AK_LIB_DIR.DS.'AkActiveRecord'.DS.'AkObserver.php');
  21.  
  22. class AkActsAsNestedSet extends AkObserver
  23. {
  24.  
  25.     /**
  26.     * This acts provides Nested Set functionality.  Nested Set is similiar to Tree, but with
  27.     * the added feature that you can select the children and all of it's descendants with
  28.     * a single query.  A good use case for this is a threaded post system, where you want
  29.     * to display every reply to a comment without multiple selects.
  30.     *
  31.     * A google search for "Nested Set" should point you in the direction to explain the
  32.     * data base theory.  I figured a bunch of this from
  33.     * http://threebit.net/tutorials/nestedset/tutorial1.html
  34.     *
  35.     * Instead of picturing a leaf node structure with child pointing back to their parent,
  36.     * the best way to imagine how this works is to think of the parent entity surrounding all
  37.     * of it's children, and it's parent surrounding it, etc.  Assuming that they are lined up
  38.     * horizontally, we store the left and right boundaries in the database.
  39.     *
  40.     * Imagine:
  41.     *   root
  42.     *     |_ Child 1
  43.     *       |_ Child 1.1
  44.     *       |_ Child 1.2
  45.     *     |_ Child 2
  46.     *       |_ Child 2.1
  47.     *       |_ Child 2.2
  48.     *
  49.     * If my circles in circles description didn't make sense, check out this sweet
  50.     * ASCII art:
  51.     *
  52.     *     ___________________________________________________________________
  53.     *    |  Root                                                             |
  54.     *    |    ____________________________    ____________________________   |
  55.     *    |   |  Child 1                  |   |  Child 2                  |   |
  56.     *    |   |   __________   _________  |   |   __________   _________  |   |
  57.     *    |   |  |  C 1.1  |  |  C 1.2 |  |   |  |  C 2.1  |  |  C 2.2 |  |   |
  58.     *    1   2  3_________4  5________6  7   8  9_________10 11_______12 13  14
  59.     *    |   |___________________________|   |___________________________|   |
  60.     *    |___________________________________________________________________| 
  61.     *
  62.     * The numbers represent the left and right boundaries.  The table them might
  63.     * look like this:
  64.     *    ID | PARENT | LEFT | RIGHT | DATA
  65.     *     1 |      0 |    1 |    14 | root
  66.     *     2 |      1 |    2 |     7 | Child 1
  67.     *     3 |      2 |    3 |     4 | Child 1.1
  68.     *     4 |      2 |    5 |     6 | Child 1.2
  69.     *     5 |      1 |    8 |    13 | Child 2
  70.     *     6 |      5 |    9 |    10 | Child 2.1
  71.     *     7 |      5 |   11 |    12 | Child 2.2
  72.     *
  73.     * So, to get all children of an entry, you
  74.     *     SELECT * WHERE CHILD.LEFT IS BETWEEN PARENT.LEFT AND PARENT.RIGHT
  75.     *
  76.     * To get the count, it's (LEFT - RIGHT + 1)/2, etc.
  77.     *
  78.     * To get the direct parent, it falls back to using the PARENT_ID field.   
  79.     *
  80.     * There are instance methods for all of these.
  81.     *
  82.     * The structure is good if you need to group things together; the downside is that
  83.     * keeping data integrity is a pain, and both adding and removing and entry
  84.     * require a full table write.        
  85.     *
  86.     * This sets up a beforeDestroy() trigger to prune the tree correctly if one of it's
  87.     * elements gets deleted.
  88.     *
  89.     */
  90.  
  91.     /**
  92.     * Configuration options are:
  93.     *
  94.     * * +parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
  95.     * * +left_column+ - column name for left boundary data, default "lft"
  96.     * * +right_column+ - column name for right boundary data, default "rgt"
  97.     * * +scope+ - restricts what is to be considered a list.
  98.     *   Example: <tt>actsAsList(array('scope' => array('todo_list_id = ? AND completed = 0',$todo_list_id)));</tt>
  99.     */
  100.  
  101.     var $_scope_condition;
  102.     var $_left_column_name = 'lft';
  103.     var $_right_column_name = 'rgt';
  104.     var $_parent_column_name = 'parent_id';
  105.  
  106.  
  107.     function AkActsAsNestedSet(&$ActiveRecordInstance)
  108.     {
  109.         $this->_ActiveRecordInstance =$ActiveRecordInstance;
  110.     }
  111.  
  112.     function init($options = array())
  113.     {
  114.         empty($options['parent_column']? null : ($this->_parent_column_name = $options['parent_column']);
  115.         empty($options['left_column']? null : ($this->_left_column_name = $options['left_column']);
  116.         empty($options['right_column']? null : ($this->_right_column_name = $options['right_column']);
  117.         empty($options['scope']? null : $this->setScopeCondition($options['scope']);
  118.         return $this->_ensureIsActiveRecordInstance($this->_ActiveRecordInstance);
  119.     }
  120.  
  121.  
  122.     function _ensureIsActiveRecordInstance(&$ActiveRecordInstance)
  123.     {
  124.         if(is_object($ActiveRecordInstance&& method_exists($ActiveRecordInstance,'actsLike')){
  125.             $this->_ActiveRecordInstance =$ActiveRecordInstance;
  126.             if(!$this->_ActiveRecordInstance->hasColumn($this->_parent_column_name|| !$this->_ActiveRecordInstance->hasColumn($this->_left_column_name|| !$this->_ActiveRecordInstance->hasColumn($this->_right_column_name)){
  127.                 trigger_error(Ak::t(
  128.                 'The following columns are required in the table "%table" for the model "%model" to act as a Nested Set: "%columns".',array(
  129.                 '%columns'=>$this->getParentColumnName().', '.$this->getLeftColumnName().', '.$this->getRightColumnName(),'%table'=>$this->_ActiveRecordInstance->getTableName(),'%model'=>$this->_ActiveRecordInstance->getModelName())),E_USER_ERROR);
  130.                 unset($this->_ActiveRecordInstance->nested_set);
  131.                 return false;
  132.             }else{
  133.                 $this->observe(&$ActiveRecordInstance);
  134.             }
  135.         }else{
  136.             trigger_error(Ak::t('You are trying to set an object that is not an active record.')E_USER_ERROR);
  137.             return false;
  138.         }
  139.         return true;
  140.     }
  141.  
  142.     function reloadActiveRecordInstance(&$nodeInstance)
  143.     {
  144.         AK_PHP5 ? null : $nodeInstance->nested_set->_ensureIsActiveRecordInstance($nodeInstance);
  145.     }
  146.  
  147.     function getType()
  148.     {
  149.         return 'nested set';
  150.     }
  151.  
  152.     function getScopeCondition()
  153.     {
  154.         if (!empty($this->variable_scope_condition)){
  155.             return $this->_ActiveRecordInstance->_getVariableSqlCondition($this->variable_scope_condition);
  156.  
  157.             // True condition in case we don't have a scope
  158.         }elseif(empty($this->scope_condition&& empty($this->scope)){
  159.             $this->scope_condition ($this->_ActiveRecordInstance->_db->type(== 'postgre''true' '1';
  160.         }elseif (!empty($this->scope)){
  161.             $this->setScopeCondition(join(' AND ',array_map(array(&$this,'getScopedColumn'),(array)$this->scope)));
  162.         }
  163.         return  $this->scope_condition;
  164.     }
  165.  
  166.  
  167.     function setScopeCondition($scope_condition)
  168.     {
  169.         if(!is_array($scope_condition&& strstr($scope_condition'?')){
  170.             $this->variable_scope_condition $scope_condition;
  171.         }else{
  172.             $this->scope_condition  $scope_condition;
  173.         }
  174.     }
  175.  
  176.     function getScopedColumn($column)
  177.     {
  178.         if($this->_ActiveRecordInstance->hasColumn($column)){
  179.             $value $this->_ActiveRecordInstance->get($column);
  180.             $condition $this->_ActiveRecordInstance->getAttributeCondition($value);
  181.             $value $this->_ActiveRecordInstance->castAttributeForDatabase($column$value);
  182.             return $column.' '.str_replace('?'$value$condition);
  183.         }else{
  184.             return $column;
  185.         }
  186.     }
  187.  
  188.     function getLeftColumnName()
  189.     {
  190.         return $this->_left_column_name;
  191.     }
  192.     function setLeftColumnName($left_column_name)
  193.     {
  194.         $this->_left_column_name = $left_column_name;
  195.     }
  196.  
  197.     function getRightColumnName()
  198.     {
  199.         return $this->_right_column_name;
  200.     }
  201.     function setRightColumnName($right_column_name)
  202.     {
  203.         $this->_right_column_name = $right_column_name;
  204.     }
  205.  
  206.  
  207.     function getParentColumnName()
  208.     {
  209.         return $this->_parent_column_name;
  210.     }
  211.     function setParentColumnName($parent_column_name)
  212.     {
  213.         $this->_parent_column_name = $parent_column_name;
  214.     }
  215.  
  216.     /**
  217.     * Returns true is this is a root node.
  218.     */
  219.     function isRoot()
  220.     {
  221.         $left_id $this->_ActiveRecordInstance->get($this->getLeftColumnName());
  222.         return ($this->_ActiveRecordInstance->get($this->getParentColumnName()) == null&& ($left_id == 1&& ($this->_ActiveRecordInstance->get($this->getRightColumnName()) $left_id);
  223.     }
  224.  
  225.     /**
  226.     * Returns true is this is a child node
  227.     */
  228.     function isChild()
  229.     {
  230.         $parent_id $this->_ActiveRecordInstance->get($this->getParentColumnName());
  231.         $left_id $this->_ActiveRecordInstance->get($this->getLeftColumnName());
  232.         return !($parent_id == 0 || is_null($parent_id)) && ($left_id > 1&& ($this->_ActiveRecordInstance->get($this->getRightColumnName()) $left_id);
  233.     }
  234.  
  235.     /**
  236.     * Returns true if we have no idea what this is
  237.     */
  238.     function isUnknown()
  239.     {
  240.         return !$this->isRoot(&& !$this->isChild();
  241.     }
  242.  
  243.     /**
  244.     * Added a child to this object in the tree.  If this object hasn't been initialized,
  245.     * it gets set up as a root node.  Otherwise, this method will update all of the
  246.     * other elements in the tree and shift them to the right. Keeping everything
  247.     * balanced. 
  248.     */
  249.     function addChild&$child )
  250.     {
  251.         $self =$this->_ActiveRecordInstance;
  252.         $self->reload();
  253.         $child->reload();
  254.         $left_column $this->getLeftColumnName();
  255.         $right_column $this->getRightColumnName();
  256.         $parent_column $this->getParentColumnName();
  257.  
  258.         if ($child->nested_set->isRoot()){
  259.             trigger_error(Ak::t("Adding sub-tree isn't currently supported"),E_USER_ERROR);
  260.         }elseif ( (is_null($self->get($left_column))) || (is_null($self->get($right_column))) ){
  261.             // Looks like we're now the root node!  Woo
  262.             $self->set($left_column1);
  263.             $self->set($right_column4);
  264.  
  265.             $self->transactionStart();
  266.             // What do to do about validation?
  267.             if(!$self->save()){
  268.                 $self->transactionFail();
  269.                 $self->transactionComplete();
  270.                 return false;
  271.             }
  272.  
  273.             $child->set($parent_column$self->getId());
  274.             $child->set($left_column2);
  275.             $child->set($right_column3);
  276.  
  277.             if(!$child->save()){
  278.                 $self->transactionFail();
  279.                 $self->transactionComplete();
  280.                 return false;
  281.             }
  282.             $self->transactionComplete();
  283.             return $child;
  284.         }else{
  285.             // OK, we need to add and shift everything else to the right
  286.             $child->set($parent_column$self->getId());
  287.             $right_bound $self->get($right_column);
  288.             $child->set($left_column$right_bound);
  289.             $child->set($right_column$right_bound +1);
  290.             $self->set($right_column$self->get($right_column+ 2);
  291.  
  292.             $self->transactionStart();
  293.             $self->updateAll"$left_column = ($left_column + 2)",  $this->getScopeCondition()." AND $left_column >= $right_bound);
  294.             $self->updateAll"$right_column = ($right_column + 2)",  $this->getScopeCondition()." AND $right_column >= $right_bound);
  295.             $self->save();
  296.             $child->save();
  297.             $this->reloadActiveRecordInstance($child);
  298.             $self->transactionComplete();
  299.             return $child;
  300.         }
  301.     }
  302.  
  303.  
  304.     /**
  305.     * Returns the parent Object
  306.     */
  307.     function &getParent()
  308.     {
  309.         if(!$this->isChild()){
  310.             $result = false;
  311.         }else{
  312.             $result =$this->_ActiveRecordInstance->find(
  313.             // str_replace(array_keys($options['conditions']), array_values($this->getSanitizedConditionsArray($options['conditions'])),$pattern);
  314.             'first'array('conditions' => " ".$this->getScopeCondition()." AND ".$this->_ActiveRecordInstance->getPrimaryKey()." = ".$this->_ActiveRecordInstance->{$this->getParentColumnName()})
  315.             );
  316.         }
  317.         return $result;
  318.     }
  319.  
  320.     /**
  321.     * Returns an array of parent Objects this is usefull to make breadcrum like stuctures
  322.     */
  323.     function &getParents()
  324.     {
  325.         $Ancestors =$this->getAncestors();
  326.         return $Ancestors;
  327.     }
  328.  
  329.  
  330.     /**
  331.     * Prunes a branch off of the tree, shifting all of the elements on the right
  332.     * back to the left so the counts still work.
  333.     */
  334.     function beforeDestroy(&$object)
  335.     {
  336.         if(!empty($object->__avoid_nested_set_before_destroy_recursion)){
  337.             return true;
  338.         }
  339.         if((empty($object->{$this->getRightColumnName()}|| empty($object->{$this->getLeftColumnName()})) || $object->nested_set->isUnknown()){
  340.             return true;
  341.         }
  342.         $dif $object->{$this->getRightColumnName()$object->{$this->getLeftColumnName()+ 1;
  343.  
  344.         $ObjectsToDelete $object->nested_set->getAllChildren();
  345.  
  346.         $object->transactionStart();
  347.  
  348.         if(!empty($ObjectsToDelete)){
  349.             foreach (array_keys($ObjectsToDeleteas $k){
  350.                 $Child =$ObjectsToDelete[$k];
  351.                 $Child->__avoid_nested_set_before_destroy_recursion = true;
  352.                 if($Child->beforeDestroy()){
  353.                     if($Child->notifyObservers('beforeDestroy'=== false){
  354.                         $Child->transactionFail();
  355.                     }
  356.                 }else{
  357.                     $Child->transactionFail();
  358.                 }
  359.             }
  360.         }
  361.  
  362.         $object->deleteAll($this->getScopeCondition().
  363.         " AND ".$this->getLeftColumnName()." > ".$object->{$this->getLeftColumnName()}.
  364.         " AND ".$this->getRightColumnName()." < ".$object->{$this->getRightColumnName()});
  365.  
  366.         $object->updateAll($this->getLeftColumnName()." = (".$this->getLeftColumnName()." - $dif)",
  367.         $this->getScopeCondition()." AND ".$this->getLeftColumnName()." >= ".$object->{$this->getRightColumnName());
  368.  
  369.         $object->updateAll($this->getRightColumnName()." = (".$this->getRightColumnName()." - $dif )",
  370.         $this->getScopeCondition()." AND ".$this->getRightColumnName()." >= ".$object->{$this->getRightColumnName()});
  371.  
  372.  
  373.         if(!empty($ObjectsToDelete)){
  374.             foreach (array_keys($ObjectsToDeleteas $k){
  375.                 $Child =$ObjectsToDelete[$k];
  376.                 $Child->__avoid_nested_set_before_destroy_recursion = true;
  377.                 if(!$Child->afterDestroy(|| $Child->notifyObservers('afterDestroy'=== false){
  378.                     $Child->transactionFail();
  379.                 }
  380.             }
  381.         }
  382.  
  383.         if($object->transactionHasFailed()){
  384.             $object->transactionComplete();
  385.             return false;
  386.         }
  387.         $object->transactionComplete();
  388.  
  389.         return true;
  390.     }
  391.  
  392.     /**
  393.      * on creation, set automatically lft and rgt to the end of the tree
  394.      */
  395.     function beforeCreate(&$object)
  396.     {
  397.         $object->nested_set->_setLeftAndRightToTheEndOfTheTree();
  398.         return true;
  399.     }
  400.  
  401.     {
  402.         $left $this->getLeftColumnName();
  403.         $right $this->getRightColumnName();
  404.  
  405.         $maxright $this->_ActiveRecordInstance->maximum($rightarray('conditions'=>$this->getScopeCondition()));
  406.         $maxright = empty($maxright? 0 : $maxright;
  407.  
  408.         $this->_ActiveRecordInstance->set($left$maxright+1);
  409.         $this->_ActiveRecordInstance->set($right$maxright+2);
  410.     }
  411.  
  412.     /**
  413.      * Returns the single root
  414.      */
  415.     function getRoot()
  416.     {
  417.         return $this->_ActiveRecordInstance->find('first'array('conditions' => " ".$this->getScopeCondition()." AND ".$this->getParentColumnName()." IS NULL "));
  418.     }
  419.  
  420.     /**
  421.      * Returns roots when multiple roots (or virtual root, which is the same)
  422.      */
  423.     function getRoots()
  424.     {
  425.         return $this->_ActiveRecordInstance->find('all'array('conditions' => " ".$this->getScopeCondition()." AND ".$this->getParentColumnName()." IS NULL ",'order' => $this->getLeftColumnName()));
  426.     }
  427.  
  428.  
  429.     /**
  430.      * Returns an array of all parents 
  431.      */
  432.     function &getAncestors()
  433.     {
  434.         $Ancestors =$this->_ActiveRecordInstance->find('all'array('conditions' => ' '.$this->getScopeCondition().' AND '.
  435.         $this->getLeftColumnName().' < '.$this->_ActiveRecordInstance->get($this->getLeftColumnName()).' AND '.
  436.         $this->getRightColumnName().' > '.$this->_ActiveRecordInstance->get($this->getRightColumnName())
  437.         ,'order' => $this->getLeftColumnName()));
  438.         return $Ancestors;
  439.     }
  440.  
  441.     /**
  442.      * Returns the array of all parents and self
  443.      */
  444.     function &getSelfAndAncestors()
  445.     {
  446.         if($result =$this->getAncestors()){
  447.             array_push($result$this->_ActiveRecordInstance);
  448.         }else{
  449.             $result = array(&$this->_ActiveRecordInstance);
  450.         }
  451.         return $result;
  452.     }
  453.  
  454.  
  455.     /**
  456.      * Returns the array of all children of the parent, except self
  457.      */
  458.     function getSiblings($search_for_self = false)
  459.     {
  460.         return $this->_ActiveRecordInstance->find('all'array('conditions' => ' (('.$this->getScopeCondition().' AND '.
  461.         $this->getParentColumnName().' = '.$this->_ActiveRecordInstance->get($this->getParentColumnName()).' AND '.
  462.         $this->_ActiveRecordInstance->getPrimaryKey().' <> '.$this->_ActiveRecordInstance->getId().
  463.         ($search_for_self&&!$this->_ActiveRecordInstance->isNewRecord()?') OR ('.$this->_ActiveRecordInstance->getPrimaryKey().' = '.$this->_ActiveRecordInstance->quotedId().'))':'))')
  464.         ,'order' => $this->getLeftColumnName()));
  465.     }
  466.  
  467.     /**
  468.      * Returns the array of all children of the parent, included self
  469.      */
  470.     function getSelfAndSiblings()
  471.     {
  472.         $parent_id $this->_ActiveRecordInstance->get($this->getParentColumnName());
  473.         if(empty($parent_id|| !$result $this->getSiblings(true)){
  474.             $result = array($this->_ActiveRecordInstance);
  475.         }
  476.         return $result;
  477.     }
  478.  
  479.  
  480.     /**
  481.      * Returns the level of this object in the tree 
  482.      * root level is 0
  483.      */
  484.     function getLevel()
  485.     {
  486.         $parent_id $this->_ActiveRecordInstance->get($this->getParentColumnName());
  487.         if(empty($parent_id)){
  488.             return 0;
  489.         }
  490.         return $this->_ActiveRecordInstance->count(' '.$this->getScopeCondition().' AND '.
  491.         $this->getLeftColumnName().' < '.$this->_ActiveRecordInstance->get($this->getLeftColumnName()).' AND '.
  492.         $this->getRightColumnName().' > '.$this->_ActiveRecordInstance->get($this->getRightColumnName()));
  493.     }
  494.  
  495.  
  496.     /**
  497.     * Returns the number of all nested children of this object.
  498.     */
  499.     function countChildren()
  500.     {
  501.         $children_count ($this->_ActiveRecordInstance->get($this->getRightColumnName()) $this->_ActiveRecordInstance->get($this->getLeftColumnName()) - 1)/2;
  502.         return $children_count > 0 ? $children_count : 0;
  503.     }
  504.  
  505.  
  506.     /**
  507.      * Returns a set of only this entry's immediate children
  508.      */
  509.     function getChildren()
  510.     {
  511.         return $this->_ActiveRecordInstance->find('all'array('conditions' => ' '.$this->getScopeCondition().' AND '.
  512.         $this->getParentColumnName().' = '.$this->_ActiveRecordInstance->getId()
  513.         ,'order' => $this->getLeftColumnName()));
  514.     }
  515.  
  516.     /**
  517.      * Returns a set of all of its children and nested children
  518.      */
  519.     function getAllChildren()
  520.     {
  521.         $args func_get_args();
  522.         $excluded_ids = array();
  523.         if(!empty($args)){
  524.             $exclude count($args> 1 ? $args (is_array($args[0]$args[0(empty($args[0]? false : array($args[0])));
  525.             if(!empty($exclude)){
  526.                 $parent_class_name get_class($this->_ActiveRecordInstance);
  527.                 foreach (array_keys($excludeas $k){
  528.                     $Item =$exclude[$k];
  529.                     if(is_a($Item,$parent_class_name)){
  530.                         $ItemToExclude =$Item;
  531.                     }else{
  532.                         $ItemToExclude =$this->_ActiveRecordInstance->find($Item);
  533.                     }
  534.                     if($ItemSet $ItemToExclude->nested_set->getFullSet()){
  535.                         foreach (array_keys($ItemSetas $l){
  536.                             $excluded_ids[$ItemSet[$l]->getId();
  537.                         }
  538.                     }
  539.                 }
  540.                 $excluded_ids array_unique(array_diff($excluded_ids,array('')));
  541.             }
  542.         }
  543.         return $this->_ActiveRecordInstance->find('all'array('conditions' => ' '.$this->getScopeCondition().' AND '.
  544.         (empty($excluded_ids'' ' id NOT IN ('.join(',',$excluded_ids).') AND ').
  545.         $this->getLeftColumnName().' > '.$this->_ActiveRecordInstance->get($this->getLeftColumnName()).' AND '.
  546.         $this->getRightColumnName().' < '.$this->_ActiveRecordInstance->get($this->getRightColumnName())
  547.         ,'order' => $this->getLeftColumnName()));
  548.     }
  549.  
  550.     /**
  551.      * Returns a set of itself and all of its nested children
  552.      */
  553.     function getFullSet($exclude = null)
  554.     {
  555.         if($this->_ActiveRecordInstance->isNewRecord(|| $this->_ActiveRecordInstance->get($this->getRightColumnName()) $this->_ActiveRecordInstance->get($this->getLeftColumnName()) == 1 ){
  556.             $result = array($this->_ActiveRecordInstance);
  557.         }else{
  558.         (array)$result $this->getAllChildren($exclude);
  559.         array_unshift($result$this->_ActiveRecordInstance);
  560.         }
  561.         return $result;
  562.     }
  563.  
  564.  
  565.     /**
  566.      * Move the node to the left of another node
  567.      */
  568.     function moveToLeftOf($node)
  569.     {
  570.         return $this->moveTo($node'left');
  571.     }
  572.  
  573.     /**
  574.      * Move the node to the left of another node
  575.      */
  576.     function moveToRightOf($node)
  577.     {
  578.         return $this->moveTo($node'right');
  579.     }
  580.  
  581.     /**
  582.      * Move the node to the child of another node
  583.      */
  584.     function moveToChildOf($node)
  585.     {
  586.         return $this->moveTo($node'child');
  587.     }
  588.  
  589.     function moveTo($target$position)
  590.     {
  591.         if($this->_ActiveRecordInstance->isNewRecord()){
  592.             trigger_error(Ak::t('You cannot move a new node')E_USER_ERROR);
  593.         }
  594.         $current_left $this->_ActiveRecordInstance->get($this->getLeftColumnName());
  595.         $current_right $this->_ActiveRecordInstance->get($this->getRightColumnName());
  596.         // $extent is the width of the tree self and children
  597.         $extent $current_right $current_left + 1;
  598.  
  599.         // load object if node is not an object
  600.         if (is_numeric($target)){
  601.             $target =$this->_ActiveRecordInstance->find($target);
  602.         }
  603.         if(!$target || !is_a($targetget_class($this->_ActiveRecordInstance))){
  604.             trigger_error(Ak::t('Invalid target')E_USER_NOTICE);
  605.             return false;
  606.         }
  607.  
  608.         $target_left $target->get($this->getLeftColumnName());
  609.         $target_right $target->get($this->getRightColumnName());
  610.  
  611.  
  612.         // detect impossible move
  613.         if ((($current_left <= $target_left&& ($target_left <= $current_right)) || (($current_left <= $target_right&& ($target_right <= $current_right))){
  614.             trigger_error(Ak::t('Impossible move, target node cannot be inside moved tree.')E_USER_ERROR);
  615.         }
  616.  
  617.         // compute new left/right for self
  618.         if ($position == 'child'){
  619.             if ($target_left $current_left){
  620.                 $new_left  $target_left + 1;
  621.                 $new_right $target_left $extent;
  622.             }else{
  623.                 $new_left  $target_left $extent + 1;
  624.                 $new_right $target_left;
  625.             }
  626.         }elseif($position == 'left'){
  627.             if ($target_left $current_left){
  628.                 $new_left  $target_left;
  629.                 $new_right $target_left $extent - 1;
  630.             }else{
  631.                 $new_left  $target_left $extent;
  632.                 $new_right $target_left - 1;
  633.             }
  634.         }elseif($position == 'right'){
  635.             if ($target_right $current_right){
  636.                 $new_left  $target_right + 1;
  637.                 $new_right $target_right $extent;
  638.             }else{
  639.                 $new_left  $target_right $extent + 1;
  640.                 $new_right $target_right;
  641.             }
  642.         }else{
  643.             trigger_error(Ak::t("Position should be either left or right ('%position' received).",array('%position'=>$position))E_USER_ERROR);
  644.         }
  645.  
  646.         // boundaries of update action
  647.         $left_boundary min($current_left$new_left);
  648.         $right_boundary max($current_right$new_right);
  649.  
  650.         // Shift value to move self to new $position
  651.         $shift $new_left $current_left;
  652.  
  653.         // Shift value to move nodes inside boundaries but not under self_and_children
  654.         $updown ($shift > 0? -$extent $extent;
  655.  
  656.         // change null to NULL for new parent
  657.         if($position == 'child'){
  658.             $new_parent $target->getId();
  659.         }else{
  660.             $target_parent $target->get($this->getParentColumnName());
  661.             $new_parent = empty($target_parent'NULL' $target_parent;
  662.         }
  663.  
  664.         $this->_ActiveRecordInstance->updateAll(
  665.  
  666.         $this->getLeftColumnName().' = CASE '.
  667.         'WHEN '.$this->getLeftColumnName().' BETWEEN '.$current_left.' AND '.$current_right.' '.
  668.         'THEN '.$this->getLeftColumnName().' + '.$shift.' '.
  669.         'WHEN '.$this->getLeftColumnName().' BETWEEN '.$left_boundary.' AND '.$right_boundary.' '.
  670.         'THEN '.$this->getLeftColumnName().' + '.$updown.' '.
  671.         'ELSE '.$this->getLeftColumnName().' END, '.
  672.  
  673.         $this->getRightColumnName().' = CASE '.
  674.         'WHEN '.$this->getRightColumnName().' BETWEEN '.$current_left.' AND '.$current_right.' '.
  675.         'THEN '.$this->getRightColumnName().' + '.$shift.' '.
  676.         'WHEN '.$this->getRightColumnName().' BETWEEN '.$left_boundary.' AND '.$right_boundary.' '.
  677.         'THEN '.$this->getRightColumnName().' + '.$updown.' '.
  678.         'ELSE '.$this->getRightColumnName().' END, '.
  679.  
  680.         $this->getParentColumnName().' = CASE '.
  681.         'WHEN '.$this->_ActiveRecordInstance->getPrimaryKey().' = '.$this->_ActiveRecordInstance->getId().' '.
  682.         'THEN '.$new_parent.' '.
  683.         'ELSE '.$this->getParentColumnName().' END',
  684.  
  685.         $this->getScopeCondition() );
  686.         $this->_ActiveRecordInstance->reload();
  687.  
  688.         return true;
  689.     }
  690.  
  691. }
  692.  
  693.  
  694. ?>

Documentation generated on Tue, 17 Jun 2008 14:24:34 +0200 by phpDocumentor 1.3.2