Skip to content

Instantly share code, notes, and snippets.

@biakaveron
Created February 8, 2011 14:52
Show Gist options
  • Save biakaveron/816534 to your computer and use it in GitHub Desktop.
Save biakaveron/816534 to your computer and use it in GitHub Desktop.
very draft Jelly MPTT class
<?php defined('SYSPATH') OR die('No direct access allowed.');
abstract class Jelly_Model_MPTT extends Jelly_Model {
protected static $_columns = array(
'left' => 'lft',
'right' => 'rgt',
'scope' => 'scope',
'level' => 'lvl',
'parent_id' => 'parent_id',
);
protected $_parent = NULL;
public static function initialize(Jelly_Meta $meta)
{
$columns = Kohana::config('mptt')->as_array();
self::$_columns = arr::overwrite(self::$_columns, $columns);
$meta->fields(array(
'id' => new Field_Primary,
'left' => new Jelly_Field_MPTT_Left(array(
'column' => self::$_columns['left'],
)),
'right' => new Jelly_Field_MPTT_Right(array(
'column' => self::$_columns['right'],
)),
'level' => new Jelly_Field_MPTT_Level(array(
'column' => self::$_columns['level'],
)),
'scope' => new Jelly_Field_MPTT_Scope(array(
'column' => self::$_columns['scope'],
)),
'parent_id' => new Field_Integer(array(
'column' => self::$_columns['parent_id'],
)),
));
}
/**
* basic operations
*/
public function save($key = NULL)
{
if ($parent = $this->parent())
{
return $this->insert($parent);
}
return $this->insert_as_root();
}
public function insert($target = NULL, $go_left = FALSE, $go_down = TRUE)
{
if ($this->loaded())
{
throw new Kohana_Exception('Cannot insert duplicate node!');
}
if ($target === TRUE)
{
// insert as root node
$this->scope($this->_generate_scope())
->level(1)
->left(1)
->right(2)
->parent(NULL);
return parent::save();
}
if(is_int($target))
{
// load parent model
$target = Jelly::select($this, $target);
}
if ( $target === NULL OR ! $target->loaded())
{
throw new Kohana_Exception('Parent node not found!');
}
if ($target instanceof $this)
{
$this->scope($target->scope());
if ($go_down === FALSE)
{
$left = $go_left ?
$target->left() // insert before
:
$target->right() + 1; // insert after
$right = $left + $this->size() - 1;
// insert as sibling
$this->_add_space($left, $this->size())
->parent($target->parent())
->left($left)
->right($right)
->level($target->level());
}
else
{
// insert as child
$left = $go_left ?
$target->left() + 1 // as first child
:
$target->right; // as last child
$right = $left + 1;
$this->_add_space($left, $this->size())
->parent($target)
->left($left)
->right($right)
->level($target->level() + 1);
}
return parent::save();
}
}
public function insert_as_root()
{
return $this->insert(TRUE);
}
public function insert_after($target)
{
return $this->insert($target, FALSE, FALSE);
}
public function insert_before($target)
{
return $this->insert($target, TRUE, FALSE);
}
public function insert_as_first_child($target)
{
return $this->insert($target, TRUE, TRUE);
}
public function insert_as_last_child($target)
{
return $this->insert($target, FALSE, TRUE);
}
public function add_node($node)
{
$node->insert_as_last_child($this);
return $this;
}
public function add_nodes($nodes)
{
// @TODO optimize query count?
foreach($nodes as $node)
{
$this->add_node($node);
}
return $this;
}
/**
* common actions
*/
protected function _lock() {
// lock table
DB::query('lock', 'LOCK TABLE '.$this->meta()->table().' WRITE')->execute($this->meta()->db());
}
protected function _unlock() {
// unlock tables
DB::query('unlock','UNLOCK TABLES')->execute($this->meta()->db());
}
protected function _generate_scope()
{
$last_scope = DB::select();
$scope = DB::select(DB::expr('IFNULL(MAX(`'.self::$_columns['scope'].'`), 0) as scope'))
->from($this->meta()->table())
->execute($this->meta()->db())
->current();
if ($scope AND intval($scope['scope'])>0)
{
return intval($scope['scope'])+1;
}
return 1;
}
protected function _add_space($start, $size = 2)
{
$sign = $size > 0 ? ' + ' : ' - ';
$size = abs($size);
$this->_lock();
DB::update($this->meta()->table())
->set(array(self::$_columns['left'] => DB::expr(self::$_columns['left'].$sign.$size)))
->where(self::$_columns['left']," >= ",$start)
->where(self::$_columns['scope'], "=", $this->scope())
->execute($this->meta()->db());
DB::update($this->meta()->table())
->set(array(self::$_columns['right'] => DB::expr(self::$_columns['right'].$sign.$size)))
->where(self::$_columns['right']," >= ",$start)
->where(self::$_columns['scope'], "=", $this->scope())
->execute($this->meta()->db());
$this->_unlock();
return $this;
}
protected function _clear_space($start, $size = 2)
{
return $this->_add_space($start, $size*-1);
}
/**
* accessing to model properties
*/
public function get_root()
{
if ($this->level() == 1)
{
return $this;
}
return Jelly::select($this)
->where(self::$_columns['scope'], '=', $this->scope())
->where(self::$_columns['level'], '=', 1)
->limit(1)
->execute($this->meta()->db());
}
/**
* Setter/getter for `left` column
*
* @param int $value Use it to set `left` value
* @chainable
* @return int|Jelly_Model_MPTT
*/
public function left($value = NULL)
{
if (func_num_args() == 0)
{
return $this->left;
}
$this->left = $value;
return $this;
}
/**
* Setter/getter for `right` column
*
* @param int $value Use it to set `right` value
* @chainable
* @return int|Jelly_Model_MPTT
*/
public function right($value = NULL)
{
if (func_num_args() == 0)
{
return $this->right;
}
$this->right = $value;
return $this;
}
/**
* Setter/getter for `scope` column
*
* @param int $value Use it to set `scope` value
* @chainable
* @return int|Jelly_Model_MPTT
*/
public function scope($value = NULL)
{
if (func_num_args() == 0)
{
return $this->scope;
}
$this->scope = $value;
return $this;
}
/**
* Setter/getter for `level` column
*
* @param int $value Use it to set `level` value
* @chainable
* @return int|Jelly_Model_MPTT
*/
public function level($value = NULL)
{
if (func_num_args() == 0)
{
return $this->level;
}
$this->level = $value;
return $this;
}
public function parent($parent = NULL)
{
if (func_num_args() == 0)
{
// find parent node
if (FALSE === $this->_parent)
{
if (is_null($this->parent_id))
{
$this->_parent = NULL;
}
else
{
$this->_parent = Jelly::select($this, $this->parent_id);
}
}
return $this->_parent;
}
if ($parent instanceof Jelly_Model)
{
$this->_parent = $parent;
$parent = $parent->id();
}
else
{
// clear previous parent object
$this->_parent = FALSE;
}
$this->parent_id = $parent;
return $this;
}
/**
* Returns node size
* @return int
*/
public function size()
{
if ( $this->saved())
{
return $this->right() - $this->left() + 1;
}
// @TODO add child counting
return 2;
}
/**
* Returns child count
* @return int
*/
public function count()
{
return $this->size() / 2 - 1;
}
public function is_root()
{
return $this->level() == 1;
}
public function has_children()
{
return ( $this->right() - $this->left() ) > 1;
}
public function get_children()
{
if ( ! $this->has_children())
{
return array();
}
return Jelly::select($this)
->where(self::$_columns['scope'], '=', $this->scope())
->where(self::$_columns['left'], '>', $this->left())
->where(self::$_columns['right'], '<', $this->right())
->order_by(self::$_columns['left'])
->execute($this->meta()->db());
}
public function get_leaves()
{
return Jelly::select($this)
->where(self::$_columns['scope'], '=', $this->scope())
->where(self::$_columns['left'], '>', $this->left())
->where(self::$_columns['right'], '<', $this->right())
->where(self::$_columns['level'], '=', $this->level() + 1)
->order_by(self::$_columns['left'])
->execute($this->meta()->db());
}
public function render($root = FALSE)
{
if ($root == TRUE)
{
$node = $this->is_root() ? $this : $this->get_root();
}
else
{
$node = $this;
}
if ($this->is_root())
{
echo '<ul><b>ROOT</b>';
echo " [".$this->left()."][".$this->right()."]";
}
else
{
echo '<li>node#'.$this->id();
echo " [".$this->left()."][".$this->right()."]";
}
if ($this->has_children())
{
echo '<ul>';
foreach($this->get_leaves() as $child)
{
$child->render();
}
echo '</ul>';
}
if ($this->is_root())
{
echo '</ul>';
}
else
{
echo '</li>';
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment