You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
PlexShare/fuel/packages/orm/classes/model/nestedset.php

1858 lines
50 KiB

<?php
/**
* Fuel
*
* Fuel is a fast, lightweight, community driven PHP5 framework.
*
* @package Fuel
* @version 1.8
* @author Fuel Development Team
* @license MIT License
* @copyright 2010 - 2016 Fuel Development Team
* @link http://fuelphp.com
*/
namespace Orm;
/**
* Implements NestedSets (http://en.wikipedia.org/wiki/Nested_set_model)
*
* Some design ideas borrowed from https://github.com/hubspace/fuel-nestedset
* (see http://fuelphp.com/forums/discussion/12206/fuel-nested-sets)
*/
class Model_Nestedset extends Model
{
/*
* @var array tree configuration for this model
*/
// protected static $_tree = array();
/**
* @var array cached tree configurations
*/
protected static $_tree_cached = array();
/*
* @var array nestedset tree configuration defaults
*/
protected static $_defaults = array(
'left_field' => 'left_id', // name of the tree node left index field
'right_field' => 'right_id', // name of the tree node right index field
'tree_field' => null, // name of the tree node tree index field
'title_field' => null, // value of the tree node title field
'read-only' => array(), // list of properties to protect against direct updates
);
// -------------------------------------------------------------------------
// tree configuration
// -------------------------------------------------------------------------
/**
* Get a tree configuration parameter
*
* @param string name of the parameter to get
* @return mixed parameter value, or null if the parameter does not exist
*/
public static function tree_config($name = null)
{
$class = get_called_class();
// configuration not loaded yet
if ( ! array_key_exists($class, static::$_tree_cached))
{
// do we have a custom config for this model?
if (property_exists($class, '_tree'))
{
static::$_tree_cached[$class] = array_merge(static::$_defaults, static::$_tree);
}
else
{
static::$_tree_cached[$class] = static::$_defaults;
}
// array of read-only column names, the can not be set manually
foreach(array('left_field', 'right_field', 'tree_field') as $field)
{
$column = static::tree_config($field) and static::$_tree_cached[$class]['read-only'][] = $column;
}
}
if (func_num_args() == 0)
{
return static::$_tree_cached[$class];
}
else
{
return array_key_exists($name, static::$_tree_cached[$class]) ? static::$_tree_cached[$class][$name] : null;
}
}
// -------------------------------------------------------------------------
// updated constructor, capture un-supported compound PK's
// -------------------------------------------------------------------------
/**
* @var array store the node operation we need to execute on save() or get()
*/
protected $_node_operation = array();
/**
* @var mixed id value of the current tree in multi-tree models
*/
protected $_current_tree_id = null;
/*
* Initialize the nestedset model instance
*
* @param array any data passed to this model
* @param bool whether or not this is a new model instance
* @param string name of a database view to use instead of a table
* @param bool whether or not to cache this object
*
* @throws OutOfBoundsException if the model has a compound primary key defined
*/
public function __construct(array $data = array(), $new = true, $view = null, $cache = true)
{
// check for a compound key, we don't do that (yet)
if (count(static::$_primary_key) > 1)
{
throw new \OutOfBoundsException('The Nestedset ORM model doesn\'t support compound primary keys.');
}
// call the ORM base model constructor
parent::__construct($data, $new, $view, $cache);
}
// -------------------------------------------------------------------------
// multi-tree select
// -------------------------------------------------------------------------
/**
* Select a specific tree if the table contains multiple trees
*
* @param mixed type depends on the field type of the tree_field
* @return Model_Nestedset this object, for chaining
*
* @throws BadMethodCallException if the model is not multi-tree
*/
public function set_tree_id($tree = null)
{
// is this a multi-tree model?
if (static::tree_config('tree_field') === null)
{
throw new \BadMethodCallException('This is not a multi-tree model, set_tree_id() can not be used.');
}
// set the tree filter value to select a specific tree
$this->_current_tree_id = $tree;
// return the object for chaining
return $this;
}
// -------------------------------------------------------------------------
/**
* Select a specific tree if the table contains multiple trees
*
* @return mixed current tree id value
*
* @throws OutOfRangeException if no tree id has been set
*/
public function get_tree_id()
{
// check if the current object is part of a tree
if (($value = $this->{static::tree_config('tree_field')}) !== null)
{
return $value;
}
// check if there is a default tree id value set
if ($this->_current_tree_id !== null)
{
return $this->_current_tree_id;
}
// we needed a tree id, but there isn't one defined
throw new \OutOfRangeException('tree id required, but none is defined.');
}
// -------------------------------------------------------------------------
// tree queries
// -------------------------------------------------------------------------
/**
* Returns a query object on the selected tree
*
* @param bool whether or not to include related models
*
* @return Query the constructed query object
*/
public function build_query($include_related = true)
{
// create a new query object
$query = $this->query();
// get params to avoid excessive method calls
$tree_field = static::tree_config('tree_field');
// add the tree id if needed
if ( ! is_null($tree_field))
{
$query->where($tree_field, $this->get_tree_id());
}
// add any relations if needed
if ($include_related and isset($this->_node_operation['related']))
{
foreach ($this->_node_operation['related'] as $relation => $conditions)
{
$query->related($relation, $conditions);
}
}
// return the query object
return $query;
}
// -------------------------------------------------------------------------
/**
* Returns the root of the tree the current node belongs to
*
* @return Model_Nestedset this object, for chaining
*/
public function root()
{
$this->_node_operation = array(
'related' => array(),
'single' => true,
'action' => 'root',
'to' => null,
);
// return the object for chaining
return $this;
}
// -------------------------------------------------------------------------
/**
* Returns the roots of all trees
*
* @return Model_Nestedset this object, for chaining
*/
public function roots()
{
$this->_node_operation = array(
'related' => array(),
'single' => false,
'action' => 'roots',
'to' => null,
);
// return the object for chaining
return $this;
}
// -------------------------------------------------------------------------
/**
* Returns the parent of the current node
*
* @return Model_Nestedset this object, for chaining
*/
public function parent()
{
$this->_node_operation = array(
'related' => array(),
'single' => true,
'action' => 'parent',
'to' => null,
);
// return the object for chaining
return $this;
}
// -------------------------------------------------------------------------
/**
* Returns the children of the current node
*
* @return Model_Nestedset this object, for chaining
*/
public function children()
{
$this->_node_operation = array(
'related' => array(),
'single' => false,
'action' => 'children',
'to' => null,
);
// return the object for chaining
return $this;
}
// -------------------------------------------------------------------------
/**
* Returns all ancestors of the current node
*
* @return Model_Nestedset this object, for chaining
*/
public function ancestors()
{
$this->_node_operation = array(
'related' => array(),
'single' => false,
'action' => 'ancestors',
'to' => null,
);
// return the object for chaining
return $this;
}
// -------------------------------------------------------------------------
/**
* Returns all descendants of the current node
*
* @return Model_Nestedset this object, for chaining
*/
public function descendants()
{
$this->_node_operation = array(
'related' => array(),
'single' => false,
'action' => 'descendants',
'to' => null,
);
// return the object for chaining
return $this;
}
// -------------------------------------------------------------------------
/**
* Returns all leafs of the current node
*
* @return Model_Nestedset this object, for chaining
*/
public function leaf_descendants()
{
$this->_node_operation = array(
'related' => array(),
'single' => false,
'action' => 'leaf_descendants',
'to' => null,
);
// return the object for chaining
return $this;
}
// -------------------------------------------------------------------------
/**
* Returns the siblings of the current node (includes the node itself!)
*
* @return Model_Nestedset this object, for chaining
*/
public function siblings()
{
$this->_node_operation = array(
'related' => array(),
'single' => false,
'action' => 'siblings',
'to' => null,
);
// return the object for chaining
return $this;
}
// -------------------------------------------------------------------------
/**
* Returns the path to the current node
*
* @return Model_Nestedset this object, for chaining
*/
public function path($addroot = true)
{
$this->_node_operation = array(
'related' => array(),
'single' => false,
'action' => 'path',
'to' => null,
'addroot' => $addroot,
);
// return the object for chaining
return $this;
}
// -------------------------------------------------------------------------
// node manipulation methods
// -------------------------------------------------------------------------
/**
* Alias for last_child()
*
* @param Model_Nestedset, or PK of the parent object, or null
* @return Model_Nestedset this object, for chaining
*/
public function child($to = null)
{
return $this->last_child($to);
}
// -------------------------------------------------------------------------
/**
* Gets or sets the first child of a node
*
* @param Model_Nestedset, or PK of the parent object, or null
* @return Model_Nestedset this object, for chaining
*/
public function first_child($to = null)
{
$this->_node_operation = array(
'related' => array(),
'single' => true,
'action' => 'first_child',
'to' => $to,
);
// return the object for chaining
return $this;
}
// -------------------------------------------------------------------------
/**
* Gets or sets the last child of a node
*
* @param Model_Nestedset, or PK of the parent object, or null
* @return Model_Nestedset this object, for chaining
*/
public function last_child($to = null)
{
$this->_node_operation = array(
'related' => array(),
'single' => true,
'action' => 'last_child',
'to' => $to,
);
// return the object for chaining
return $this;
}
// -------------------------------------------------------------------------
/**
* Alias for next_sibling()
*
* @param Model_Nestedset, or PK of the parent object, or null
* @return Model_Nestedset this object, for chaining
*/
public function sibling($to = null)
{
return $this->next_sibling($to);
}
// -------------------------------------------------------------------------
/**
* Gets or sets the previous sibling of a node
*
* @param Model_Nestedset, or PK of the parent object, or null
* @return Model_Nestedset this object, for chaining
*/
public function previous_sibling($to = null)
{
$this->_node_operation = array(
'related' => array(),
'single' => true,
'action' => 'previous_sibling',
'to' => $to,
);
// return the object for chaining
return $this;
}
// -------------------------------------------------------------------------
/**
* Gets or sets the next sibling of a node
*
* @param Model_Nestedset, or PK of the parent object, or null
* @return Model_Nestedset this object, for chaining
*/
public function next_sibling($to = null)
{
$this->_node_operation = array(
'related' => array(),
'single' => true,
'action' => 'next_sibling',
'to' => $to,
);
// return the object for chaining
return $this;
}
// -------------------------------------------------------------------------
// boolean tree functions
// -------------------------------------------------------------------------
/**
* Check if the object is a tree root
*
* @return bool
*/
public function is_root()
{
return $this->{static::tree_config('left_field')} == 1;
}
// -------------------------------------------------------------------------
/**
* Check if the object is a tree leaf (node with no children)
*
* @return bool
*/
public function is_leaf()
{
return $this->{static::tree_config('right_field')} - $this->{static::tree_config('left_field')} == 1;
}
// -------------------------------------------------------------------------
/**
* Check if the object is a child node (not a root node)
*
* @return bool
*/
public function is_child()
{
return ! $this->is_root($this);
}
// -------------------------------------------------------------------------
/**
* Check if the object is a child of node
*
* @param Model_Nestedset of the parent to check
* @return bool
*/
public function is_child_of(Model_Nestedset $parent)
{
// get our parent
$our_parent = $this->parent()->get_one();
// and check if the parents match
return $parent == $our_parent;
}
// -------------------------------------------------------------------------
/**
* Check if the object is a direct descendant of node
*
* @param Model_Nestedset of the parent to check
* @return bool
*/
public function is_descendant_of(Model_Nestedset $parent)
{
// get params to avoid excessive method calls
$left_field = static::tree_config('left_field');
$right_field = static::tree_config('right_field');
return $this->{$left_field} > $parent->{$left_field} and
$this->{$right_field} < $parent->{$right_field};
}
// -------------------------------------------------------------------------
/**
* Check if the object is the parent of node
*
* @param Model_Nestedset of the child to check
* @return bool
*/
public function is_parent_of(Model_Nestedset $child)
{
return $this == $child->parent()->get_one();
}
// -------------------------------------------------------------------------
/**
* Check if the object is the ancestor of node
*
* @param Model_Nestedset of the child to check
* @return bool
*/
public function is_ancestor_of(Model_Nestedset $child)
{
// get params to avoid excessive method calls
$left_field = static::tree_config('left_field');
$right_field = static::tree_config('right_field');
return $child->{$left_field} > $this->{$left_field} and
$child->{$right_field} < $this->{$right_field};
}
// -------------------------------------------------------------------------
/**
* Check if the object is the same model
*
* @param Model_Nestedset object to verify against
* @return bool
*/
public function is_same_model_as(Model_Nestedset $object)
{
return (get_class($object) == get_class($this));
}
// -------------------------------------------------------------------------
/**
* Check if the object is the same model and the same tree
*
* @param Model_Nestedset object to verify against
* @return bool
*/
public function is_same_tree_as(Model_Nestedset $object)
{
// make sure they're the same model
if ($this->is_same_model_as($object))
{
// get params to avoid excessive method calls
$tree_field = static::tree_config('tree_field');
if (empty($this->{$tree_field}) or $this->{$tree_field} === $object->{$tree_field})
{
// same tree, or not a multi-tree model
return true;
}
}
// not the same tree
return false;
}
// -------------------------------------------------------------------------
/**
* Check if the object has a parent
*
* Note: this is an alias for is_child()
*
* @return bool
*/
public function has_parent()
{
return $this->is_child($this);
}
// -------------------------------------------------------------------------
/**
* Check if the object has children
*
* @return bool
*/
public function has_children()
{
return $this->is_leaf($this) ? false : true;
}
// -------------------------------------------------------------------------
/**
* Check if the object has a previous sibling
*
* @return bool
*/
public function has_previous_sibling()
{
return ! is_null($this->previous_sibling()->get_one());
}
// -------------------------------------------------------------------------
/**
* Check if the object has a next sibling
*
* @return bool
*/
public function has_next_sibling()
{
return ! is_null($this->next_sibling()->get_one());
}
// -------------------------------------------------------------------------
// integer tree methods
// -------------------------------------------------------------------------
/**
* Return the count of the objects children
*
* @return mixed integer, or false in case no valid object was passed
*/
public function count_children()
{
$result = $this->children()->get();
return $result ? count($result) : 0;
}
// -------------------------------------------------------------------------
/**
* Return the count of the objects descendants
*
* @return mixed integer, or false in case no valid object was passed
*/
public function count_descendants()
{
return ($this->{static::tree_config('right_field')} - $this->{static::tree_config('left_field')} - 1) / 2;
}
// -------------------------------------------------------------------------
/**
* Return the depth of the object in the tree, where the root = 0
*
* @return mixed integer, of false in case no valid object was found
*/
public function depth()
{
// get params to avoid excessive method calls
$left_field = static::tree_config('left_field');
$right_field = static::tree_config('right_field');
// we need a valid object for this to work
if ($this->is_new())
{
return false;
}
else
{
// if we have a valid object, run the query to calculate the depth
$query = $this->build_query(false)
->where($left_field, '<', $this->{$left_field})
->where($right_field, '>', $this->{$right_field});
// return the result count
return $query->count();
}
}
// -------------------------------------------------------------------------
/**
* Return the tree, with the current node as root, as a nested array structure
*
* @param bool whether or not to return an array of objects
* @param string property name to store the node's children
* @param string property name to store the node's display path
* @param string property name to store the node's uri path
* @return array
*/
public function dump_tree($as_object = false, $children = 'children', $path = 'path', $pathuri = null)
{
// get the PK
$pk = reset(static::$_primary_key);
// and the tree pointers
$left_field = static::tree_config('left_field');
$right_field = static::tree_config('right_field');
$title_field = static::tree_config('title_field');
// storage for the result, start with the current node
if ($as_object)
{
$this->_custom_data[$children] = array();
$tree = array($this->{$pk} => $this);
}
else
{
$this[$children] = array();
$tree = array($this->{$pk} => $this->to_array(true));
}
if ( ! empty($title_field) and isset($this->{$title_field}))
{
if ($as_object)
{
$this->_custom_data[$path] = '/';
$pathuri and $this->_custom_data['path_'.$pathuri] = '/';
}
else
{
$tree[$this->{$pk}][$path] = '/';
$pathuri and $tree[$this->{$pk}]['path_'.$pathuri] = '/';
}
}
// parent tracker
$tracker = array();
$index = 0;
$tracker[$index] =& $tree[$this->{$pk}];
// loop over the descendants
foreach ($this->descendants()->get() as $treenode)
{
// get the data for this node and make sure we have a place to store child information
if ($as_object)
{
$node = $treenode;
$node->_custom_data[$children] = array();
}
else
{
$node = $treenode->to_array(true);
$node[$children] = array();
}
// is this node a child of the current parent?
while ($treenode->{$left_field} > $tracker[$index][$right_field])
{
// no, so pop the last parent and move a level back up
$index--;
}
// add the path to this node
if ( ! empty($title_field) and isset($treenode->{$title_field}))
{
if ($as_object)
{
$node->_custom_data[$path] = rtrim($tracker[$index][$path], '/').'/'.$node->{$title_field};
$pathuri and $node->_custom_data['path_'.$pathuri] = rtrim($tracker[$index]['path_'.$pathuri], '/').'/'.$node->{$pathuri};
}
else
{
$node[$path] = rtrim($tracker[$index][$path], '/').'/'.$node[$title_field];
$pathuri and $node['path_'.$pathuri] = rtrim($tracker[$index]['path_'.$pathuri], '/').'/'.$node[$pathuri];
}
}
// add it as a child to the current parent
if ($as_object)
{
$tracker[$index]->_custom_data[$children][$treenode->{$pk}] = $node;
}
else
{
$tracker[$index][$children][$treenode->{$pk}] = $node;
}
// does this node have children?
if ($treenode->{$right_field} - $treenode->{$left_field} > 1)
{
// create a new parent level
if ($as_object)
{
$tracker[$index+1] =& $tracker[$index]->_custom_data[$children][$treenode->{$pk}];
}
else
{
$tracker[$index+1] =& $tracker[$index][$children][$treenode->{$pk}];
}
$index++;
}
}
return $as_object ? $this : $tree;
}
// -------------------------------------------------------------------------
/**
* Capture __unset() to make sure no read-only properties are erased
*
* @param string $property
*/
public function __unset($property)
{
// make sure we're not unsetting a read-only value
if (in_array($property, static::tree_config('read-only')))
{
throw new \InvalidArgumentException('Property "'.$property.'" is read-only and can not be changed');
}
parent::__unset($property);
}
// -------------------------------------------------------------------------
/**
* Capture set() to make sure no read-only properties are overwritten
*
* @param string|array $property
* @param string $value in case $property is a string
* @return Model
*/
public function set($property, $value = null)
{
// check if we're in a frozen state
if ($this->_frozen)
{
throw new FrozenObject('No changes allowed.');
}
// make sure we're not setting a read-only value
if (in_array($property, static::tree_config('read-only')) and $this->{$property} !== $value)
{
throw new \InvalidArgumentException('Property "'.$property.'" is read-only and can not be changed');
}
return parent::set($property, $value);
}
// -------------------------------------------------------------------------
/**
* Capture calls to save(), to make sure no new record is inserted
* directly which would seriously break the tree...
*/
public function save($cascade = null, $use_transaction = false)
{
// get params to avoid excessive method calls
$tree_field = static::tree_config('tree_field');
$left_field = static::tree_config('left_field');
$right_field = static::tree_config('right_field');
// deal with new objects
if ($this->_is_new)
{
// was a relocation of this node asked?
if ( ! empty($this->_node_operation))
{
if ($this->_node_operation['to'])
{
// do we have a model? if not, try to autoload it
if ( ! $this->_node_operation['to'] instanceOf Model_Nestedset)
{
$this->_node_operation['to'] = static::find($this->_node_operation['to']);
}
// verify that both objects are from the same model
$this->_same_model_as($this->_node_operation['to'], __METHOD__);
// set the tree id if needed
if ( ! is_null($tree_field))
{
$this->_data[$tree_field] = $this->_node_operation['to']->get_tree_id();
}
}
// add the left- and right pointers to the current object, and make room for it
if ($use_transaction)
{
$db = \Database_Connection::instance(static::connection(true));
$db->start_transaction();
}
try
{
switch ($this->_node_operation['action'])
{
case 'next_sibling':
// set the left- and right pointers for the new node
$this->_data[$left_field] = $this->_node_operation['to']->{$right_field} + 1;
$this->_data[$right_field] = $this->_node_operation['to']->{$right_field} + 2;
// create room for this new node
$this->_shift_rl_values($this->{$left_field}, 2);
break;
case 'previous_sibling':
// set the left- and right pointers for the new node
$this->_data[$left_field] = $this->_node_operation['to']->{$left_field};
$this->_data[$right_field] = $this->_node_operation['to']->{$left_field} + 1;
// create room for this new node
$this->_shift_rl_values($this->{$left_field}, 2);
break;
case 'first_child':
// set the left- and right pointers for the new node
$this->_data[$left_field] = $this->_node_operation['to']->{$left_field} + 1;
$this->_data[$right_field] = $this->_node_operation['to']->{$left_field} + 2;
// create room for this new node
$this->_shift_rl_values($this->{$left_field}, 2);
break;
case 'last_child':
// set the left- and right pointers for the new node
$this->_data[$left_field] = $this->_node_operation['to']->{$right_field};
$this->_data[$right_field] = $this->_node_operation['to']->{$right_field} + 1;
// create room for this new node
$this->_shift_rl_values($this->{$left_field}, 2);
break;
default:
throw new \OutOfBoundsException('You can not define a '.$this->_node_operation['action'].'() action before a save().');
break;
}
}
catch (\Exception $e)
{
$use_transaction and $db->rollback_transaction();
throw $e;
}
}
// assume we want a new root node
else
{
// set the left- and right pointers for the new root
$this->_data[$left_field] = 1;
$this->_data[$right_field] = 2;
$pk = reset(static::$_primary_key);
// we need to check if we don't already have this root
$query = \DB::select($pk)
->from(static::table())
->where($left_field, '=', 1);
// multi-root tree? And no new tree id defined?
if ( ! is_null($tree_field) and empty($this->{$tree_field}))
{
// get the next free tree id, and hope it's numeric...
$this->_data[$tree_field] = $this->max($tree_field) + 1;
// and set it as the default tree id for this node
$this->set_tree_id($this->_data[$tree_field]);
}
// add the tree_id to the query if present
if ( ! empty($this->{$tree_field}))
{
$query->where($tree_field, '=', $this->_data[$tree_field]);
}
// run the check
$result = $query->execute(static::connection());
// any hits?
if (count($result))
{
throw new \OutOfBoundsException('You can not add this new tree root, it already exists.');
}
}
}
// and with existing objects
else
{
// get the classname of this model
$class = get_called_class();
// readonly fields may not be changed
foreach (static::$_tree_cached[$class]['read-only'] as $column)
{
// so reset them if they were changed
$this->_data[$column] = $this->_original[$column];
}
// was a relocation of this node asked
if ( ! empty($this->_node_operation))
{
if ($this->_node_operation['to'])
{
// do we have a model? if not, try to autoload it
if ( ! $this->_node_operation['to'] instanceOf Model_Nestedset)
{
$this->_node_operation['to'] = static::find($this->_node_operation['to']);
}
// verify that both objects are from the same model
$this->_same_model_as($this->_node_operation['to'], __METHOD__);
// and from the same tree (if we have multi-tree support for this object)
if ( ! is_null($tree_field))
{
if ($this->{$tree_field} !== $this->_node_operation['to']->{$tree_field})
{
throw new \OutOfBoundsException('When moving nodes, nodes must be part of the same tree.');
}
}
}
// move the node
if ($use_transaction)
{
$db = \Database_Connection::instance(static::connection(true));
$db->start_transaction();
}
try
{
switch ($this->_node_operation['action'])
{
case 'next_sibling':
$this->_move_subtree($this->_node_operation['to']->{static::tree_config('right_field')} + 1);
break;
case 'previous_sibling':
$this->_move_subtree($this->_node_operation['to']->{static::tree_config('left_field')});
break;
case 'first_child':
$this->_move_subtree($this->_node_operation['to']->{static::tree_config('left_field')} + 1);
break;
case 'last_child':
$this->_move_subtree($this->_node_operation['to']->{static::tree_config('right_field')});
break;
default:
throw new \OutOfBoundsException('You can not define a '.$this->_node_operation['action'].'() action before a save().');
break;
}
}
catch (\Exception $e)
{
$use_transaction and $db->rollback_transaction();
throw $e;
}
}
}
// reset the node operation store to make sure nothings pending...
$this->_node_operation = array();
// save the current node and return the result
return parent::save($cascade, $use_transaction);
}
// -------------------------------------------------------------------------
/**
* Capture calls to delete(), to make sure no delete happens without reindexing
*
* @param mixed $cascade
* null = use default config,
* bool = force/prevent cascade,
* array cascades only the relations that are in the array
* @return Model this instance as a new object without primary key(s)
*
* @throws DomainException if you try to delete a root node with multiple children
*/
public function delete($cascade = null, $use_transaction = false)
{
if ($use_transaction)
{
$db = \Database_Connection::instance(static::connection(true));
$db->start_transaction();
}
// get params to avoid excessive method calls
$left_field = static::tree_config('left_field');
$right_field = static::tree_config('right_field');
// if this is a root node with multiple children, bail out
if ($this->is_root() and $this->count_children() > 1)
{
throw new \DomainException('You can not delete a tree root with multiple children.');
}
// put the entire operation in a try/catch, so we can rollback if needed
try
{
// delete the node itself
$result = parent::delete($cascade);
// check if the delete was succesful
if ($result !== false)
{
// re-index the tree
$this->_shift_rl_range($this->{$left_field} + 1, $this->{$right_field} - 1, -1);
$this->_shift_rl_values($this->{$right_field} + 1, -2);
}
}
catch (\Exception $e)
{
$use_transaction and $db->rollback_transaction();
throw $e;
}
// reset the node operation store to make sure nothings pending...
$this->_node_operation = array();
// and return the result
return $result;
}
// -------------------------------------------------------------------------
// tree destructors
// -------------------------------------------------------------------------
/**
* Deletes the entire tree structure using the current node as starting point
*
* @param mixed $cascade
* null = use default config,
* bool = force/prevent cascade,
* array cascades only the relations that are in the array
* @return Model this instance as a new object without primary key(s)
*/
public function delete_tree($cascade = null, $use_transaction = false)
{
if ($use_transaction)
{
$db = \Database_Connection::instance(static::connection(true));
$db->start_transaction();
}
// get params to avoid excessive method calls
$left_field = static::tree_config('left_field');
$right_field = static::tree_config('right_field');
$pk = reset(static::$_primary_key);
// put the entire operation in a try/catch, so we can rollback if needed
try
{
// check if the node has children
if ($this->has_children())
{
// get them
$children = $this->children()->get();
// and delete them to
foreach ($children as $child)
{
if ($child->delete_tree($cascade) === false)
{
throw new \UnexpectedValueException('delete of child node with PK "'.$child->{$pk}.'" failed.');
}
}
}
// delete the node itself
$result = parent::delete($cascade);
// check if the delete was succesful
if ($result !== false)
{
// re-index the tree
$this->_shift_rl_values($this->{$right_field} + 1, $this->{$left_field} - $this->{$right_field} - 1);
}
}
catch (\Exception $e)
{
$use_transaction and $db->rollback_transaction();
throw $e;
}
// reset the node operation store to make sure nothings pending...
$this->_node_operation = array();
// and return the result
return $result;
}
// -------------------------------------------------------------------------
// get methods
// -------------------------------------------------------------------------
/**
* Creates a new query with optional settings up front, or return a pre-build
* query to further chain upon
*
* @param array
* @return Query
*/
public function get_query()
{
// make sure there's a node operation defined
if (empty($this->_node_operation))
{
// assume a get-all operation
$this->_node_operation = array(
'related' => array(),
'single' => false,
'action' => 'all',
'to' => null,
);
}
return $this->_fetch_nodes('query');
}
// -------------------------------------------------------------------------
/**
* Get one or more tree nodes, and provide fallback for
* the original model getter
*
* @param mixed
*
* @returns mixed
* @throws BadMethodCallException if called without a parameter and without a node to fetch
*/
public function & get($query = null, array $conditions = array())
{
// do we have any parameters passed?
if (func_num_args())
{
// capture normal getter calls
if ($query instanceOf Query)
{
// run a get() on the query
return $query->get();
}
else
{
// assume it's a model getter call
return parent::get($query, $conditions);
}
}
// make sure there's a node operation defined
if (empty($this->_node_operation))
{
// assume a get-all operation
$this->_node_operation = array(
'related' => array(),
'single' => false,
'action' => 'all',
'to' => null,
);
}
// no parameters, so we need to fetch something
$result = $this->_fetch_nodes('multiple');
return $result;
}
// -------------------------------------------------------------------------
/*
* Get a single tree node
*
* @param Query
*
* @returns mixed
* @throws BadMethodCallException if called without a parameter and without a node to fetch
*/
public function get_one(Query $query = null)
{
// do we have a query object passed?
if (func_num_args())
{
// return the query result
return $query->get_one();
}
// make sure there's a node operation defined
if (empty($this->_node_operation))
{
// assume a get-all operation
$this->_node_operation = array(
'related' => array(),
'single' => true,
'action' => 'all',
'to' => null,
);
}
// so we need to fetch something
return $this->_fetch_nodes('single');
}
/**
* Set a relation to include
*
* @param string $relation
* @param array $conditions Optionally
*
* @return $this
*/
public function related($relation, $conditions = array())
{
// make sure there's a node operation defined
if (empty($this->_node_operation))
{
// assume a get-all operation
$this->_node_operation = array(
'related' => array(),
'single' => false,
'action' => 'all',
'to' => null,
);
}
// store the relation to include
$this->_node_operation['related'][$relation] = $conditions;
return $this;
}
// -------------------------------------------------------------------------
// protected class functions
// -------------------------------------------------------------------------
/**
* Check if the object passed is an instance of the current model
*
* @param Model_Nestedset
* @param string optional method name to display in the exception message
* @return bool
*
* @throws OutOfBoundsException in case the two objects are not part of the same model
*/
protected function _same_model_as($object, $method = 'unknown')
{
if ( ! $this->is_same_model_as($object))
{
throw new \OutOfBoundsException('Model object passed to '.$method.'() is not an instance of '.get_class($this).'.');
}
}
// -------------------------------------------------------------------------
/*
* Fetch a node or nodes, and return the result
*
* @param string action, either 'single' or 'multiple'
* @return mixed Model_Nestedset or an array of Model_Nestedset, or null if none found
*
* @throws \UnexpectedValueException Relation was not found in the model
*/
protected function _fetch_nodes($action)
{
// get params to avoid excessive method calls
$left_field = static::tree_config('left_field');
$right_field = static::tree_config('right_field');
$tree_field = static::tree_config('tree_field');
// construct the query
switch ($this->_node_operation['action'])
{
case 'all':
$query = $this->build_query();
break;
case 'root':
$query = $this->build_query()
->where($left_field, '=', 1);
break;
case 'roots':
$query = $this->query()
->where($left_field, '=', 1);
break;
case 'parent':
$query = $this->build_query()
->where($left_field, '<', $this->{$left_field})
->where($right_field, '>', $this->{$right_field})
->order_by($right_field, 'ASC');
break;
case 'first_child':
$query = $this->build_query()
->where($left_field, $this->{$left_field} + 1);
break;
case 'last_child':
$query = $this->build_query()
->where($right_field, $this->{$right_field} - 1);
break;
case 'children':
// get the PK's of all child objects
$pk = reset(static::$_primary_key);
$left = $this->{$left_field};
$right = $this->{$right_field};
// if we're multitree, add the tree filter to the query
if (is_null($tree_field))
{
$query = \DB::select('child.'.$pk)
->from(array(static::table(), 'child'))
->join(array(static::table(), 'ancestor'), 'left')
->on(\DB::identifier('ancestor.' . $left_field), 'BETWEEN', \DB::expr(($left + 1) . ' AND ' . ($right - 1)))
->on(\DB::identifier('child.' . $left_field), 'BETWEEN', \DB::expr(\DB::identifier('ancestor.'.$left_field).' + 1 AND '.\DB::identifier('ancestor.'.$right_field).' - 1'))
->where(\DB::identifier('child.' . $left_field), 'BETWEEN', \DB::expr(($left + 1) . ' AND ' . ($right - 1)))
->and_where('ancestor.'.$pk, null);
}
else
{
$query = \DB::select('child.'.$pk)
->from(array(static::table(), 'child'))
->join(array(static::table(), 'ancestor'), 'left')
->on(\DB::identifier('ancestor.' . $left_field), 'BETWEEN', \DB::expr(($left + 1) . ' AND ' . ($right - 1) . ' AND '.\DB::identifier('ancestor.'.$tree_field).' = '.$this->get_tree_id()))
->on(\DB::identifier('child.' . $left_field), 'BETWEEN', \DB::expr(\DB::identifier('ancestor.'.$left_field).' + 1 AND '.\DB::identifier('ancestor.'.$right_field).' - 1'))
->where(\DB::identifier('child.' . $left_field), 'BETWEEN', \DB::expr(($left + 1) . ' AND ' . ($right - 1)))
->and_where('ancestor.'.$pk, null)
->and_where('child.'.$tree_field, '=', $this->get_tree_id());
}
// extract the PK's, and bail out if no children found
if ( ! $pks = $query->execute(static::connection())->as_array())
{
return null;
}
// construct the query to find all child objects
$query = $this->build_query()
->where($pk, 'IN', $pks)
->order_by($left_field, 'ASC');
break;
case 'ancestors':
// storage for the result
$result = array();
// new objects don't have a parent
if ( ! $this->is_new())
{
$parent = $this;
$pk = reset(static::$_primary_key);
while (($parent = $parent->parent()->get_one()) !== null)
{
$result[$parent->{$pk}] = $parent;
}
}
// reverse the result
$result = array_reverse($result, true);
// return the result
return $result;
break;
case 'descendants':
$query = $this->build_query()
->where($left_field, '>', $this->{$left_field})
->where($right_field, '<', $this->{$right_field})
->order_by($left_field, 'ASC');
break;
case 'leaf_descendants':
$query = $this->build_query()
->where($left_field, '>', $this->{$left_field})
->where($right_field, '<', $this->{$right_field})
->where(\DB::expr(\DB::quote_identifier($right_field) . ' - ' . \DB::quote_identifier($left_field)), '=', 1)
->order_by($left_field, 'ASC');
break;
case 'previous_sibling':
$query = $this->build_query()
->where(static::tree_config('right_field'), $this->{static::tree_config('left_field')} - 1);
break;
case 'next_sibling':
$query = $this->build_query()
->where(static::tree_config('left_field'), $this->{static::tree_config('right_field')} + 1);
break;
case 'siblings':
// if we have a parent object
if ($parent = $this->parent()->get_one())
{
// get the children of that parent
return $parent->children()->get();
}
else
{
// no siblings
return null;
}
break;
case 'path':
// do we have a title field defined?
if ($title_field = static::tree_config('title_field'))
{
// storage for the path
$path = '';
// do we need to add the root?
$addroot = $this->_node_operation['addroot'];
// get all parents
$result = $this->ancestors()->get();
// construct the path
foreach($result as $object)
{
if ($addroot or $object->{$left_field} > 1)
{
$path .= $object->{$title_field}.'/';
}
}
$path .= $this->{$title_field};
// and return it
return $path;
}
else
{
throw new \OutOfBoundsException('You can call path(), the "'.get_class($this).'" model does not define a title field.');
}
break;
default:
throw new \OutOfBoundsException('You can not set a '.$this->_node_operation['action'].'() operation on a get() or get_one().');
break;
}
// reset the node operation store to make sure nothings pending...
$this->_node_operation = array();
if ($action == 'query')
{
// return the query object for further chaining
return $query;
}
else
{
// return the query result based on the action type
return $action == 'single' ? $query->get_one() : $query->get();
}
}
// -------------------------------------------------------------------------
/**
* Interal tree operation. Shift left-right pointers to make room for
* one or mode nodes, or to re-order the pointers after a delete
* operation.
*
* @param integer left pointer of the first node to shift
* @param integer number of positions to shift (if negative the shift will be to the left)
*/
protected function _shift_rl_values($first, $delta)
{
// get params to avoid excessive method calls
$tree_field = static::tree_config('tree_field');
$left_field = static::tree_config('left_field');
$right_field = static::tree_config('right_field');
$query = \DB::update(static::table());
// if we have multiple roots
if ( ! is_null($tree_field))
{
$query->where($tree_field, $this->get_tree_id());
}
$query->where($left_field, '>=', $first);
// correct the delta
$sqldelta = ($delta < 0) ? (' - '.abs($delta)) : (' + '.$delta);
// set clause
$query->set(array(
$left_field => \DB::expr(\DB::quote_identifier($left_field).$sqldelta),
));
// update in the correct order to avoid constraint conflicts
$query->order_by($left_field, ($delta < 0 ? 'ASC' : 'DESC'));
// execute it
$query->execute(static::connection(true));
$query = \DB::update(static::table());
// if we have multiple roots
if ( ! is_null($tree_field))
{
$query->where($tree_field, $this->get_tree_id());
}
$query->where($right_field, '>=', $first);
// set clause
$query->set(array(
$right_field => \DB::expr(\DB::quote_identifier($right_field).$sqldelta),
));
// update in the correct order to avoid constraint conflicts
$query->order_by($right_field, ($delta < 0 ? 'ASC' : 'DESC'));
// execute it
$query->execute(static::connection(true));
// update cached objects, we've modified pointers
$class = get_called_class();
if (array_key_exists($class, static::$_cached_objects))
{
foreach (static::$_cached_objects[$class] as $object)
{
if (is_null($tree_field) or $object->{$tree_field} == $this->{$tree_field})
{
if ($object->{$left_field} >= $first)
{
if ($delta < 0)
{
$object->_data[$left_field] -= abs($delta);
$object->_original[$left_field] -= abs($delta);
}
else
{
$object->_data[$left_field] += $delta;
$object->_original[$left_field] += $delta;
}
}
if ($object->{$right_field} >= $first)
{
if ($delta < 0)
{
$object->_data[$right_field] -= abs($delta);
$object->_original[$right_field] -= abs($delta);
}
else
{
$object->_data[$right_field] += $delta;
$object->_original[$right_field] += $delta;
}
}
}
}
}
}
// -------------------------------------------------------------------------
/**
* Interal tree operation. Shift left-right pointers to make room for
* one or mode nodes, or to re-order the pointers after a delete
* operation, in the given range
*
* @param integer left pointer of the first node to shift
* @param integer right pointer of the last node to shift
* @param integer number of positions to shift (if negative the shift will be to the left)
*/
protected function _shift_rl_range($first, $last, $delta)
{
// get params to avoid excessive method calls
$tree_field = static::tree_config('tree_field');
$left_field = static::tree_config('left_field');
$right_field = static::tree_config('right_field');
$query = \DB::update(static::table());
// if we have multiple roots
if ( ! is_null($tree_field))
{
$query->where($tree_field, $this->get_tree_id());
}
// select the range
$query->where($left_field, '>=', $first);
$query->where($right_field, '<=', $last);
// correct the delta
$sqldelta = ($delta < 0) ? (' - '.abs($delta)) : (' + '.$delta);
// set clause
$query->set(array(
$left_field => \DB::expr(\DB::quote_identifier($left_field).$sqldelta),
$right_field => \DB::expr(\DB::quote_identifier($right_field).$sqldelta),
));
// update in the correct order to avoid constraint conflicts
$query->order_by($right_field, ($delta < 0 ? 'ASC' : 'DESC'));
// execute it
$query->execute(static::connection(true));
// update cached objects, we've modified pointers
$class = get_called_class();
if (array_key_exists($class, static::$_cached_objects))
{
foreach (static::$_cached_objects[$class] as $object)
{
if (is_null($tree_field) or $object->{$tree_field} == $this->{$tree_field})
{
if ($object->{$left_field} >= $first and $object->{$right_field} <= $last)
{
if ($delta < 0)
{
$object->_data[$left_field] -= abs($delta);
$object->_data[$right_field] -= abs($delta);
$object->_original[$left_field] -= abs($delta);
$object->_original[$right_field] -= abs($delta);
}
else
{
$object->_data[$left_field] += $delta;
$object->_data[$right_field] += $delta;
$object->_original[$left_field] += $delta;
$object->_original[$right_field] += $delta;
}
}
}
}
}
}
// -------------------------------------------------------------------------
/**
* Interal tree operation. Move the current node and all children
* to a new position in the tree
*
* @param integer new left pointer location to move to
*/
protected function _move_subtree($destination_id)
{
// get params to avoid excessive method calls
$tree_field = static::tree_config('tree_field');
$left_field = static::tree_config('left_field');
$right_field = static::tree_config('right_field');
// catch a move into the subtree
if ( $destination_id >= $this->{$left_field} and $destination_id <= $this->{$right_field} )
{
// it would make no change to the tree
return $this;
}
// determine the size of the tree to move
$treesize = $this->{$right_field} - $this->{$left_field} + 1;
// get the objects left- and right pointers
$left_id = $this->{$left_field};
$right_id = $this->{$right_field};
// shift to make some space
$this->_shift_rl_values($destination_id, $treesize);
// correct pointers if there were shifted to
if ($this->{$left_field} >= $destination_id)
{
$left_id += $treesize;
$right_id += $treesize;
}
// enough room now, start the move
$this->_shift_rl_range($left_id, $right_id, $destination_id - $left_id);
// and correct index values after the source
$this->_shift_rl_values(++$right_id, (-1 * $treesize));
// return the moved object
return $this;
}
}