Location: PHPKode > projects > Porte > porte-0.2.2/src/plugins/Hierarchy.php
<?php

/**
 * Copyright (c) 2008, SARL Adaltas. All rights reserved.
 * Code licensed under the BSD License:
 * http://porte.adaltas.com/en/developer/license.html
 */

/**
 * PorteHierarchy
 *
 * Plugin to add hierarchical functionnalities to record manipulation. This implementation
 * use the nested set model.
 * 
 * @package    Porte
 * @subpackage plugin
 * @author     David Worms info(at)adaltas.com
 * @copyright  2008 Adaltas
 */
class PorteHierarchy{
	
	// A reference to the current record
	private $record;
	 // Temporary events used by the "clean" method
	private $events=array();
	
	public function __construct(PorteRecord $record){
		$this->record = $record;
	}
	
	/**
	 * Return the parent record. Result is cached after its first
	 * retrieval but it is possible to force its reloading by
	 * setting the 'force' option.
	 * 
	 * Options include
	 * 		force bool Force the reloading of the record
	 * 
	 * @return object Parent record or null if current record is root
	 */
	public function getParent($options=array()){
		return PorteHierarchy::_getParent(null,$this->record,$options);
	}
	
	public static function _getParent($arg,$record,$options=array()){
		if(isset($record->associations['parent'])&&empty($options['force'])){
			return $record->associations['parent'];
		}else if(PorteHierarchy::_isRoot(null,$record)){
			return null;
		}
		$record->associations['parent'] = PorteHierarchy::_getRootline(null,$record,array('limit'=>1,'offset'=>1,'direction'=>'DESC'));
		return $record->associations['parent']->array[0];
	}
	
	public function getSibling(){
		return PorteHierarchy::_getSibling(null,$this->record);
	}
	
	public static function _getSibling($arg,$record){
		throw new PorteException('Not yet implemented');
	}
	
	/**
	 * Return the children associated to this record. Records are cached unless the 
	 * option 'force' is present. The level of depth can be provided with the option
	 * 'depth'. If the depth is 1, only the direct descendants are retrieved, if greated 
	 * than 1, this indicate the number of level in the hierarchy, if equals or less
	 * than 0, the number of level is infinite.
	 * 
	 * Available options:
	 * 		force bool Force the reloading of children
	 * 		depth int Level of depth to load sub-children
	 * 		flatten bool Return all the record instead of the direct descendants
	 * 
	 * @return array Array of records
	 * @param $options Object[optional]
	 */
	public function getChildren($options=array()){
		return PorteHierarchy::_getChildren(null,$this->record,$options);
	}
	
	public static function _getChildren($arg,$record,$options=array()){
		// Sanitize options
		//if(!isset($options['depth'])||(!is_int($options['depth'])||!ctype_digit($options['depth']))){
		//if(!isset($options['depth'])||!is_int($options['depth'])||!ctype_digit($options['depth'])){
		if(!isset($options['depth'])||(!is_int($options['depth'])&&!ctype_digit($options['depth']))){
			$depth = 1;
		}else{
			$depth = intval($options['depth']);
		}
		// Cache
		if(!empty($options['force'])&&$record->associations['children']){
			return $record->associations['children'];
		}
		$model = $record->porte->models->{$record->type};
		// Prepare variables
		//$metaTable = $record->model->table;
		$db = $model['database'];
		$table = $model['table'];
		$rightField = $model['hierarchical']['right'];
		$leftField = $model['hierarchical']['left'];
		$pkField = $model['primary_key'];
		$pkValue = $record->getIdentifier();
		$depthProperty = $model['hierarchical']['depth'];
		$relativeDepthProperty = $model['hierarchical']['relative_depth'];
		// Build Sql
		if(empty($options['select'])){
			// todo: does not honor field to property mapping (replace "node.*")
			$options['select'] = 'node.*, (COUNT(parent.`'.$pkField.'`) - (sub_tree.'.$relativeDepthProperty.' + 1)) AS '.$relativeDepthProperty.'';
		}
		$options['from'] = '`'.$db.'`.`'.$table.'` AS node,';
		$options['from'] .= '`'.$db.'`.`'.$table.'` AS parent,';
		$options['from'] .= '`'.$db.'`.`'.$table.'` AS sub_parent,';
		$options['from'] .= '(';
		$options['from'] .=     ' SELECT node.`'.$pkField.'`, (COUNT(parent.`'.$pkField.'`) - 1) AS '.$relativeDepthProperty.'';
		$options['from'] .=     ' FROM `'.$db.'`.`'.$table.'` AS node,';
		$options['from'] .=     ' `'.$db.'`.`'.$table.'` AS parent';
		$options['from'] .=     ' WHERE node.`'.$leftField.'` BETWEEN parent.`'.$leftField.'` AND parent.`'.$rightField.'`';
		$options['from'] .=     ' AND node.`'.$pkField.'` = '.$pkValue.'';
		$options['from'] .=     ' GROUP BY node.`'.$pkField.'`';
		$options['from'] .=     ' ORDER BY node.`'.$leftField.'`';
		$options['from'] .= ') AS sub_tree';
		if(empty($options['where'])){
			$options['where'] = ' 1 ';
		}
		$options['where'] = ' AND node.`'.$leftField.'` BETWEEN parent.`'.$leftField.'` AND parent.`'.$rightField.'`';
		$options['where'] .= ' AND node.`'.$leftField.'` BETWEEN sub_parent.`'.$leftField.'` AND sub_parent.`'.$rightField.'`';
		$options['where'] .= ' AND sub_parent.`'.$pkField.'` = sub_tree.`'.$pkField.'`';
		$options['group_by'] = 'node.`'.$pkField.'`';
		if($depth>0){
			$options['having'] = ''.$relativeDepthProperty.' BETWEEN 1 AND '.$depth;
		}else{
			$options['having'] = ''.$relativeDepthProperty.' > 0';
		}
		$options['order_by'] = 'node.`'.$leftField.'`';
		$children = $record->table->find($options);
		if(!empty($options['return_sql'])||!empty($options['return_builder'])){
			return $children;
		}
		if(!empty($options['flatten'])){
			$flatten = new PorteIterator($record->table,$children);
		}
		$record->associations['children'] = new PorteIterator($record->table);
		$record->attributes[$model['hierarchical']['count_direct_children']] = 0;
		$record->attributes[$model['hierarchical']['count_all_children']] = (PorteTypes::getInt($rightField,$record) - PorteTypes::getInt($leftField,$record) - 1 ) / 2;
		if(!isset($record->attributes[$depthProperty]))$record->attributes[$depthProperty] = PorteHierarchy::_getDepth(null,$record);
		$record->attributes[$relativeDepthProperty] = 0;
		$lastChilds = array($record);
		$lastChild = $record;
		$lastDepth = 1;
		while($child = array_shift($children->array)){
			//if($lastDepth<$child->attributes[$relativeDepthProperty]){
			if($lastDepth<PorteTypes::getInt($relativeDepthProperty,$child)){
				$lastChilds[] = $lastChild;
				$lastDepth++;
			}else if($lastDepth>$child->attributes[$relativeDepthProperty]){
				while($child->attributes['porte_relative_depth']!=$lastDepth){
					array_pop($lastChilds);
					$lastDepth--;
				}
			}
			$lastChild = $child;
			if(!isset($lastChilds[count($lastChilds)-1]->associations['children'])){
				$lastChilds[count($lastChilds)-1]->associations['children'] = new PorteIterator($record->table);
			}
			if(!isset($child->attributes[$model['hierarchical']['count_direct_children']])){
				$child->attributes[$model['hierarchical']['count_direct_children']] = 0;
			}
			//if(!isset($child->attributes[$model['hierarchical']['count_all_children']])){
				//$child->attributes[$model['hierarchical']['count_all_children']] = ($child->attributes[$rightField] - $child->attributes[$leftField] - 1 ) / 2;
				$child->attributes[$model['hierarchical']['count_all_children']] = (PorteTypes::getInt($rightField,$child) - PorteTypes::getInt($leftField,$child) - 1 ) / 2;
			//}
			$child->attributes[$depthProperty] = $lastChilds[count($lastChilds)-1]->attributes[$depthProperty]+1;
			$lastChilds[count($lastChilds)-1]->associations['children']->array[] = $child;
			$lastChilds[count($lastChilds)-1]->attributes[$model['hierarchical']['count_direct_children']]++;
		}
		if(isset($flatten)){
			return $flatten;
		}
		return $record->associations['children'];
	}
	
	/**
	 * Return the parents of the current child. The method only apply
	 * to record which have been previously saved (inserted into the 
	 * rootline) otherwise false is returned.
	 * 
	 * Option include all the options present in 'find' method plus:
	 * 		exclude_itself Include current child in the rootline
	 * 
	 * @return Array of parent records
	 * @param $index Object[optional]
	 */
	public function getRootline(array $options=array()){
		return PorteHierarchy::_getRootline(null,$this->record,$options);
	}
	
	public static function _getRootline($arg,$record,array $options=array()){
		if($record->isNew()) return false;
		$model = $record->porte->models->{$record->type};
		$db = $model['database'];
		$table = $model['table'];
		$rightField = $model['hierarchical']['right'];
		$leftField = $model['hierarchical']['left'];
		$options['select'] = '`parent`.*';
		$options['from'] = '`'.$db.'`.`'.$table.'` AS node, `'.$db.'`.`'.$table.'` AS parent';
		$options['where'] = 'node.`'.$leftField.'` BETWEEN parent.`'.$leftField.'` AND parent.`'.$rightField.'` AND node.`'.$model['primary_key'].'` = \''.$record->getIdentifier().'\'';
		$options['order_by'] = 'parent.`'.$leftField.'`';
		$rootline = $record->table->find($options);
		if(!empty($options['exclude_itself'])) array_pop($rootline->array);
		return $rootline;
	}
	
	/**
	 * Count the depth of the current record relative to the root record. If the current
	 * record is the root record, its depth will always be equals to 0.
	 * 
	 * @return depth (int)
	 * @param $options array[optional]
	 */
	public function getDepth(array $options=array()){
		return PorteHierarchy::_getDepth(null,$this->record,$options);
	}
	
	public static function _getDepth($arg,$record,array $options=array()){
		if($record->isNew()) return false;
		$model = $record->porte->models->{$record->type};
		$depthProperty = $model['hierarchical']['depth'];
		if(isset($record->attributes[$depthProperty])&&empty($options['force']))
			return $record->attributes[$depthProperty];
		$db = $model['database'];
		$table = $model['table'];
		$rightField = $model['hierarchical']['right'];
		$leftField = $model['hierarchical']['left'];
		// Prepare the query
		$options['select'] = '(COUNT(parent.'.$model['primary_key'].') - 1) AS depth';
		$options['from'] = '`'.$db.'`.`'.$table.'` AS node, `'.$db.'`.`'.$table.'` AS parent';
		$options['where'] = 'node.`'.$leftField.'` BETWEEN parent.`'.$leftField.'` AND parent.`'.$rightField.'` AND node.`'.$model['primary_key'].'` = \''.$record->getIdentifier().'\'';
		$options['order_by'] = 'parent.`'.$leftField.'`';
		$statement = $record->porte->query($record->table->find(array_merge(array('return_sql' => true),$options)));
		$count = intval($statement->fetchColumn());
		$statement->closeCursor();
		return $record->attributes[$depthProperty] = $count;
	}
	
	/**
	 * Check wether the current record is the root record.
	 * @return boolean True if records is root record
	 */
	public function isRoot(){
		return PorteHierarchy::_isRoot(null,$this->record);
	}
	
	public static function _isRoot($arg,$record){
		//$metaTable = $record->model->table;
		if($record->isNew()){
			return !$record->table->count();
		}
		$leftField = $record->porte->models->{$record->type}['hierarchical']['left'];
		return (PorteTypes::getInt($leftField,$record)=='1');
	}
	
	/**
	 * Return the root record. There is only one root record for a given type of records.
	 * @return PorteRecord The root record
	 */
	public function getRoot(){
		return PorteHierarchy::_getRoot(null,$this->record);
	}
	
	public static function _getRoot($arg,$record){
		if($root = $record->table->load('`'.$record->porte->models->{$record->type}['hierarchical']['left'].'` = 1',array()))
			return $root;
		throw new PorteException('Root Node Does Not Exist');
	}
	
	/**
	 * Add a child record to the current record.
	 * @return PorteRecord The current record
	 * @param $child PorteRecord
	 */
	public function addChild(PorteRecord $child){
		return PorteHierarchy::_addChild(null,$this->record,$child);
	}
	
	public static function _addChild($arg,$record,PorteRecord $child){
		if(!$child instanceof PorteRecord){
			throw new Exception('F*ck');
		}
		$class = $record->porte->models->{$record->type}['class'];
		if(!$child instanceof $class){
			throw new PorteException('Child is not an instance of "'.$class.'"');
		}
		$child->setParent($record);
		return $record;
	}
	
	/**
	 * Set a record as the parent of the current record, in other words, move the current record
	 * as a child of the provided record.
	 * 
	 * @return PorteRecord The current record
	 * @param $parent PorteRecord The new parent of the current record
	 */
	public function setParent(PorteRecord $parent){
		return PorteHierarchy::_setParent(null,$this->record,$parent);
	}
	
	public static function _setParent($arg,$record,PorteRecord $parent){
		$register = true;
		if(!isset($parent->associations['children'])){
			$parent->associations['children'] = new PorteIterator($parent->table);
		}
		if($record->isNew()){
				foreach($parent->associations['children'] as $child){
					if($child===$record){
						$register = false;
						break;
					}
				}
		}else{
			$currentParent = PorteHierarchy::_getParent($arg,$record);
			if(is_null($currentParent)){
				throw new PorteException('Invalid Operation: root record can not be moved in the hierarchy');
			}else if($currentParent->getIdentifier()==$parent->getIdentifier()){
				$register = false;
			}else{
				$record->hierarchy->_parentOld = $currentParent;
			}
		}
		if($register){
			$parent->associations['children']->array[] = $record;
		}
		$record->associations['parent'] = $parent;
		$parent->hierarchy->events[] = $parent->events->connect('record_save_after',array($parent->hierarchy,'_recordSaveAfter'));
		return $record;
	}
	
	/**
	 * Count the number of children own by this child. By default, it 
	 * return the number of direct descendants, but this behavior can be 
	 * altered with the 'depth' option. If 'depth' is zero or negative, 
	 * all the descendant will be included. If 'depth' is 1, only the direct
	 * descendant are included.
	 * 
	 * Options includes
	 * 		depth int Level of depth to search for, default 1
	 * 
	 * @return int Number of children
	 * @param $options array
	 */
	public function countChildren(array $options=array()){
		return PorteHierarchy::_countChildren(null,$this->record,$options);
	}
	
	public static function _countChildren($arg,$record,array $options=array()){
		// same as countDescendants? descendants = (right – left - 1) / 2
		if(empty($options['depth'])||(!is_int($options['depth'])&&!ctype_digit($options['depth']))){
			$depth = 1;
		}else{
			$depth = intval($options['depth']);
		}
		$model = $record->porte->models->{$record->type};
		$rightValue = PorteTypes::getInt($model['hierarchical']['right'],$record);
		$leftValue = PorteTypes::getInt($model['hierarchical']['left'],$record);
		if($depth<1){
			return ($rightValue-$leftValue-1)/2;
		}else{
			// todo This is ugly, we are simply fetching the children, and count them here
			// need help here, for once, google was not friendly
			$query = PorteHierarchy::_getChildren($arg,$record,array_merge(array(
				'return_sql' => true,
			),$options));
			$count = 0;
			$statement = $record->porte->query($query);
    		while($row = $statement->fetch(Porte::FETCH_ASSOC)){
    			$count++;
			}
			$statement->closeCursor();
			return $count;
		}
	}
	
	public function hasChildren(){
		return PorteHierarchy::_hasChildren(null,$this->record);
	}
	
	public static function _hasChildren($arg,$record){
		$model = $record->porte->models->{$record->type};
		$rightValue = PorteTypes::getInt($model['hierarchical']['right'],$record);
		$leftValue = PorteTypes::getInt($model['hierarchical']['left'],$record);
		return ($rightValue-$leftValue>1);
	}
	
	/**
	 * Remove from the database all the children of the current record.
	 * @return PorteRecord The current record
	 */
	public function deleteChildren(){
		return PorteHierarchy::_deleteChildren(null,$this->record);
	}
	
	public static function _deleteChildren($arg,$record){
		/*
		 * todo: the following is way faster to execute, problem is 
		 * we won't be notified when a child is deleted, meaning we 
		 * won't be able to clean its external associations. a solution
		 * could be to introduce a new property "meta_table[has_external_references]",
		 * set while enriching associations and which will determine the method used 
		 * to remove children.
		
		$queries = array(
			'LOCK TABLE `'.$table.'` WRITE;',
			'DELETE FROM `'.$table.'` WHERE `'.$rightField.'` BETWEEN \''.$leftValue.'\' AND \''.$rightValue.'\';',
			'UPDATE `'.$table.'` SET `'.$rightField.'` = `'.$rightField.'` - \''.$width.'\' WHERE `'.$rightField.'` > \''.$rightValue.'\';',
			'UPDATE `'.$table.'` SET `'.$leftField.'` = `'.$leftField.'` - \''.$width.'\' WHERE `'.$leftField.'` > \''.$rightValue.'\';',
			'UNLOCK TABLES;',
		);
		
		*/
		if(!PorteHierarchy::_hasChildren($arg,$record))
			return $record;
		$children = PorteHierarchy::_getChildren($arg,$record,array(
			'depth'=>-1,
			'flatten'=>true,
		));
		$children = array_reverse($children->array);
		while($child = array_shift($children)){
			$child->delete();
		}
		$record->associations['children'] = new PorteIterator($record->table);
		return $record;
	}
	
	public static function isValid(Porte $porte,$type){
		$model = $porte->models->{$type};
		$leftField = $model['hierarchical']['left'];
		$rightField = $model['hierarchical']['right'];
		$statement = $porte->query('SELECT CONCAT( REPEAT( \'+ \', (COUNT(parent.`'.$model['primary_key'].'`) - 1) ), node.`'.$model['primary_key'].'`) AS info, node.`'.$leftField.'`, node.`'.$rightField.'`, (COUNT(parent.`'.$model['primary_key'].'`)-1) AS level FROM `'.$model['database'].'`.`'.$model['table'].'` AS node, `'.$model['database'].'`.`'.$model['table'].'` AS parent WHERE node.`'.$leftField.'` BETWEEN parent.`'.$leftField.'` AND parent.`'.$rightField.'` GROUP BY node.`'.$model['primary_key'].'` ORDER BY node.`'.$leftField.'`;');
		$stack = array();
		$siblings = array();
		$level = 0;
		$lefts = array();
		$rights = array();
		//array_unique
		while($row = $statement->fetch(Porte::FETCH_ASSOC)){
			$lefts[] = $row[$leftField];
			$rights[] = $row[$rightField];
			$diff = $row['level'] - $level;
			if($diff==0&&$level==0){
				$stack[] = array($row);
				if((int)$row[$leftField]!=1) return false;
			}else if($diff==0){
			// same level
				$previousSiblig = $stack[count($stack)-1];
				$previousSiblig = $previousSiblig[count($previousSiblig)-1];
				$stack[count($stack)-1][] = $row;
				if((int)$previousSiblig[$leftField]!=$previousSiblig[$rightField]-1) return false;
			}else if($diff==1){
			// moving up
				$parent = $stack[count($stack)-1];
				$parent = $parent[count($parent)-1];
				//$parent = $stack[count($stack)+($level-$row['level'])];
				//$parent = $parent[count($parent)-1];
				$stack[] = array($row);
				if((int)$row[$leftField]!=$parent[$leftField]+1) return false;
			}else if($diff<0){
			// moving down
				for($i=$diff;$i<0;$i++)
					array_pop($stack);
				$previousSiblig = $stack[count($stack)-1];
				$previousSiblig = $previousSiblig[count($previousSiblig)-1];
				$parent = $stack[count($stack)-2];
				$parent = $parent[count($parent)-1];
				if((int)$row[$leftField]!=$previousSiblig[$rightField]+1) throw new PorteException('Record '.$row['info'].' is invalid: left field is '.$row[$leftField].' but is expected to be '.($previousSiblig[$rightField]+1));
				//if((int)$row[$rightField]!=$parent[$rightField]-1) throw new PorteException('Record '.$row['info'].' is invalid: right field is '.$row[$rightField].' but is expected to be '.($parent[$rightField]-1));
				$stack[count($stack)-1][] = $row;
			}else{
				return false;
			}
			$level = (int)$row['level'];
			if(count($stack)!=$level+1) return false;
		}
		$statement->closeCursor();
		while($row = array_pop($stack)){
			if(!count($stack)) break;
			$parent = $stack[count($stack)-1];
			$parent = $parent[count($parent)-1];
			$row = $row[count($row)-1];
			//if((int)$row[$rightField]!=$parent[$rightField]-1) return false;
		}
		return true;
	}
	
	public function _recordLoadAfter(){
		$this->events['record_delete_before'] = PorteEvents::connect($this->record->type,'hierarchical_delete_before',array($this,'_HierarchicalDeleteBefore'));
	}
	
	/**
	 * Listener called before a record is created.
	 * 
	 * @return 
	 * @param $record Object
	 */
	public function _recordCreateBefore($record){
		$this->_recordLoadAfter();
		$model = $record->porte->models->{$record->type};
		$leftField = $model['hierarchical']['left'];
		$rightField = $model['hierarchical']['right'];
		$depthProperty = $model['hierarchical']['depth'];
		$this->record->attributes[$model['hierarchical']['count_direct_children']] = 0;
		$this->record->attributes[$model['hierarchical']['count_all_children']] = 0;
		// Check if others records exists
		if(!isset($this->record->associations['parent'])&&$this->record->table->count()){
		// no parent, but if a root node exist, insert this record as last child of the root node
			$root = $this->getRoot();
			$root->attributes[$rightField] += 2;
			$root->save();
			$this->record->attributes[$leftField] = PorteTypes::getInt($rightField,$root)-2;
			$this->record->attributes[$rightField] = PorteTypes::getInt($rightField,$root)-1;
			$this->record->attributes[$depthProperty] = 1;
		}else if(!isset($this->record->associations['parent'])){
		// no root node exist, so this will become the one
			$this->record->attributes[$leftField] = 1;
			$this->record->attributes[$rightField] = 2;
			$this->record->attributes[$depthProperty] = 0;
		}else{
			$parent = $this->record->associations['parent'];
			$db = $model['database'];
			$table = $model['table'];
			//$rightField = $model['hierarchical']['right'];
			//$leftField = $model['hierarchical']['left'];
			$query = 'LOCK TABLE `'.$db.'`.`'.$table.'` WRITE;';
			$this->record->porte->exec($query);
			$query = 'SELECT `'.$rightField.'` FROM `'.$db.'`.`'.$table.'` WHERE `'.$db.'`.`'.$table.'`.`'.$model['primary_key'].'` = \''.$parent->getIdentifier().'\';';
			$right = intval($this->record->porte->query($query)->fetchColumn());
			$query = 'UPDATE `'.$db.'`.`'.$table.'` SET `'.$rightField.'` = `'.$rightField.'` + 2 WHERE `'.$rightField.'` >= '.$right.';';
			$this->record->porte->exec($query);
			$query = 'UPDATE `'.$db.'`.`'.$table.'` SET `'.$leftField.'` = `'.$leftField.'` + 2 WHERE `'.$leftField.'` > '.$right.';';
			$this->record->porte->exec($query);
			$query = 'UNLOCK TABLES;';
			$this->record->porte->exec($query);
			$this->record->attributes[$leftField] = $right;
			$this->record->attributes[$rightField] = $right+1;
			$this->record->attributes[$depthProperty] = $parent->getDepth()+1;
		}
	}
	
	/**
	 */
	public function _recordUpdateBefore(){
		if(!empty($this->_parentOld)){
			$model = $this->record->porte->models->{$this->record->type};
			$newParent = $this->record->getParent();
			$db = $model['database'];
			$table = $model['table'];
			$rightField = $model['hierarchical']['right'];
			$leftField = $model['hierarchical']['left'];
			$rightValue = PorteTypes::getInt($rightField,$this->record);
			$leftValue = PorteTypes::getInt($leftField,$this->record);
			$newParentRightValue = PorteTypes::getInt($rightField,$newParent);
			$newParentLeftValue = PorteTypes::getInt($leftField,$newParent);
			$newParentRightValue = PorteTypes::getInt($rightField,$newParent);
			$newParentLeftValue = PorteTypes::getInt($leftField,$newParent);
			$depthProperty = $model['hierarchical']['depth'];
			$queries = array();
			if($leftValue<$newParentLeftValue && $leftValue<$newParentRightValue){
				$direction = 1;
				$affected_lft  = $leftValue;
				$displaced_lft = $rightValue + 1;
				$displaced_rgt = $newParentRightValue - 1;
				$affected_rgt  = $newParentRightValue - 1;
			}else{
				$direction = -1;
				$affected_lft  = $newParentRightValue;            
				$displaced_lft = $newParentRightValue;
				$displaced_rgt = $leftValue - 1;     
				$affected_rgt  = $rightValue;
				$newParentRightValue = $newParentRightValue+2;
			}
			
			$src_move_offset = $direction * ($displaced_rgt - $displaced_lft + 1);
			$displace_width = -$direction * ($rightValue       - $leftValue       + 1);

			$queries[] = '
				UPDATE `'.$db.'`.`'.$table.'` SET `'.$leftField.'` = CASE
			        WHEN `'.$leftField.'` BETWEEN '.$leftValue.' AND '.$rightValue.' THEN
			            `'.$leftField.'` + '.$src_move_offset.'
			        ELSE
			            `'.$leftField.'` + '.$displace_width.'
			    END
			    WHERE `'.$leftField.'` BETWEEN '.$affected_lft.' AND '.$affected_rgt.';
			';
			$queries[] = '
			    UPDATE `'.$db.'`.`'.$table.'` SET `'.$rightField.'` = CASE
			        WHEN `'.$rightField.'` BETWEEN '.$leftValue.' AND '.$rightValue.' THEN
			            `'.$rightField.'` + '.$src_move_offset.'
			        ELSE
			            `'.$rightField.'` + '.$displace_width.'
			    END
			    WHERE `'.$rightField.'` BETWEEN '.$affected_lft.' AND '.$affected_rgt.';
			';
			
			$this->record->porte->exec($queries);
			$this->record->attributes[$leftField] = PorteTypes::getInt($leftField,$this->record)+$src_move_offset;
			$this->record->attributes[$rightField] = PorteTypes::getInt($rightField,$this->record)+$src_move_offset;
			$this->record->attributes[$depthProperty] = $newParent->getDepth()+1;
			$newParent->attributes[$leftField] = PorteTypes::getInt($leftField,$newParent);
			$newParent->attributes[$rightField] = PorteTypes::getInt($rightField,$newParent)+$displace_width;
		}
	}
	
	public function _recordSaveAfter($record){
		foreach($this->record->associations['children'] as $child){
			$child->save();
		}
	}
	
	public function _recordDeleteBefore(){
		$this->deleteChildren();
		$model = $this->record->porte->models->{$this->record->type};
		$db = $model['database'];
		$table = $model['table'];
		$leftField = $model['hierarchical']['left'];
		$rightField = $model['hierarchical']['right'];
		$leftValue = PorteTypes::getInt($leftField,$this->record);
		$rightValue = PorteTypes::getInt($rightField,$this->record);
		$width = $rightValue - $leftValue + 1;
		$queries = array(
			'LOCK TABLE `'.$db.'`.`'.$table.'` WRITE;',
			//'DELETE FROM `'.$table.'` WHERE `'.$rightField.'` BETWEEN \''.$leftValue.'\' AND \''.$rightValue.'\';',
			'UPDATE `'.$db.'`.`'.$table.'` SET `'.$rightField.'` = `'.$rightField.'` - \''.$width.'\' WHERE `'.$rightField.'` > \''.$rightValue.'\';',
			'UPDATE `'.$db.'`.`'.$table.'` SET `'.$leftField.'` = `'.$leftField.'` - \''.$width.'\' WHERE `'.$leftField.'` > \''.$rightValue.'\';',
			'UNLOCK TABLES;',
		);
		$this->record->porte->exec($queries);
		unset($this->record->associations['parent']);
		unset($this->record->associations['children']);
		// Throw a static event to notify existing parent to update their left and right value
		$this->hierarchicalClean();
		$action = 'hierarchical_delete_before';
		$args = array($this->record,$leftValue,$rightValue);
		PorteEvents::call($model['type'],$action,$args);
	}
	
	public function _HierarchicalDeleteBefore($ref,$refLeft,$refRight){
		$refWidth = $refRight - $refLeft + 1;
		$model = $this->record->porte->models->{$this->record->type};
		$leftField = $model['hierarchical']['left'];
		$rightField = $model['hierarchical']['right'];
		if(!array_key_exists($leftField,$this->record->attributes)){
			$this->record->load($this->record->getIdentifier(),array('force'=>true));
		}
		$leftValue = PorteTypes::getInt($leftField,$this->record);
		$rightValue = PorteTypes::getInt($rightField,$this->record);
		
		$leftValue = $this->record->attributes[$leftField];
		$rightValue = $this->record->attributes[$rightField];
		
		if($rightValue>$refRight)  $this->record->attributes[$rightField] = $rightValue-$refWidth;
		if($leftValue>$refRight)  $this->record->attributes[$leftField] = $leftField-$refWidth;
	}
	
	public function hierarchicalClean(){
		while(count($this->events)){
			$event = array_shift($this->events);
			$this->record->events->disconnect($event);
		}
	}
	
	public static function _modelTableAfter(PorteModels $models,$type){
		if(!empty($models->{$type}['hierarchical'])){
			// todo: remove next line
			if(!is_int($models->{$type}['hierarchical'])&&!ctype_digit($models->{$type}['hierarchical'])){
				$models->{$type}['hierarchical'] = array();
			}
			if(empty($models->{$type}['hierarchical']['right'])){
				$models->{$type}['hierarchical']['right'] = 'porte_right';
			}
			if(empty($models->{$type}['hierarchical']['left'])){
				$models->{$type}['hierarchical']['left'] = 'porte_left';
			}
			if(empty($models->{$type}['hierarchical']['depth'])){
				$models->{$type}['hierarchical']['depth'] = 'porte_depth';
			}
			if(empty($models->{$type}['hierarchical']['relative_depth'])){
				$models->{$type}['hierarchical']['relative_depth'] = 'porte_relative_depth';
			}
			if(empty($models->{$type}['hierarchical']['count_direct_children'])){
				$models->{$type}['hierarchical']['count_direct_children'] = 'porte_count_direct_children';
			}
			if(empty($models->{$type}['hierarchical']['count_all_children'])){
				$models->{$type}['hierarchical']['count_all_children'] = 'porte_count_all_children';
			}
			//if(!isset($model->to_array)) $model->to_array = array();
			// Enrich model table config
			$models->{$type}['to_array']['by_property']['parent'] = array('depth'=>0);
			$models->{$type}['to_array']['exclude'][] = $models->{$type}['hierarchical']['left'];
			$models->{$type}['to_array']['exclude'][] = $models->{$type}['hierarchical']['right'];
			// Add new model properties
			$property = $models->{$type}['hierarchical']['left'];
			PorteModel::sanitizeProperty($models,$type,$property,array('type'=>'int','read_only'));
			$property = $models->{$type}['hierarchical']['right'];
			PorteModel::sanitizeProperty($models,$type,$property,array('type'=>'int','read_only'));
			$property = $models->{$type}['hierarchical']['depth'];
			PorteModel::sanitizeProperty($models,$type,$property,array('type'=>'int','read_only','transient'));
			$property = $models->{$type}['hierarchical']['relative_depth'];
			PorteModel::sanitizeProperty($models,$type,$property,array('type'=>'int','read_only','transient'));
			$property = $models->{$type}['hierarchical']['count_direct_children'];
			PorteModel::sanitizeProperty($models,$type,$property,array('type'=>'int','read_only','transient','lazy'));
			$property = $models->{$type}['hierarchical']['count_all_children'];
			PorteModel::sanitizeProperty($models,$type,$property,array('type'=>'int','read_only','transient','lazy'));
			// Register static events for record type
			PorteEvents::connect($type,'record_get',array('PorteHierarchy','_recordGet'));
			PorteEvents::connect($type,'record_model_after',array('PorteHierarchy','_recordModelBefore'));
			// Register new methods
			PorteModel::addMethod($models,$type,'getParent',null,array('PorteHierarchy','_getParent'));
			PorteModel::addMethod($models,$type,'getSibling',null,array('PorteHierarchy','_getSibling'));
			PorteModel::addMethod($models,$type,'getChildren',null,array('PorteHierarchy','_getChildren'));
			PorteModel::addMethod($models,$type,'getRootline',null,array('PorteHierarchy','_getRootline'));
			PorteModel::addMethod($models,$type,'getDepth',null,array('PorteHierarchy','_getDepth'));
			PorteModel::addMethod($models,$type,'isRoot',null,array('PorteHierarchy','_isRoot'));
			PorteModel::addMethod($models,$type,'getRoot',null,array('PorteHierarchy','_getRoot'));
			PorteModel::addMethod($models,$type,'addChild',null,array('PorteHierarchy','_addChild'));
			PorteModel::addMethod($models,$type,'setParent',null,array('PorteHierarchy','_setParent'));
			PorteModel::addMethod($models,$type,'hasParent',null,array('PorteHierarchy','_hasParent'));
			PorteModel::addMethod($models,$type,'hasChildren',null,array('PorteHierarchy','_hasChildren'));
			PorteModel::addMethod($models,$type,'countChildren',null,array('PorteHierarchy','_countChildren'));
			PorteModel::addMethod($models,$type,'deleteChildren',null,array('PorteHierarchy','_deleteChildren'));
		}
	}
	
	public static function _recordModelBefore($record){
		$model = $record->porte->models->{$record->type};
		//$metaTable = $model->table;
		if(!empty($model['hierarchical'])){
			if(!isset($record->hierarchy))
				$record->hierarchy = new PorteHierarchy($record);
			$record->events->connect('record_load_after',array($record->hierarchy,'_recordLoadAfter'));
			$record->events->connect('record_create_before',array($record->hierarchy,'_recordCreateBefore'));
			$record->events->connect('record_update_before',array($record->hierarchy,'_recordUpdateBefore'));
			$record->events->connect('record_delete_before',array($record->hierarchy,'_recordDeleteBefore'));
			$record->events->connect('record_reset_before',array($record->hierarchy,'hierarchicalClean'));
		}
	}
	
	public static function _recordGet($record,$key){
		if($key=='hierarchy'){
			return $record->hierarchy = new PorteHierarchy($record);
		}
	}
	
}

PorteEvents::connect('model_table_after',array('PorteHierarchy','_modelTableAfter'));

?>
Return current item: Porte