Created
February 8, 2011 14:52
-
-
Save biakaveron/816534 to your computer and use it in GitHub Desktop.
very draft Jelly MPTT class
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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