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 // +----------------------------------------------------------------------+
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 |
17 // +----------------------------------------------------------------------+
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');
33 * Makes your model acts as a tree (surprise!). Consider the following example:
35 * class Category extends ActiveRecord {
36 * var $acts_as = 'tree';
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:
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
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';
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
)){
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
);
142 $this->observe(&$ActiveRecordInstance);
145 trigger_error(Ak
::t('You are trying to set an object that is not an active record.'), E_USER_ERROR
);
151 function reloadActiveRecordInstance(&$nodeInstance)
153 AK_PHP5 ?
null : $nodeInstance->tree
->_ensureIsActiveRecordInstance($nodeInstance);
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;
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);
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;
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();
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
);
247 $child->{$this->getParentColumnName()} = $this->_ActiveRecordInstance
->getId();
248 if (!$child->save()) {
249 $this->_ActiveRecordInstance
->transactionFail();
250 $this->_ActiveRecordInstance
->transactionComplete();
254 $this->_ActiveRecordInstance
->transactionComplete();
256 $this->reloadActiveRecordInstance($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());
273 if (!$this->hasParent()){
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()) {
290 $last = $this->getParent();
291 $ancestors = array($last);
294 // we can't do end($ancestors)->hasParent() due to PHP4 compatibility
295 while ($level != 0 && $last->tree
->hasParent()) {
296 $last = $last->tree
->getParent();
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()) {
329 return $this->_recursiveGetDescendants($level, $this->getChildren());
331 function _recursiveGetDescendants($level, $from) {
339 foreach ($from as $item) {
340 if ($item->tree
->hasChildren()) {
341 $children[] = array($item, $this->_recursiveGetDescendants($level, $item->tree
->getChildren()));
350 function beforeDestroy(&$object)
352 if(!$object->tree
->hasChildren()){
356 $object->transactionStart();
358 if ($this->getDependent()){
359 $object->deleteAll($this->getScopeCondition().' AND '.$this->getParentColumnName().' = '.$object->getId());
361 $object->updateAll( $this->getParentColumnName() .' = NULL', $this->getScopeCondition().' AND '.$this->getParentColumnName().' = '.$object->getId() );
364 if($object->transactionHasFailed()){
365 $object->transactionComplete();
368 $object->transactionComplete();