Ensuring tests pass after bringing back support for db DSN connection. Rel. [501...
[akelos.git] / lib / AkActiveRecord / AkActsAsBehaviours / AkActsAsTree.php
blob6859d844cea1bb232e56c6ce71c81d09abdf54bf
1 <?php
2 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
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 // +----------------------------------------------------------------------+
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>
27 require_once(AK_LIB_DIR.DS.'AkActiveRecord'.DS.'AkObserver.php');
30 /**
31 * acts_as_tree
33 * Makes your model acts as a tree (surprise!). Consider the following example:
35 * class Category extends ActiveRecord {
36 * var $acts_as = 'tree';
37 * }
39 * $Category = new Category;
41 * $CategoryA = $Category->create();
42 * $CategoryAa = $Category->create();
43 * $CategoryAa1 = $Category->create();
44 * $CategoryAa2 = $Category->create();
45 * $CategoryAb = $Category->create();
46 * $CategoryB = $Category->create();
48 * $CategoryA->tree->addChild($CategoryAa)
49 * $CategoryA->tree->addChild($CategoryAb)
50 * $CategoryAa->tree->addChild($CategoryAa1)
51 * $CategoryAa->tree->addChild($CategoryAa2)
54 * This will effectively give you:
56 * Category A
57 * \_ Category Aa
58 * \_ Category Aa1
59 * \_ Category Aa2
60 * \_ Category Ab
61 * Category B
64 * OK. Admittedly you won't get a graph in real life. But at least the following functions:
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)
73 * $CategoryAa->tree->getChildren() # ==> array($CategoryAa1, $CategoryAa2)
74 * $CategoryAa->tree->getSiblings() # ==> array($CategoryAb)
75 * $CategoryAa->tree->hasParent() # ==> true
76 * $CategoryAa->tree->getParent() # ==> $CategoryA
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)
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)
90 * (Note that on adding a child it will be saved. If the parent has been unsaved until now it will also be saved.)
92 class AkActsAsTree extends AkObserver
97 /**
98 * Configuration options are:
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>
108 var $parent_column = 'parent_id';
109 var $scope;
110 var $scope_condition;
111 var $_parent_column_name = 'parent_id';
112 var $_dependent = false;
114 var $_ActiveRecordInstance;
116 function AkActsAsTree(&$ActiveRecordInstance)
118 $this->_ActiveRecordInstance =& $ActiveRecordInstance;
121 function init($options = array())
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);
131 function _ensureIsActiveRecordInstance(&$ActiveRecordInstance)
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);
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;
148 return true;
151 function reloadActiveRecordInstance(&$nodeInstance)
153 AK_PHP5 ? null : $nodeInstance->tree->_ensureIsActiveRecordInstance($nodeInstance);
156 function getType()
158 return 'tree';
161 function getScopeCondition()
163 if (!empty($this->variable_scope_condition)){
164 return $this->_ActiveRecordInstance->_getVariableSqlCondition($this->variable_scope_condition);
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(''))));
172 return $this->scope_condition;
176 function setScopeCondition($scope_condition)
178 if(!is_array($scope_condition) && strstr($scope_condition, '?')){
179 $this->variable_scope_condition = $scope_condition;
180 }else{
181 $this->scope_condition = $scope_condition;
185 function getScopedColumn($column)
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;
197 function getParentColumnName()
199 return $this->_parent_column_name;
202 function setParentColumnName($parent_column_name)
204 $this->_parent_column_name = $parent_column_name;
207 function getDependent()
209 return $this->_dependent;
212 function setDependent($val)
214 $this->_dependent = (bool)$val;
217 function hasChildren()
219 return $this->childrenCount() > 0;
222 function hasParent()
224 $parent_id = $this->_ActiveRecordInstance->{$this->getParentColumnName()};
225 return !empty($parent_id);
228 function addChild( &$child )
230 $this->_ActiveRecordInstance->transactionStart();
232 if ($this->_ActiveRecordInstance->isNewRecord()){
233 if (!$this->_ActiveRecordInstance->save()) {
234 $this->_ActiveRecordInstance->transactionFail();
235 $this->_ActiveRecordInstance->transactionComplete();
236 return false;
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;
247 $child->{$this->getParentColumnName()} = $this->_ActiveRecordInstance->getId();
248 if (!$child->save()) {
249 $this->_ActiveRecordInstance->transactionFail();
250 $this->_ActiveRecordInstance->transactionComplete();
251 return false;
254 $this->_ActiveRecordInstance->transactionComplete();
256 $this->reloadActiveRecordInstance($child);
257 return $child;
260 function childrenCount()
263 return $this->_ActiveRecordInstance->isNewRecord() ? 0 : $this->_ActiveRecordInstance->count(" ".$this->getScopeCondition()." AND ".$this->getParentColumnName()." = ".$this->_ActiveRecordInstance->getId());
266 function getChildren()
268 return $this->_ActiveRecordInstance->isNewRecord() ? false : $this->_ActiveRecordInstance->findAll(" ".$this->getScopeCondition()." AND ".$this->getParentColumnName()." = ".$this->_ActiveRecordInstance->getId());
271 function getParent()
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()}));
282 * @param integer $level How deep do you want to search? everything <= 0 means infinite deep
284 function getAncestors($level=0)
286 if (!$this->hasParent()) {
287 return array();
290 $last = $this->getParent();
291 $ancestors = array($last);
292 --$level;
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;
301 return $ancestors;
305 function getSiblings($options = array())
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);
315 function getSelfAndSiblings()
317 return $this->getSiblings(array('include_self'=>true));
321 * @param integer $level How deep do you want to search? everything <= 0 means infinite deep
323 function getDescendants($level=0)
325 if (!$this->hasChildren()) {
326 return array();
329 return $this->_recursiveGetDescendants($level, $this->getChildren());
331 function _recursiveGetDescendants($level, $from) {
332 --$level;
334 if ($level == 0) {
335 return $from;
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;
347 return $children;
350 function beforeDestroy(&$object)
352 if(!$object->tree->hasChildren()){
353 return true;
356 $object->transactionStart();
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() );
364 if($object->transactionHasFailed()){
365 $object->transactionComplete();
366 return false;
368 $object->transactionComplete();
370 return true;