Guest

biakaveron

By: a guest on Jun 28th, 2009  |  syntax: PHP  |  size: 13.31 KB  |  hits: 601  |  expires: Never
download  |  raw  |  embed  |  report abuse
Copied
  1. <?php defined('SYSPATH') OR die('No direct access allowed.');
  2. /**
  3.  *
  4.  * Its a combination of a traditional MPTT tree and additional usefull data (parent id, level value)
  5.  *
  6.  * @package    CMS
  7.  * @author     Brotkin Ivan (BIakaVeron) <BIakaVeron@gmail.com>
  8.  * @copyright  Copyright (c) 2009 Brotkin Ivan
  9.  *
  10.  * @property Database $db
  11.  */
  12.  
  13. abstract class ORM_MPTT_Core extends ORM
  14. {
  15.         protected $left_column = 'lft';
  16.         protected $right_column = 'rgt';
  17.         protected $level_column = 'lvl';
  18.         protected $scope_column = 'scope';
  19.         protected $parent_column = 'parent_id';
  20.         protected $sorting;
  21.         protected $full_table_name; // should use only in plain SQL queries
  22.  
  23.         public function __construct($id = NULL) {
  24.                 if (!isset($this->sorting)) {
  25.                         $this->sorting = array($this->left_column => 'ASC');
  26.                 }
  27.                 parent::__construct($id);
  28.                 $this->full_table_name = $this->table_prefix().$this->table_name;
  29.         }
  30.  
  31. /*
  32.  * Insert operations
  33.  *
  34.  */
  35.  
  36.         public function save() {
  37.                 // overload basic ORM::save() method
  38.                 if (!$this->loaded) {
  39.                         return $this->make_root();
  40.                 }
  41.                 else {
  42.                         return parent::save();
  43.                 }
  44.         }
  45.  
  46.         public function make_root($scope = NULL) {
  47.                 // save node as root
  48.                 if ($this->loaded) throw new Kohana_User_Exception('Error inserting node', 'Cannot insert the same node twice');
  49.                 if (is_null($scope)) {
  50.                         $scope = self::get_next_scope();
  51.                 }
  52.                 elseif (self::scope_available($scope) === FALSE) {
  53.                         $scope = self::get_next_scope();
  54.                 }
  55.                 $this->{$this->scope_column} = $scope;
  56.                 $this->{$this->level_column} = 1;
  57.                 $this->{$this->left_column} = 1;
  58.                 $this->{$this->right_column} = 2;
  59.                 $this->{$this->parent_column} = NULL;
  60.                 return parent::save();
  61.         }
  62.  
  63.         public function make_child($id, $first = FALSE) {
  64.                 // inserts node as direct child for $id node
  65.                 $this->lock();
  66.                 if (!is_a($id, get_class($this))) {
  67.                         $id = self::factory($this->object_name, $id);
  68.                 }
  69.                 if ($first === TRUE) {
  70.                         $lft = $id->{$this->left_column}+1;
  71.                 }
  72.                 else {
  73.                         $lft = $id->{$this->right_column};
  74.                 }
  75.                 $this->{$this->scope_column} = $id->scope();
  76.                 $this->add_space($lft, 2);
  77.                 $this->{$this->parent_column} = $id->primary_key_value;
  78.                 $this->{$this->level_column} = $id->level() + 1;
  79.                 $this->{$this->left_column} = $lft;
  80.                 $this->{$this->right_column} = $lft+1;
  81.                 parent::save();
  82.                 $this->unlock();
  83.                 return $this;
  84.         }
  85.  
  86.         public function insert_near($id, $before = FALSE) {
  87.                 // inserts node as next/prev sibling
  88.                 if ($this->loaded) throw new Kohana_User_Exception('Error inserting node', 'Cannot insert the same node twice');
  89.                 if ($this->size() > 2) throw new Kohana_User_Exception('Error inserting node', 'Cannot use a node with children');
  90.                 if (!is_a($id, get_class($this))) {
  91.                         $id = self::factory($this->object_name, $id);
  92.                 }
  93.                 if ($before) {
  94.                         $lft = $id->left();
  95.                 }
  96.                 else {
  97.                         $lft = $id->right() + 1;
  98.                 }
  99.                 $this->{$this->scope_column} = $id->scope();
  100.                 $this->lock();
  101.                 $this->add_space($lft);
  102.                 $this->{$this->left_column} = $lft;
  103.                 $this->{$this->right_column} = $lft+1;
  104.                 $this->{$this->parent_column} = $id->parent();
  105.                 $this->{$this->level_column} = $id->level();
  106.                 parent::save();
  107.                 $this->unlock();
  108.         }
  109.  
  110.         public function delete() {
  111.                 // deletes current node with descendants
  112.                 $this->lock();
  113.                 $this->db
  114.                         ->where($this->left_column." >=".$this->left())
  115.                         ->where($this->left_column." <= ".$this->right())
  116.                                 ->delete($this->table_name);
  117.                 $this->clear_space($this->left(), $this->size());
  118.                 $this->unlock();
  119.         }
  120.  
  121.         public function move_to($id, $first = FALSE) {
  122.                 // moves current node with descendants to a node $id
  123.                 if (!is_a($id, get_class($this))) {
  124.                         $id = self::factory($this->object_name, $id);
  125.                 }
  126.                 if ($this->is_in_descendants($id)) {
  127.                         throw new Kohana_User_Exception('Error replacing node', 'Cannot move nodes to themself');
  128.                 }
  129.                 $ids = $this->subtree(TRUE)->primary_key_array();
  130.                 $lft = ($first==TRUE ? $id->left() + 1 : $id->right());
  131.                 $oldlft = $this->left();
  132.                 $level = $id->level() + 1;
  133.                 $delta = $lft - $this->left();
  134.                 if ($delta < 0) $delta = "(".$delta.")";
  135.                 $deltalevel = $level - $this->level();
  136.                 if ($deltalevel < 0) $deltalevel = "(".$deltalevel.")";
  137.                 $this->lock();
  138.                 // temporary setting scope to 0
  139.                 $this->db
  140.                         ->in($this->primary_key, $ids)
  141.                         ->set($this->scope_column, 0)
  142.                         ->update($this->table_name);
  143.                 $this->clear_space($oldlft, $this->size());
  144.                 $this->{$this->scope_column} = $id->scope();
  145.                 $this->add_space($lft, $this->size());
  146.  
  147.                 $this->db
  148.                         ->in($this->primary_key, $ids)
  149.                         ->set($this->left_column, new Database_Expression($this->left_column. " + ".$delta))
  150.                         ->set($this->right_column, new Database_Expression($this->right_column. " + ".$delta))
  151.                         ->set($this->level_column, new Database_Expression($this->level_column. " + ".$deltalevel))
  152.                         ->set($this->scope_column, $id->scope())
  153.                         ->update($this->table_name);
  154.                 $this->{$this->parent_column} = $id->primary_key_value;
  155.                 parent::save();
  156.                 $this->unlock();
  157.         }
  158.  
  159.         public function move_children_to($id, $first = FALSE) {
  160.                 // moves all descendants to $id node WITHOUT current node
  161.                 if (!$this->has_children()) return FALSE;
  162.                 if (!is_a($id, get_class($this))) {
  163.                         $id = self::factory($this->object_name, $id);
  164.                 }
  165.                 $ids = $this->subtree(FALSE)->primary_key_array();
  166.                 $lft = ($first==TRUE ? $id->left() + 1 : $id->right());
  167.                 $oldlft = $this->left() + 1;
  168.                 $level = $id->level() + 1;
  169.                 $delta = $lft - $oldlft;
  170.                 if ($delta < 0) $delta = "(".$delta.")";
  171.                 $deltalevel = $level - $this->level() - 1;
  172.                 if ($deltalevel < 0) $deltalevel = "(".$deltalevel.")";
  173.                 $this->lock();
  174.                 $this->db
  175.                         ->in($this->primary_key, $ids)
  176.                         ->set($this->scope_column, 0)
  177.                         ->update($this->table_name);
  178.                 $this->clear_space($oldlft, $this->size() - 2);
  179.                 // this is need for correct add_space() work
  180.                 $this->{$this->scope_column} = $id->scope();
  181.                 $this->add_space($lft, $this->size() - 2);
  182.  
  183.                 $this->db
  184.                         ->in($this->primary_key, $ids)
  185.                         ->set($this->left_column, new Database_Expression($this->left_column. " + ".$delta))
  186.                         ->set($this->right_column, new Database_Expression($this->right_column. " + ".$delta))
  187.                         ->set($this->level_column, new Database_Expression($this->level_column. " + ".$deltalevel))
  188.                         ->set($this->scope_column, $id->scope())
  189.                         ->update($this->table_name);
  190.                 $this->db
  191.                         ->set($this->parent_column, $id->primary_key_value)
  192.                         ->where($this->level_column, $id->level() + 1)
  193.                         ->in($this->primary_key, $ids)
  194.                         ->update($this->table_name);
  195.                 $this->unlock();
  196.                 $this->reload();
  197.         }
  198.  
  199. /*
  200.  * Retrieving info methods
  201.  *
  202.  */
  203.  
  204.         public function get_root($scope = NULL) {
  205.                 if (is_null($scope)) {
  206.                         // returns all roots
  207.                         return $this->db
  208.                                 ->where($this->level_column, 1)
  209.                                 ->get($this->table_name);
  210.                 }
  211.                 else {
  212.                         // only current root
  213.                         return $this->db
  214.                                 ->where($this->scope_column, $scope)
  215.                                 ->get($this->table_name);
  216.                 }
  217.         }
  218.  
  219.         public function get_parents($with_self = FALSE) {
  220.                 $suffix = $with_self ? "= " : " ";
  221.                 // returns all current node parents
  222.                 return self::factory($this->object_name)
  223.                         ->where($this->left_column." <".$suffix.$this->left())
  224.                         ->where($this->right_column." >".$suffix.$this->right())
  225.                         ->where($this->scope_column, $this->scope())
  226.                         ->find_all();
  227.         }
  228.  
  229.         public function get_parent() {
  230.                 if ($this->is_root()) return NULL;
  231.                 return self::factory($this->object_name, $this->parent());
  232.         }
  233.  
  234.         public function get_children() {
  235.                 // returns only direct children
  236.                 return self::factory($this->object_name)
  237.                         ->where($this->left_column." >".$this->left())
  238.                         ->where($this->right_column." <".$this->right())
  239.                         ->where($this->scope_column, $this->scope())
  240.                         ->where($this->level_column, $this->level() + 1)
  241.                         ->find_all();
  242.         }
  243.  
  244.         public function get_subtree($with_parent = FALSE) {
  245.                 // return all descendants of current node
  246.                 $suffix = ($with_parent ? "= " : " ");
  247.                 return self::factory($this->object_name)
  248.                         ->where($this->left_column." >".$suffix.$this->left())
  249.                         ->where($this->right_column." <".$suffix.$this->right())
  250.                         ->where($this->scope_column, $this->scope())
  251.                         ->find_all();
  252.         }
  253.        
  254.         public function get_fulltree($use_scope = TRUE) {
  255.                 // returns full tree (with or without scope checking)
  256.                 $result = self::factory($this->object_name);
  257.                 if ($use_scope) $result->where($this->scope_column, $this->{$this->scope_column});
  258.                 if ($use_scope == FALSE) $result->orderby($this->scope_column, 'ASC')->orderby($this->left_column, 'ASC');
  259.                 return $result->find_all();
  260.         }
  261.  
  262.         public function get_leaves() {
  263.                 // returns only leaves of current node
  264.                 return self::factory($this->object_name)
  265.                         ->where($this->left_column." >".$suffix.$this->left())
  266.                         ->where($this->right_column." <".$suffix.$this->right())
  267.                         ->where($this->left_column, new Database_Expression($this->right_column." - 1"))
  268.                         ->where($this->scope_column, $this->{$this->scope_column})
  269.                         ->find_all();
  270.         }
  271.  
  272. /*
  273.  * Simple methods for getting/setting primary info
  274.  *
  275.  */
  276.  
  277.         public function set_title($title) {
  278.                 $this->title = $title;
  279.                 return $this;
  280.         }
  281.  
  282.         public function left() {
  283.                 return $this->{$this->left_column};
  284.         }
  285.  
  286.         public function right() {
  287.                 return $this->{$this->right_column};
  288.         }
  289.  
  290.         public function level() {
  291.                 return $this->{$this->level_column};
  292.         }
  293.  
  294.         public function scope() {
  295.                 return $this->{$this->scope_column};
  296.         }
  297.  
  298.         public function parent() {
  299.                 return $this->{$this->parent_column};
  300.         }
  301.  
  302.         public function size() {
  303.                 return $this->{$this->right_column} - $this->left() + 1;
  304.         }
  305.  
  306.         public function count() {
  307.                 return $this->size() - 2;
  308.         }
  309.  
  310.         public function has_children() {
  311.                 return ($this->size() > 2);
  312.         }
  313.  
  314.         public function is_parent($id) {
  315.                 // is current node a direct parent of $id node
  316.                 if (!is_a($id, get_class($this))) {
  317.                         $id = self::factory($this->object_name, $id);
  318.                 }
  319.                 return $id->{$this->parent_column} == $this->primary_key_value;
  320.         }
  321.  
  322.         public function is_child($id) {
  323.                 // is current node a direct child of $id node
  324.                 if (!is_a($id, get_class($this))) {
  325.                         $id = self::factory($this->object_name, $id);
  326.                 }
  327.                 return $this->{$this->parent_column} == $id->primary_key_value;
  328.         }
  329.  
  330.         public function is_in_descendants($id) {
  331.                 // is current node one of a $id node child
  332.                 if (!is_a($id, get_class($this))) {
  333.                         $id = self::factory($this->object_name, $id);
  334.                 }
  335.                 if ($this->scope() != $id->scope()) return FALSE;
  336.                 if ($this->left() <= $id->left()) return FALSE;
  337.                 if ($this->right() >= $id->right()) return FALSE;
  338.                 return TRUE;
  339.         }
  340.  
  341.         public function is_in_parents($id) {
  342.                 // is current node one of a $id node parents
  343.                 if (!is_a($id, get_class($this))) {
  344.                         $id = self::factory($this->object_name, $id);
  345.                 }
  346.                 return $id->is_in_descendants($this);
  347.         }
  348.  
  349.         public function is_heighbor($id) {
  350.                 // is current node neighbor of $id node (the same direct parent)
  351.                 if (!is_a($id, get_class($this))) {
  352.                         $id = self::factory($this->object_name, $id);
  353.                 }
  354.                 return ($this->parent() == $id->parent());
  355.         }
  356.  
  357.         public function is_root() {
  358.                 // is current node a root node
  359.                 return $this->level() == 1;
  360.         }
  361.  
  362. /*
  363.  * Support methods
  364.  *
  365.  */
  366.  
  367.         protected function add_space($start, $size = 2) {
  368.                 // add space for adding/inserting nodes
  369.                 // $this->scope should be set before adding space!
  370.                 $this->db
  371.                         ->set($this->left_column, new Database_Expression($this->left_column.' + '.$size))
  372.                         ->where($this->left_column." >= ".$start)
  373.                         ->where($this->scope_column, $this->scope())
  374.                         ->update($this->table_name);
  375.                 $this->db
  376.                         ->set($this->right_column, new Database_Expression($this->right_column.' + '.$size))
  377.                         ->where($this->right_column." >= ".$start)
  378.                         ->where($this->scope_column, $this->scope())
  379.                         ->update($this->table_name);
  380.         }
  381.  
  382.         protected function clear_space($start, $size = 2) {
  383.                 // remove space after deleting/moving node
  384.                 $this->db
  385.                         ->set($this->left_column, new Database_Expression($this->left_column.' - '.$size))
  386.                         ->where($this->left_column." >= ".$start)
  387.                         ->where($this->scope_column, $this->scope())
  388.                         ->update($this->table_name);
  389.                 $this->db
  390.                         ->set($this->right_column, new Database_Expression($this->right_column.' - '.$size))
  391.                         ->where($this->right_column." >= ".$start)
  392.                         ->where($this->scope_column, $this->scope())
  393.                         ->update($this->table_name);
  394.         }
  395.  
  396.         protected function lock() {
  397.                 // lock table
  398.                 $this->db->query('LOCK TABLE '.$this->full_table_name.' WRITE');
  399.         }
  400.  
  401.         protected function unlock() {
  402.                 // unlock tables
  403.                 $this->db->query('UNLOCK TABLES');
  404.         }
  405.  
  406.         protected function scope_available($scope) {
  407.                 // checking for supplied scope available
  408.                 return ! self::factory($this->object_name)
  409.                         ->where($this->scope_column, $scope)
  410.                         ->count_all();
  411.         }
  412.  
  413.         protected function get_next_scope() {
  414.                 // returns available value for scope
  415.                 $scope = $this->db->select(new Database_Expression('IFNULL(MAX(`'.$this->scope_column.'`), 0) as scope'))->get($this->table_name)->current();
  416.                 if ($scope AND intval($scope->scope)>0) return intval($scope->scope)+1;
  417.                 return 1;
  418.         }
  419.  
  420.         public function __get($column) {
  421.                 if ($column === 'parent')
  422.                         return $this->get_parent();
  423.                 elseif ($column === 'parents')
  424.                         return $this->get_parents();
  425.                 elseif ($column === 'children')
  426.                         return $this->get_children();
  427.                 elseif ($column === 'leaves')
  428.                         return $this->get_leaves();
  429.                 elseif ($column === 'subtree')
  430.                         return $this->get_subtree();
  431.                 elseif ($column === 'fulltree')
  432.                         return $this->get_fulltree();
  433.                 else return parent::__get($column);
  434.         }
  435.  
  436. }