[ Index ]

PHP Cross Reference of Akelos Framework

title

Body

[close]

/AkActiveRecord/AkActsAsBehaviours/ -> AkActsAsTree.php (source)

   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  // | Acts as Tree                                                         |
  11  // +----------------------------------------------------------------------+
  12  // | Copyright (c) 2006, Raw Ideas Pty Ltd & Niels Ganser                 |
  13  // | Released under the GNU Lesser General Public License, see LICENSE.txt|
  14  // | If the Akelos Framework License is changed to another one as or less |
  15  // | restrictive as the LGPL, permission is granted to also re-license    |
  16  // | this file.                                                           |
  17  // +----------------------------------------------------------------------+
  18  
  19  /**
  20   * @package ActiveRecord
  21   * @subpackage Behaviours
  22   * @author Niels Ganser <ng a.t depoll d.e>
  23   * @copyright Copyright (c) 2006, Raw Ideas Pty Ltd
  24   * @license GNU Lesser General Public License <http://www.gnu.org/copyleft/lesser.html>
  25   */
  26  
  27  require_once (AK_LIB_DIR.DS.'AkActiveRecord'.DS.'AkObserver.php');
  28  
  29  
  30  /**
  31   * acts_as_tree
  32   * 
  33   * Makes your model acts as a tree (surprise!). Consider the following example:
  34   * 
  35   * class Category extends ActiveRecord {
  36   *   var $acts_as = 'tree';
  37   * }
  38   * 
  39   * $Category = new Category;
  40   * 
  41   * $CategoryA = $Category->create();
  42   * $CategoryAa = $Category->create();
  43   * $CategoryAa1 = $Category->create();
  44   * $CategoryAa2 = $Category->create();
  45   * $CategoryAb = $Category->create();
  46   * $CategoryB = $Category->create();
  47   * 
  48   * $CategoryA->tree->addChild($CategoryAa)
  49   * $CategoryA->tree->addChild($CategoryAb)
  50   * $CategoryAa->tree->addChild($CategoryAa1)
  51   * $CategoryAa->tree->addChild($CategoryAa2)
  52   * 
  53   * 
  54   * This will effectively give you:
  55   * 
  56   * Category A
  57   *  \_ Category Aa
  58   *      \_ Category Aa1
  59   *      \_ Category Aa2
  60   *  \_ Category Ab
  61   * Category B
  62   * 
  63   * 
  64   * OK. Admittedly you won't get a graph in real life. But at least the following functions:
  65   * 
  66   * $CategoryA->tree->hasChildren()        # ==> true
  67   * $CategoryA->tree->childrenCount()    # ==> 2
  68   * $CategoryA->tree->getChildren()         # ==> array($CategoryAa, $CategoryAb)
  69   * // fairly expensive operation follows
  70   * // (yes, array(parent, array_of_children) is not nice but unfortunately PHP doesn't allow for objects as keys)
  71   * $CategoryA->tree->getDescendants()    # ==> array(array($CategoryAa, array($CategoryAa1, $CategoryAa2)), $CategoryAb)
  72   * 
  73   * $CategoryAa->tree->getChildren()        # ==> array($CategoryAa1, $CategoryAa2)
  74   * $CategoryAa->tree->getSiblings()        # ==> array($CategoryAb)
  75   * $CategoryAa->tree->hasParent()        # ==> true
  76   * $CategoryAa->tree->getParent()        # ==> $CategoryA
  77   * 
  78   * $CatagoryAa1->tree->hasChildren()    # ==> false
  79   * $CategoryAa1->tree->getParent()        # ==> $CategoryAa
  80   * // fairly expensive operation follows
  81   * $CategoryAa1->tree->getAncestors()    # ==> array($CategoryAa, $CategoryA)
  82   * // fairly expensive operation follows
  83   * $CategoryAa1->tree->getAncestors(1)    # ==> array($CategoryAa)
  84   *  
  85   * 
  86   * To make this work your model needs a parent_id column (whose name can be overriden with +parent_column+. Furthermore
  87   * you can set the +dependent+ option to automatically delete all children if their parent gets deleted. Otherwise they
  88   * will become orphants (i.e. have parent_id = NULL)
  89   * 
  90   * (Note that on adding a child it will be saved. If the parent has been unsaved until now it will also be saved.)
  91   */
  92  class AkActsAsTree extends AkObserver
  93  {
  94  
  95  
  96  
  97      /**
  98      * Configuration options are:
  99      *
 100      * * +parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
 101      * * +dependent+ - set to true to automatically delete all children when its parent is deleted
 102      * * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id" 
 103      *   (if that hasn't been already) and use that as the foreign key restriction. It's also possible 
 104      *   to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
 105      *   Example: <tt>actsAsTree(array('scope' => array('todo_list_id = ? AND completed = 0',$todo_list_id)));</tt>
 106      */
 107  
 108      var $parent_column = 'parent_id';
 109      var $scope;
 110      var $scope_condition;
 111      var $_parent_column_name = 'parent_id';
 112      var $_dependent = false;
 113  
 114      var $_ActiveRecordInstance;
 115  
 116      function AkActsAsTree(&$ActiveRecordInstance)
 117      {
 118          $this->_ActiveRecordInstance =& $ActiveRecordInstance;
 119      }
 120  
 121      function init($options = array())
 122      {
 123          empty($options['parent_column']) ? null : ($this->_parent_column_name = $options['parent_column']);
 124          empty($options['dependent']) ? null : ($this->_dependent = $options['dependent']);
 125          empty($options['scope']) ? null : $this->setScopeCondition($options['scope']);
 126          $this->parent_column = !empty($options['parent_column']) ? $options['parent_column'] : $this->parent_column;
 127          return $this->_ensureIsActiveRecordInstance($this->_ActiveRecordInstance);
 128      }
 129  
 130  
 131      function _ensureIsActiveRecordInstance(&$ActiveRecordInstance)
 132      {
 133          if(is_object($ActiveRecordInstance) && method_exists($ActiveRecordInstance,'actsLike')){
 134              $this->_ActiveRecordInstance =& $ActiveRecordInstance;
 135              if(!$this->_ActiveRecordInstance->hasColumn($this->_parent_column_name)){
 136                  trigger_error(Ak::t(
 137                  'The following columns are required in the table "%table" for the model "%model" to act as a Tree: "%columns".',array(
 138                  '%columns'=>$this->getParentColumnName(),'%table'=>$this->_ActiveRecordInstance->getTableName(),'%model'=>$this->_ActiveRecordInstance->getModelName())),E_USER_ERROR);
 139                  unset($this->_ActiveRecordInstance->tree);
 140                  return false;
 141              }else{
 142                  $this->observe(&$ActiveRecordInstance);
 143              }
 144          }else{
 145              trigger_error(Ak::t('You are trying to set an object that is not an active record.'), E_USER_ERROR);
 146              return false;
 147          }
 148          return true;
 149      }
 150  
 151      function reloadActiveRecordInstance(&$nodeInstance)
 152      {
 153          AK_PHP5 ? null : $nodeInstance->tree->_ensureIsActiveRecordInstance($nodeInstance);
 154      }
 155  
 156      function getType()
 157      {
 158          return 'tree';
 159      }
 160      
 161      function getScopeCondition()
 162      {
 163          if (!empty($this->variable_scope_condition)){
 164              return $this->_ActiveRecordInstance->_getVariableSqlCondition($this->variable_scope_condition);
 165              
 166          // True condition in case we don't have a scope
 167          }elseif(empty($this->scope_condition) && empty($this->scope)){
 168              $this->scope_condition = ($this->_ActiveRecordInstance->_db->type() == 'postgre') ? 'true' : '1';
 169          }elseif (!empty($this->scope)){
 170              $this->setScopeCondition(join(' AND ',array_diff(array_map(array(&$this,'getScopedColumn'),(array)$this->scope),array(''))));
 171          }
 172          return  $this->scope_condition;
 173      }
 174  
 175  
 176      function setScopeCondition($scope_condition)
 177      {
 178          if(!is_array($scope_condition) && strstr($scope_condition, '?')){
 179              $this->variable_scope_condition = $scope_condition;
 180          }else{
 181              $this->scope_condition  = $scope_condition;
 182          }
 183      }
 184  
 185      function getScopedColumn($column)
 186      {
 187          if($this->_ActiveRecordInstance->hasColumn($column)){
 188              $value = $this->_ActiveRecordInstance->get($column);
 189              $condition = $this->_ActiveRecordInstance->getAttributeCondition($value);
 190              $value = $this->_ActiveRecordInstance->castAttributeForDatabase($column, $value);
 191              return $column.' '.str_replace('?', $value, $condition);
 192          }else{
 193              return $column;
 194          }
 195      }
 196  
 197      function getParentColumnName()
 198      {
 199          return $this->_parent_column_name;
 200      }
 201  
 202      function setParentColumnName($parent_column_name)
 203      {
 204          $this->_parent_column_name = $parent_column_name;
 205      }
 206  
 207      function getDependent()
 208      {
 209          return $this->_dependent;
 210      }
 211  
 212      function setDependent($val)
 213      {
 214          $this->_dependent = (bool)$val;
 215      }
 216  
 217      function hasChildren()
 218      {
 219          return $this->childrenCount() > 0;
 220      }
 221  
 222      function hasParent()
 223      {
 224          $parent_id = $this->_ActiveRecordInstance->{$this->getParentColumnName()};
 225          return !empty($parent_id);
 226      }
 227  
 228      function addChild( &$child )
 229      {
 230          $this->_ActiveRecordInstance->transactionStart();
 231  
 232          if ($this->_ActiveRecordInstance->isNewRecord()){
 233              if (!$this->_ActiveRecordInstance->save()) {
 234                  $this->_ActiveRecordInstance->transactionFail();
 235                  $this->_ActiveRecordInstance->transactionComplete();
 236                  return false;
 237              }
 238          }
 239  
 240          if ($this->_ActiveRecordInstance->getId() == $child->getId()) {
 241              $this->_ActiveRecordInstance->transactionFail();
 242              $this->_ActiveRecordInstance->transactionComplete();
 243              trigger_error(Ak::t('Cannot add myself as a child to myself'), E_USER_ERROR);
 244              return false;
 245          }
 246  
 247          $child->{$this->getParentColumnName()} = $this->_ActiveRecordInstance->getId();
 248          if (!$child->save()) {
 249              $this->_ActiveRecordInstance->transactionFail();
 250              $this->_ActiveRecordInstance->transactionComplete();
 251              return false;
 252          }
 253  
 254          $this->_ActiveRecordInstance->transactionComplete();
 255  
 256          $this->reloadActiveRecordInstance($child);
 257          return $child;
 258      }
 259  
 260      function childrenCount()
 261      {
 262          
 263          return $this->_ActiveRecordInstance->isNewRecord() ? 0 : $this->_ActiveRecordInstance->count(" ".$this->getScopeCondition()." AND ".$this->getParentColumnName()." = ".$this->_ActiveRecordInstance->getId());
 264      }
 265  
 266      function getChildren()
 267      {
 268          return $this->_ActiveRecordInstance->isNewRecord() ? false : $this->_ActiveRecordInstance->findAll(" ".$this->getScopeCondition()." AND ".$this->getParentColumnName()." = ".$this->_ActiveRecordInstance->getId());
 269      }
 270  
 271      function getParent()
 272      {
 273          if (!$this->hasParent()){
 274              return false;
 275          } else {
 276              return $this->_ActiveRecordInstance->find('first',
 277              array('conditions' => ' '.$this->getScopeCondition().' AND '.$this->_ActiveRecordInstance->getPrimaryKey()." = ".$this->_ActiveRecordInstance->{$this->getParentColumnName()}));
 278          }
 279      }
 280  
 281      /**
 282       * @param    integer    $level    How deep do you want to search? everything <= 0 means infinite deep
 283       */
 284      function getAncestors($level=0)
 285      {
 286          if (!$this->hasParent()) {
 287              return array();
 288          }
 289  
 290          $last = $this->getParent();
 291          $ancestors = array($last);
 292          --$level;
 293  
 294          // we can't do end($ancestors)->hasParent() due to PHP4 compatibility
 295          while ($level != 0 && $last->tree->hasParent()) {
 296              $last = $last->tree->getParent();
 297              $ancestors[]= $last;
 298              --$level;
 299          }
 300  
 301          return $ancestors;
 302      }
 303  
 304  
 305      function getSiblings($options = array())
 306      {
 307          $default_options = array('include_self'=>false);
 308          $options = array_merge($default_options, $options);
 309          $parent_condition = (is_null($this->_ActiveRecordInstance->{$this->getParentColumnName()})) ? 'ISNULL('. $this->getParentColumnName() .")" : $this->getParentColumnName() .' = '. $this->_ActiveRecordInstance->{$this->getParentColumnName()};
 310          $id_condition = !empty($options['include_self']) ? '' : ' AND '. $this->_ActiveRecordInstance->getPrimaryKey() .' != '. $this->_ActiveRecordInstance->getId();
 311          return $this->_ActiveRecordInstance->findAll(' '. $this->getScopeCondition().
 312          ' AND '. $parent_condition.$id_condition);
 313      }
 314      
 315      function getSelfAndSiblings()
 316      {
 317          return $this->getSiblings(array('include_self'=>true));
 318      }
 319  
 320      /**
 321       * @param    integer    $level    How deep do you want to search? everything <= 0 means infinite deep
 322       */
 323      function getDescendants($level=0)
 324      {
 325          if (!$this->hasChildren()) {
 326              return array();
 327          }
 328  
 329          return $this->_recursiveGetDescendants($level, $this->getChildren());
 330      }
 331      function _recursiveGetDescendants($level, $from) {
 332          --$level;
 333  
 334          if ($level == 0) {
 335              return $from;
 336          }
 337  
 338          $children = array();
 339          foreach ($from as $item) {
 340              if ($item->tree->hasChildren()) {
 341                  $children[] = array($item, $this->_recursiveGetDescendants($level, $item->tree->getChildren()));
 342              } else {
 343                  $children[] = $item;
 344              }
 345          }
 346  
 347          return $children;
 348      }
 349  
 350      function beforeDestroy(&$object)
 351      {
 352          if(!$object->tree->hasChildren()){
 353              return true;
 354          }
 355  
 356          $object->transactionStart();
 357  
 358          if ($this->getDependent()){
 359              $object->deleteAll($this->getScopeCondition().' AND '.$this->getParentColumnName().' = '.$object->getId());
 360          }else{
 361              $object->updateAll(    $this->getParentColumnName() .' = NULL', $this->getScopeCondition().' AND '.$this->getParentColumnName().' = '.$object->getId() );
 362          }
 363  
 364          if($object->transactionHasFailed()){
 365              $object->transactionComplete();
 366              return false;
 367          }
 368          $object->transactionComplete();
 369  
 370          return true;
 371      }
 372  
 373  }
 374  
 375  
 376  ?>


Generated: Mon Oct 27 12:43:49 2008 Cross-referenced by PHPXref 0.6