/library/classes/Tree.class.php
PHP | 355 lines | 183 code | 64 blank | 108 comment | 41 complexity | 755fc23708ce7e02074600e9dd247e29 MD5 | raw file
- <?php
- define("ROOT_TYPE_ID",1);
- define("ROOT_TYPE_NAME",2);
- /**
- * class Tree
- * This class is a clean implementation of a modified preorder tree traversal hierachy to relational model
- * Don't use this class directly as it won't work, extend it and set the $this->_table variable, currently
- * this class needs its own sequence per table. MPTT uses a lot of self referential parent child relationships
- * and having ids that are more or less sequential makes human reading, fixing and reconstruction much easier.
- */
- class Tree {
- /*
- * This is the name of the table this tree is stored in
- * @var string
- */
- var $_table;
-
- /*
- * This is a lookup table so that you can get a node name or parent id from its id
- * @var array
- */
- var $_id_name;
-
- /*
- * This is a db abstraction object compatible with ADODB
- * @var object the constructor expects it to be available as $GLOBALS['adodb']['db']
- */
- var $_db;
-
- /*
- * The constructor takes a value and a flag determining if the value is the id of a the desired root node or the name
- * @param mixed $root name or id of desired root node
- * @param int $root_type optional flag indicating if $root is a name or id, defaults to id
- */
- function Tree($root,$root_type = ROOT_TYPE_ID) {
- $this->_db = $GLOBALS['adodb']['db'];
- $this->_root = mysql_real_escape_string($root);
- $this->_root_type = mysql_real_escape_string($root_type);
- $this->load_tree();
- }
-
- function load_tree() {
- $root = $this->_root;
- $tree = array();
- $tree_tmp = array();
-
- //get the left and right value of the root node
- $sql = "SELECT * FROM " . $this->_table . " WHERE id='".$root."'";
-
- if ($this->root_type == ROOT_TYPE_NAME) {
- $sql = "SELECT * FROM " . $this->_table . " WHERE name='".$root."'";
- }
- $result = $this->_db->Execute($sql) or die("Error: " . $this->_db->ErrorMsg());
- $row = array();
- if($result && !$result->EOF) {
- $row = $result->fields;
- }
- else {
- $this->tree = array();
- }
- // start with an empty right stack
- $right = array();
- // now, retrieve all descendants of the root node
- $sql = "SELECT * FROM " . $this->_table . " WHERE lft BETWEEN " . $row['lft'] . " AND " . $row['rght'] . " ORDER BY parent,name ASC;";
- $result = $this->_db->Execute($sql);
- $this->_id_name = array();
-
- while ($result && !$result->EOF) {
- $ar = array();
- $row = $result->fields;
-
- //create a lookup table of id to name for every node that will end up in this tree, this is used
- //by the array building code below to find the chain of parents for each node
-
- // ADDED below by BM on 06-2009 to translate categories, if applicable
- if ($this->_table == "categories") {
- $this->_id_name[$row['id']] = array("id" => $row['id'], "name" => xl_document_category($row['name']), "parent" => $row['parent']);
- }
- else {
- $this->_id_name[$row['id']] = array("id" => $row['id'], "name" => $row['name'], "parent" => $row['parent']);
- }
-
- // only check stack if there is one
- if (count($right)>0) {
- // check if we should remove a node from the stack
- while ($right[count($right)-1]<$row['rght']) {
- array_pop($right);
- }
- }
- //set up necessary variables to then determine the chain of parents for each node
- $parent = $row['parent'];
- $loop = 0;
-
- //this is a string that gets evaled below to create the array representing the tree
- $ar_string = "[\"".($row['id']) ."\"] = \$row[\"value\"]";
-
- //if parent is 0 then the node has no parents, the number of nodes in the id_name lookup always includes any nodes
- //that could be the parent of any future node in the record set, the order is deterministic because of the algorithm
- while($parent != 0 && $loop < count($this->_id_name)) {
- $ar_string = "[\"" . ($this->_id_name[$parent]['id']) . "\"]" . $ar_string;
- $loop++;
- $parent = $this->_id_name[$parent]['parent'];
- }
-
- $ar_string = '$ar' . $ar_string . ";";
- //echo $ar_string;
-
- //now eval the string to create the tree array
- //there must be a more efficient way to do this than eval?
- eval($ar_string);
-
- //merge the evaled array with all of the already exsiting tree elements,
- //merge recursive is used so that no keys are replaced in other words a key
- //with a specific value will not be replace but instead that value will be turned into an array
- //consisting of the previous value and the new value
- $tree = array_merge_n($tree,$ar);
-
- // add this node to the stack
- $right[] = $row['rght'];
- $result->MoveNext();
- }
-
- $this->tree = $tree;
- }
- /*
- * This function completely rebuilds a tree starting from parent to ensure that all of its preorder values
- * are integrous.
- * Upside is that it fixes any kind of goofiness, downside is that it is recursive and consequently
- * exponentially expensive with the size of the tree.
- * On adds and deletes the tree does dynamic updates as appropriate to maintain integrity of the algorithm,
- * however you can still force it to do goofy things and afterwards you will need this function to fix it.
- * If you need to do a huge number of adds or deletes it will be much faster to act directly on the db and then
- * call this to fix the mess than to use the add and delete functions.
- * @param int $parent id of the node you would like to rebuild all nodes below
- * @param int $left optional proper left value of the node you are rebuilding below, then used recursively
- */
- function rebuild_tree($parent, $left = null) {
-
- //if no left is supplied assume the existing left is proper
- if ($left == null) {
- $sql = "SELECT lft FROM " . $this->_table . " WHERE id='" . $parent . "';";
- $result = $this->_db->Execute($sql) or die("Error: " . $this->_db->ErrorMsg());
- if($result && !$result->EOF) {
- $left = $result->fields['lft'];
- }
- else {
- //the node you are rebuilding below if goofed up and you didn't supply a proper value
- //nothing we can do so error
- die("Error: The node you are rebuilding from could not be found, please supply an existing node id.");
- }
- }
- // get all children of this node
- $sql = "SELECT id FROM " . $this->_table . " WHERE parent='" . $parent . "' ORDER BY id;";
- $result = $this->_db->Execute($sql) or die("Error: " . $this->_db->ErrorMsg());
-
- // the right value of this node is the left value + 1
- $right = $left+1;
-
- while ($result && !$result->EOF) {
- $row = $result->fields;
- // recursive execution of this function for each
- // child of this node
- // $right is the current right value, which is
- // incremented by the rebuild_tree function
- $right = $this->rebuild_tree($row['id'], $right);
- $result->MoveNext();
- }
- // we've got the left value, and now that we've processed
- // the children of this node we also know the right value
- $sql = "UPDATE " . $this->_table . " SET lft=".$left.", rght=".$right." WHERE id='".$parent."';";
- //echo $sql . "<br>";
- $this->_db->Execute($sql) or die("Error: $sql" . $this->_db->ErrorMsg());
- // return the right value of this node + 1
- return $right+1;
- }
-
- /*
- * Call this to add a new node to the tree
- * @param int $parent id of the node you would like the new node to have as its parent
- * @param string $name the name of the new node, it will be used to reference its value in the tree array
- * @param string $value optional value this node is to contain
- * @return int id of newly added node
- */
- function add_node($parent_id,$name,$value="") {
-
- $sql = "SELECT * from " . $this->_table . " where parent = '" . $parent_id . "' and name='" . $name . "'";
- $result = $this->_db->Execute($sql) or die("Error: " . $this->_db->ErrorMsg());
-
- if ($result && !$result->EOF) {
- die("You cannot add a node with the name '" . $name ."' because one already exists under parent " . $parent_id . "<br>");
- }
-
- $sql = "SELECT * from " . $this->_table . " where id = '" . $parent_id . "'";
- $result = $this->_db->Execute($sql) or die("Error: " . $this->_db->ErrorMsg());
-
- $next_right = 0;
-
- if ($result && !$result->EOF) {
- $next_right = $result->fields['rght'];
- }
-
- $sql = "UPDATE " . $this->_table . " SET rght=rght+2 WHERE rght>=" . $next_right;
- $this->_db->Execute($sql) or die("Error: " . $this->_db->ErrorMsg());
- $sql = "UPDATE " . $this->_table . " SET lft=lft+2 WHERE lft>=" . $next_right;
- $this->_db->Execute($sql) or die("Error: " . $this->_db->ErrorMsg());
-
- $id = $this->_db->GenID($this->_table . "_seq");
- $sql = "INSERT INTO " . $this->_table . " SET name='" . $name . "', value='" . $value . "', lft='" . $next_right . "', rght='" . ($next_right + 1) . "', parent='" . $parent_id . "', id='" . $id . "'";
- $this->_db->Execute($sql) or die("Error: $sql :: " . $this->_db->ErrorMsg());
- //$this->rebuild_tree(1,1);
- $this->load_tree();
- return $id;
- }
-
- /*
- * Call this to delete a node from the tree, the nodes children (and their children, etc) will become children
- * of the deleted nodes parent
- * @param int $id id of the node you want to delete
- */
- function delete_node($id) {
-
- $sql = "SELECT * from " . $this->_table . " where id = '" . $id . "'";
- //echo $sql . "<br>";
- $result = $this->_db->Execute($sql) or die("Error: " . $this->_db->ErrorMsg());
-
- $left = 0;
- $right = 1;
- $new_parent = 0;
-
- if ($result && !$result->EOF) {
- $left = $result->fields['lft'];
- $right = $result->fields['rght'];
- $new_parent = $result->fields['parent'];
- }
-
- $sql = "UPDATE " . $this->_table . " SET rght=rght-2 WHERE rght>" . $right;
- //echo $sql . "<br>";
- $this->_db->Execute($sql) or die("Error: " . $this->_db->ErrorMsg());
-
- $sql = "UPDATE " . $this->_table . " SET lft=lft-2 WHERE lft>" . $right;
- //echo $sql . "<br>";
- $this->_db->Execute($sql) or die("Error: " . $this->_db->ErrorMsg());
-
- $sql = "UPDATE " . $this->_table . " SET lft=lft-1, rght=rght-1 WHERE lft>" . $left . " and rght < " . $right;
- //echo $sql . "<br>";
- $this->_db->Execute($sql) or die("Error: " . $this->_db->ErrorMsg());
-
- //only update the childrens parent setting if the node has children
- if ($right > ($left +1)) {
- $sql = "UPDATE " . $this->_table . " SET parent='" . $new_parent . "' WHERE parent='" . $id . "'";
- //echo $sql . "<br>";
- $this->_db->Execute($sql) or die("Error: " . $this->_db->ErrorMsg());
- }
-
- $sql = "DELETE FROM " . $this->_table . " where id='" . $id . "'";
- //echo $sql . "<br>";
- $this->_db->Execute($sql) or die("Error: " . $this->_db->ErrorMsg());
- $this->load_tree();
-
- return true;
- }
-
- function get_node_info($id) {
- if(!empty($this->_id_name[$id])) {
- return $this->_id_name[$id];
- }
- else {
- return array();
- }
- }
-
- function get_node_name($id) {
- if(!empty($this->_id_name[$id])) {
- return $this->_id_name[$id]['name'];
- }
- else {
- return false;
- }
- }
- }
- function array_merge_2(&$array, &$array_i) {
- // For each element of the array (key => value):
- foreach ($array_i as $k => $v) {
- // If the value itself is an array, the process repeats recursively:
- if (is_array($v)) {
- if (!isset($array[$k])) {
- $array[$k] = array();
- }
- array_merge_2($array[$k], $v);
- // Else, the value is assigned to the current element of the resulting array:
- } else {
- if (isset($array[$k]) && is_array($array[$k])) {
- $array[$k][0] = $v;
- } else {
- if (isset($array) && !is_array($array)) {
- $temp = $array;
- $array = array();
- $array[0] = $temp;
- }
- $array[$k] = $v;
- }
- }
- }
- }
- function array_merge_n() {
- // Initialization of the resulting array:
- $array = array();
- // Arrays to be merged (function's arguments):
- $arrays =& func_get_args();
- // Merging of each array with the resulting one:
- foreach ($arrays as $array_i) {
- if (is_array($array_i)) {
- array_merge_2($array, $array_i);
- }
- }
- return $array;
- }
- /*
- $db = $GLOBALS['adodb']['db'];
- $sql = "USE document;";
- $db->Execute($sql);
- $t = new Tree(1);
- echo "<pre>";
- print_r($t->tree);
- //$nid = $t->add_node(0,"test node2","test value");
- //$t->add_node($nid,"test child","another value");
- //$t->add_node($nid,"test child");
- print_r($t->tree);
- echo "</pre>";
- */
- ?>