<?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'));
?>