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.
1858 lines
50 KiB
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;
|
|
}
|
|
}
|