<?php defined('SYSPATH') OR
die('No direct access allowed.');
/**
*
* Its a combination of a traditional MPTT tree and additional usefull data (parent id, level value)
*
* @package CMS
* @author Brotkin Ivan (BIakaVeron) <BIakaVeron@gmail.com>
* @copyright Copyright (c) 2009 Brotkin Ivan
*
* @property Database $db
*/
abstract class ORM_MPTT_Core extends ORM
{
protected $left_column = 'lft';
protected $right_column = 'rgt';
protected $level_column = 'lvl';
protected $scope_column = 'scope';
protected $parent_column = 'parent_id';
protected $sorting;
protected $full_table_name; // should use only in plain SQL queries
public function __construct($id = NULL) {
if (!isset($this->sorting)) {
$this->sorting = array($this->left_column => 'ASC');
}
parent::__construct($id);
$this->full_table_name = $this->table_prefix().$this->table_name;
}
/*
* Insert operations
*
*/
public function save() {
// overload basic ORM::save() method
if (!$this->loaded) {
return $this->make_root();
}
else {
return parent::save();
}
}
public function make_root($scope = NULL) {
// save node as root
if ($this->loaded) throw new Kohana_User_Exception('Error inserting node', 'Cannot insert the same node twice');
$scope = self::get_next_scope();
}
elseif (self::scope_available($scope) === FALSE) {
$scope = self::get_next_scope();
}
$this->{$this->scope_column} = $scope;
$this->{$this->level_column} = 1;
$this->{$this->left_column} = 1;
$this->{$this->right_column} = 2;
$this->{$this->parent_column} = NULL;
return parent::save();
}
public function make_child($id, $first = FALSE) {
// inserts node as direct child for $id node
$this->lock();
$id = self::factory($this->object_name, $id);
}
if ($first === TRUE) {
$lft = $id->{$this->left_column}+1;
}
else {
$lft = $id->{$this->right_column};
}
$this->{$this->scope_column} = $id->scope();
$this->add_space($lft, 2);
$this->{$this->parent_column} = $id->primary_key_value;
$this->{$this->level_column} = $id->level() + 1;
$this->{$this->left_column} = $lft;
$this->{$this->right_column} = $lft+1;
parent::save();
$this->unlock();
return $this;
}
public function insert_near($id, $before = FALSE) {
// inserts node as next/prev sibling
if ($this->loaded) throw new Kohana_User_Exception('Error inserting node', 'Cannot insert the same node twice');
if ($this->size() > 2) throw new Kohana_User_Exception('Error inserting node', 'Cannot use a node with children');
$id = self::factory($this->object_name, $id);
}
if ($before) {
$lft = $id->left();
}
else {
$lft = $id->right() + 1;
}
$this->{$this->scope_column} = $id->scope();
$this->lock();
$this->add_space($lft);
$this->{$this->left_column} = $lft;
$this->{$this->right_column} = $lft+1;
$this->{$this->parent_column} = $id->parent();
$this->{$this->level_column} = $id->level();
parent::save();
$this->unlock();
}
public function delete() {
// deletes current node with descendants
$this->lock();
$this->db
->where($this->left_column." >=".$this->left())
->where($this->left_column." <= ".$this->right())
->delete($this->table_name);
$this->clear_space($this->left(), $this->size());
$this->unlock();
}
public function move_to($id, $first = FALSE) {
// moves current node with descendants to a node $id
$id = self::factory($this->object_name, $id);
}
if ($this->is_in_descendants($id)) {
throw new Kohana_User_Exception('Error replacing node', 'Cannot move nodes to themself');
}
$ids = $this->subtree(TRUE)->primary_key_array();
$lft = ($first==TRUE ? $id->left() + 1 : $id->right());
$oldlft = $this->left();
$level = $id->level() + 1;
$delta = $lft - $this->left();
if ($delta < 0) $delta = "(".$delta.")";
$deltalevel = $level - $this->level();
if ($deltalevel < 0) $deltalevel = "(".$deltalevel.")";
$this->lock();
// temporary setting scope to 0
$this->db
->in($this->primary_key, $ids)
->set($this->scope_column, 0)
->update($this->table_name);
$this->clear_space($oldlft, $this->size());
$this->{$this->scope_column} = $id->scope();
$this->add_space($lft, $this->size());
$this->db
->in($this->primary_key, $ids)
->set($this->left_column, new Database_Expression($this->left_column. " + ".$delta))
->set($this->right_column, new Database_Expression($this->right_column. " + ".$delta))
->set($this->level_column, new Database_Expression($this->level_column. " + ".$deltalevel))
->set($this->scope_column, $id->scope())
->update($this->table_name);
$this->{$this->parent_column} = $id->primary_key_value;
parent::save();
$this->unlock();
}
public function move_children_to($id, $first = FALSE) {
// moves all descendants to $id node WITHOUT current node
if (!$this->has_children()) return FALSE;
$id = self::factory($this->object_name, $id);
}
$ids = $this->subtree(FALSE)->primary_key_array();
$lft = ($first==TRUE ? $id->left() + 1 : $id->right());
$oldlft = $this->left() + 1;
$level = $id->level() + 1;
$delta = $lft - $oldlft;
if ($delta < 0) $delta = "(".$delta.")";
$deltalevel = $level - $this->level() - 1;
if ($deltalevel < 0) $deltalevel = "(".$deltalevel.")";
$this->lock();
$this->db
->in($this->primary_key, $ids)
->set($this->scope_column, 0)
->update($this->table_name);
$this->clear_space($oldlft, $this->size() - 2);
// this is need for correct add_space() work
$this->{$this->scope_column} = $id->scope();
$this->add_space($lft, $this->size() - 2);
$this->db
->in($this->primary_key, $ids)
->set($this->left_column, new Database_Expression($this->left_column. " + ".$delta))
->set($this->right_column, new Database_Expression($this->right_column. " + ".$delta))
->set($this->level_column, new Database_Expression($this->level_column. " + ".$deltalevel))
->set($this->scope_column, $id->scope())
->update($this->table_name);
$this->db
->set($this->parent_column, $id->primary_key_value)
->where($this->level_column, $id->level() + 1)
->in($this->primary_key, $ids)
->update($this->table_name);
$this->unlock();
$this->reload();
}
/*
* Retrieving info methods
*
*/
public function get_root($scope = NULL) {
// returns all roots
return $this->db
->where($this->level_column, 1)
->get($this->table_name);
}
else {
// only current root
return $this->db
->where($this->scope_column, $scope)
->get($this->table_name);
}
}
public function get_parents($with_self = FALSE) {
$suffix = $with_self ? "= " : " ";
// returns all current node parents
return self::factory($this->object_name)
->where($this->left_column." <".$suffix.$this->left())
->where($this->right_column." >".$suffix.$this->right())
->where($this->scope_column, $this->scope())
->find_all();
}
public function get_parent() {
if ($this->is_root()) return NULL;
return self::factory($this->object_name, $this->parent());
}
public function get_children() {
// returns only direct children
return self::factory($this->object_name)
->where($this->left_column." >".$this->left())
->where($this->right_column." <".$this->right())
->where($this->scope_column, $this->scope())
->where($this->level_column, $this->level() + 1)
->find_all();
}
public function get_subtree($with_parent = FALSE) {
// return all descendants of current node
$suffix = ($with_parent ? "= " : " ");
return self::factory($this->object_name)
->where($this->left_column." >".$suffix.$this->left())
->where($this->right_column." <".$suffix.$this->right())
->where($this->scope_column, $this->scope())
->find_all();
}
public function get_fulltree($use_scope = TRUE) {
// returns full tree (with or without scope checking)
$result = self::factory($this->object_name);
if ($use_scope) $result->where($this->scope_column, $this->{$this->scope_column});
if ($use_scope == FALSE) $result->orderby($this->scope_column, 'ASC')->orderby($this->left_column, 'ASC');
return $result->find_all();
}
public function get_leaves() {
// returns only leaves of current node
return self::factory($this->object_name)
->where($this->left_column." >".$suffix.$this->left())
->where($this->right_column." <".$suffix.$this->right())
->where($this->left_column, new Database_Expression($this->right_column." - 1"))
->where($this->scope_column, $this->{$this->scope_column})
->find_all();
}
/*
* Simple methods for getting/setting primary info
*
*/
public function set_title($title) {
$this->title = $title;
return $this;
}
public function left() {
return $this->{$this->left_column};
}
public function right() {
return $this->{$this->right_column};
}
public function level() {
return $this->{$this->level_column};
}
public function scope() {
return $this->{$this->scope_column};
}
public function parent() {
return $this->{$this->parent_column};
}
public function size() {
return $this->{$this->right_column} - $this->left() + 1;
}
public function count() {
return $this->size() - 2;
}
public function has_children() {
return ($this->size() > 2);
}
public function is_parent($id) {
// is current node a direct parent of $id node
$id = self::factory($this->object_name, $id);
}
return $id->{$this->parent_column} == $this->primary_key_value;
}
public function is_child($id) {
// is current node a direct child of $id node
$id = self::factory($this->object_name, $id);
}
return $this->{$this->parent_column} == $id->primary_key_value;
}
public function is_in_descendants($id) {
// is current node one of a $id node child
$id = self::factory($this->object_name, $id);
}
if ($this->scope() != $id->scope()) return FALSE;
if ($this->left() <= $id->left()) return FALSE;
if ($this->right() >= $id->right()) return FALSE;
return TRUE;
}
public function is_in_parents($id) {
// is current node one of a $id node parents
$id = self::factory($this->object_name, $id);
}
return $id->is_in_descendants($this);
}
public function is_heighbor($id) {
// is current node neighbor of $id node (the same direct parent)
$id = self::factory($this->object_name, $id);
}
return ($this->parent() == $id->parent());
}
public function is_root() {
// is current node a root node
return $this->level() == 1;
}
/*
* Support methods
*
*/
protected function add_space($start, $size = 2) {
// add space for adding/inserting nodes
// $this->scope should be set before adding space!
$this->db
->set($this->left_column, new Database_Expression($this->left_column.' + '.$size))
->where($this->left_column." >= ".$start)
->where($this->scope_column, $this->scope())
->update($this->table_name);
$this->db
->set($this->right_column, new Database_Expression($this->right_column.' + '.$size))
->where($this->right_column." >= ".$start)
->where($this->scope_column, $this->scope())
->update($this->table_name);
}
protected function clear_space($start, $size = 2) {
// remove space after deleting/moving node
$this->db
->set($this->left_column, new Database_Expression($this->left_column.' - '.$size))
->where($this->left_column." >= ".$start)
->where($this->scope_column, $this->scope())
->update($this->table_name);
$this->db
->set($this->right_column, new Database_Expression($this->right_column.' - '.$size))
->where($this->right_column." >= ".$start)
->where($this->scope_column, $this->scope())
->update($this->table_name);
}
protected function lock() {
// lock table
$this->db->query('LOCK TABLE '.$this->full_table_name.' WRITE');
}
protected function unlock() {
// unlock tables
$this->db->query('UNLOCK TABLES');
}
protected function scope_available($scope) {
// checking for supplied scope available
return ! self::factory($this->object_name)
->where($this->scope_column, $scope)
->count_all();
}
protected function get_next_scope() {
// returns available value for scope
$scope = $this->db->select(new Database_Expression('IFNULL(MAX(`'.$this->scope_column.'`), 0) as scope'))->get($this->table_name)->current();
if ($scope AND
intval($scope->scope)>0
) return intval($scope->scope)+1;
return 1;
}
public function __get($column) {
if ($column === 'parent')
return $this->get_parent();
elseif ($column === 'parents')
return $this->get_parents();
elseif ($column === 'children')
return $this->get_children();
elseif ($column === 'leaves')
return $this->get_leaves();
elseif ($column === 'subtree')
return $this->get_subtree();
elseif ($column === 'fulltree')
return $this->get_fulltree();
else return parent::__get($column);
}
}