<?php
/***************************************************************
Nodes Class
Author: Shadi Ali
hide@address.com: hide@address.com
You Can use this class freely in your Commercial Applications.
---------------------------------
SQL TABLE:
-- at min you must have the following fields in your table structure ..
CREATE TABLE `nodes` (
`id` BIGINT NOT NULL AUTO_INCREMENT ,
`position` VARCHAR( 255 ) NOT NULL ,
`ord` int NOT NULL ,
-- add your fields here
-- ie -> `node_name` varchar(160),
-- add your fields here
PRIMARY KEY ( `id` ) ,
INDEX ( `position` )
);
class summary:
------------------
->add_new($parent , $name , $fields ) // add new node
->delete($id) // delete existing node and all sub-nodes, returns the affected Ids .. so you could run any other operations later .. maybe deleting linked records or deleting linked image files based on the returned ids.
->update($id , $parent , $fields ) // update existing node
->build_list($id=0,$clickable=TRUE) // return array with the nodes ordered by "ord" , it could be clickable by setting $clickable = true, or else , it will be fully expanded
->browse_by_id($id) // return array with sub nodes under a specific node only.
->fetch ($id) // return existing node info.
->count_nodes($id) // get sub nodes count below a TOP-LEVEL node $id.
->order_node($id , $new_order) // change the order of a node Inside Its LEVEL.
->html_output($id , $clickable) // output a html list ( customizable via the class variable $HtmlTree );
->html_row_output($id) // out put a You>Are>Here like menu .. requires the current node id.
********************************************************************/
class nodes
{
var $HtmlTree;
var $HtmlRow;
var $table_name = "categories";
// use the following keys into the $HtmlTree varialbe.
/**************************************************
--- NO CHANGES TO BE DONE BELOW ---
**************************************************/
var $c_list = array(); // DON'T CHANGE THIS
var $Group = 0; // DON'T CHANGE THIS
function nodes($table_name = NULL)
{
$this->table_name = $table_name;
// --> use direct fields names from your mysql table into the following template variables.
// --> just put the field name inside square brackets!
// --> [fieldname] will be replaced with the correct value...
//////////////////////////////////////////////////////////////
// HtmlTree is used with $this->html_output() method to print a nested list
$this->HtmlTree = array(
"OpenTag" => '<ul>' , // this is the overall tag opener , example <ul>
"FirstLevelOpenTag" => '<li><a href="?id=[id]">[id]</a><ul>', // this is printed for the ROOT parent ( node has children/sub-nodes) ie : <ul><li><h2>[name]</h2>
"FirstLevelOpenTagSelected" => '<li><a href="?id=[id]"><b>[id]</b></a><ul>', // this is printed for the ROOT parent ( node has children/sub-nodes) ie : <ul><li><h2>[name]</h2>
"LevelOpenTag" => '<li><a href="?id=[id]">[id]</a><ul>', // this is printed for the parent ( node has children/sub-nodes) ie : <ul><li><h2>[name]</h2>
"LevelOpenTagSelected" => '<li><a href="?id=[id]"><b>[id]</b></a><ul>' , // // this is printed for the parent ( node has children/sub-nodes) .. WHEN SELECTED! ie : <ul><li><h2><STRONG>[name]</STRONG></h2>
"Node" => '<li><a href="?id=[id]">[id]</a></li>', // node item tag ..
"NodeSelected" => '<li><a href="?id=[id]"><b>[id]</b></a></li>' , // node item tag .. when selected !
"FirstLevelCloseTag" => '</ul></li>', // ROOT parent tag closer. ( when getting out of sub-level)
"FirstLevelCloseTagSelected"=> '</ul></li>', // ROOT parent tag closer, while selected.
"LevelCloseTag" => '</ul></li>', // parent tag closer. ( when getting out of sub-level)
"LevelCloseTagSelected" => '</ul></li>', // parent tag closer, while selected.
"CloseTag" => '</ul>' , // this is the overall tag opener , example <ul>
);
// HtmlTree is used with $this->html_row_output() method to print a You>Are>Here like menu
$this->HtmlRow = array(
"OpenTag" => '<div>' , // this is the overall tag opener , example <ul>
"Seprator" => ' > ', // seprator between the items. example " > " which means " > "
"NodeUnselected" => '<a href="?id=[id]">[id]</a>', // item tag ..
"NodeSelected" => '<a href="?id=[id]"><strong>[id]</strong></a>' , // item tag .. when selected !
"CloseTag" => '</div>' , // this is the overall tag opener , example <ul>
);
}
// ********************************************************
// Add New Node
// ********************************************************
function add_new($parent = 0 , $fields = array() ) // add new category
{
$keys = array_keys($fields);
$values= array_values($fields);
// lets get the position from the $parent value
$position = $this->get_position($parent);
// lets insert add the new category into the database.
$sql = "INSERT into ".$this->table_name."(position, ".implode("," , $keys).")
VALUES('', '".implode("','" , $values)."' )";
mysql_query($sql) or die(trigger_error("<br><storng><u>MySQL Error:</u></strong><br>".mysql_error()."<br><br><storng><u>Query Used:</u></strong><br>".$sql."<br><br><storng><u>Info:</u></strong><br>",E_USER_ERROR));
$node_id = mysql_insert_id();
$position .= $node_id.">";
$sql = "UPDATE ".$this->table_name."
SET position = '".$position."'
WHERE id = '".mysql_insert_id()."'";
mysql_query($sql) or die(trigger_error("<br><storng><u>MySQL Error:</u></strong><br>".mysql_error()."<br><br><storng><u>Query Used:</u></strong><br>".$sql."<br><br><storng><u>Info:</u></strong><br>",E_USER_ERROR));
$this->_optimize_orders($position);
}
// ********************************************************
// Delete Node
// ********************************************************
function delete($id) // delete this category and all categories under it
{
$myNode = $this->fetch($id);
// lets get the ids before delete operations
$sql1 = "SELECT id
FROM ".$this->table_name."";
$res1 = mysql_query($sql1) or die(trigger_error("<br><storng><u>MySQL Error:</u></strong><br>".mysql_error()."<br><br><storng><u>Query Used:</u></strong><br>".$sql1."<br><br><storng><u>Info:</u></strong><br>",E_USER_ERROR));
$before_ids = array();
while($crd_id = mysql_fetch_array($res1)){
$before_ids[] = $crd_id;
}
$position = $this->get_position($id);
$sql2 = "DELETE FROM ".$this->table_name."
WHERE
position
LIKE
'".$position."%'";
mysql_query($sql2) or die(trigger_error("<br><storng><u>MySQL Error:</u></strong><br>".mysql_error()."<br><br><storng><u>Query Used:</u></strong><br>".$sql2."<br><br><storng><u>Info:</u></strong><br>",E_USER_ERROR));
// ok now lets get the ids after the deletion.
$sql3 = "SELECT id
FROM ".$this->table_name."";
$res3 = mysql_query($sql1) or die(trigger_error("<br><storng><u>MySQL Error:</u></strong><br>".mysql_error()."<br><br><storng><u>Query Used:</u></strong><br>".$sql3."<br><br><storng><u>Info:</u></strong><br>",E_USER_ERROR));
$after_ids = array();
while($crd_id = mysql_fetch_array($res1)){
$after_ids[] = $crd_id;
}
$this->_optimize_orders($myNode['position']);
// lets return an array with the affected IDs
$affected_ids = array_diff($before_ids, $after_ids);
return $affected_ids;
}
// ********************************************************
// Update Node
// ********************************************************
function update($id , $parent = 0 , $fields= array())
{
$keys = array_keys($fields);
$values= array_values($fields);
// lets get the current position
$position = $this->get_position($id);
$new_position = $this->get_position($parent).$id.">";
if($position != $new_position){
// then we update all the sub_categories position to be still under the current category
$sql1 = "SELECT id,position
FROM ".$this->table_name."
WHERE position LIKE '".$position."%'";
$res = mysql_query($sql1) or die(trigger_error("<br><storng><u>MySQL Error:</u></strong><br>".mysql_error()."<br><br><storng><u>Query Used:</u></strong><br>".$sql1."<br><br><storng><u>Info:</u></strong><br>",E_USER_ERROR));
while($sub = mysql_fetch_array($res)){
$new_sub_position = str_replace($position,$new_position,$sub['position']);
$sql2 = "UPDATE ".$this->table_name."
SET position = '".$new_sub_position."'
WHERE position RLIKE '".$sub['id']."%>'";
mysql_query($sql2) or die(trigger_error("<br><storng><u>MySQL Error:</u></strong><br>".mysql_error()."<br><br><storng><u>Query Used:</u></strong><br>".$sql2."<br><br><storng><u>Info:</u></strong><br>",E_USER_ERROR));
}
}
// finally update the category position.
$sql3 = "UPDATE ".$this->table_name."
SET position = '".$new_position."'
WHERE position = '".$position."'";
mysql_query($sql3) or die(trigger_error("<br><storng><u>MySQL Error:</u></strong><br>".mysql_error()."<br><br><storng><u>Query Used:</u></strong><br>".$sql3."<br><br><storng><u>Info:</u></strong><br>",E_USER_ERROR));
$this->_optimize_orders($position);
$this->_optimize_orders($new_position);
$sql = "UPDATE ".$this->table_name."
SET ";
// lets see what changes should be done and add it to the sql query.
foreach($fields as $key => $value){
if ($key == 'id') continue; // no change will be done on the id
if ($key == 'position' ) continue; // position change have been done in the section above
$sql .= "".$key." = '".$value."',";
}
$sql = substr_replace($sql,"",-1); // remove the extra comma ,
$sql .= "WHERE id=".$id."";
mysql_query($sql) or die(trigger_error("<br><storng><u>MySQL Error:</u></strong><br>".mysql_error()."<br><br><storng><u>Query Used:</u></strong><br>".$sql."<br><br><storng><u>Info:</u></strong><br>",E_USER_ERROR));
}
// ********************************************************
// Build Nodes Array
// ********************************************************
function build_list($id=0,$clickable = true) //return an array with the categories ordered by position
{
$RootPos = "";
$this->c_list = array();
if($id != 0){
$this_category = $this->fetch($id);
$positions = explode(">",$this_category['position']);
$RootPos = $positions[0];
}
// lets fetch the root categories
$sql = "SELECT *
FROM ".$this->table_name."
WHERE position RLIKE '^([0-9]+>){1,1}$'";
$res = mysql_query($sql) or die(trigger_error("<br><storng><u>MySQL Error:</u></strong><br>".mysql_error()."<br><br><storng><u>Query Used:</u></strong><br>".$sql."<br><br><storng><u>Info:</u></strong><br>",E_USER_ERROR));
while($root = mysql_fetch_array($res)){
$root["prefix"] = $this->get_prefix($root['position']);
$this->c_list[$root['id']] = $root;
if($RootPos == $root['id'] AND $id != 0 AND $clickable){
$this->list_by_id($id);
continue;
}else{
// lets check if there is sub-categories
if($clickable == "" AND $id==0){
$has_children = $this->has_children($root['position']);
if($has_children == TRUE) $this->get_children($root['position'],0);
}
}}
return $this->c_list;
}
// ********************************************************
// Check if Node has childrens
// ********************************************************
function has_children($position) // return TRUE if that position has sub-categories otherwise returns FALSE
{
$check_sql = "SELECT id FROM ".$this->table_name." WHERE position RLIKE '^".$position."[0-9]+>$'";
$check_res = mysql_query($check_sql) or die(trigger_error("<br><storng><u>MySQL Error:</u></strong><br>".mysql_error()."<br><br><storng><u>Query Used:</u></strong><br>".$check_sql."<br><br><storng><u>Info:</u></strong><br>",E_USER_ERROR));
$check = mysql_fetch_array($check_res);
if($check['id'] != "")return TRUE;
else return FALSE;
}
// ********************************************************
// Get Childrens
// ********************************************************
function get_children($position , $id = 0){
$sql = "SELECT *
FROM ".$this->table_name."
WHERE position RLIKE '^".$position."[0-9]+>$'
ORDER BY ord";
$res = mysql_query($sql) or die(trigger_error("<br><storng><u>MySQL Error:</u></strong><br>".mysql_error()."<br><br><storng><u>Query Used:</u></strong><br>".$sql."<br><br><storng><u>Info:</u></strong><br>",E_USER_ERROR));
while($child = mysql_fetch_array($res)){
$child["prefix"] = $this->get_prefix($child['position']);
if($id != 0)
{
$this->c_list_by_id[$child['id']] = $child;
$has_children = $this->has_children($child['position']);
if($has_children == TRUE){
$this->get_children($child['position']);
}
continue;
}else{
// lets check if there is sub-categories
$this->c_list[$child['id']] = $child;
$has_children = $this->has_children($child['position']);
if($has_children == TRUE)$this->get_children($child['position']);
}}
}
// ********************************************************
// Get children of Specific nodes only.
// ********************************************************
function list_by_id($id) //return an array with the categories under the given ID and ordered by name
{
$this_category = $this->fetch($id);
$positions = explode(">",$this_category['position']);
$pCount = count($positions);
$i = 0;
// lets fetch from top to center
while($i < $pCount){
$pos_id = $positions["$i"];
if($pos_id == ""){$i++; continue;}
$list = $this->browse_by_id($pos_id);
foreach($list as $key=>$value){
$this->c_list["$key"] = $value;
$ni = $i + 1;
$nxt_id = $positions[$ni];
if($key == $nxt_id ) break;
} $i++;
}
//center to end
$i = $pCount-1;
while($i >= 0){
$pos_id = $positions["$i"];
if($pos_id == ""){$i--; continue;}
$list = $this->browse_by_id($pos_id);
foreach($list as $key=>$value){
$ni = $i - 1;
if($ni < 0) $ni =0;
$nxt_id = $positions[$ni];
if($key == $nxt_id ) break;
$this->c_list["$key"] = $value;
} $i--;
}
}
/***************************************
Get array of nodes under specific category.
****************************************/
function browse_by_id($id) // return array of categories under specific category.
{
$children = array();
$this_category = $this->fetch($id);
$position = $this_category['position'];
$sql = "SELECT *
FROM ".$this->table_name."
WHERE position RLIKE '^".$position."(([0-9])+\>){1}$'
ORDER BY ord";
$res = mysql_query($sql) or die(trigger_error("<br><storng><u>MySQL Error:</u></strong><br>".mysql_error()."<br><br><storng><u>Query Used:</u></strong><br>".$sql."<br><br><storng><u>Info:</u></strong><br>",E_USER_ERROR));
while($child = mysql_fetch_array($res)){
$child["prefix"] = $this->get_prefix($child['position']);
$children[$child['id']] = $child;
}
return $children;
}
// ********************************************************
// Get Position
// ********************************************************
function get_position($id)
{
if($id == 0)return "";
$sql = "SELECT position
FROM ".$this->table_name."
WHERE id = '".$id."'";
$res = mysql_query($sql) or die(trigger_error("<br><storng><u>MySQL Error:</u></strong><br>".mysql_error()."<br><br><storng><u>Query Used:</u></strong><br>".$sql."<br><br><storng><u>Info:</u></strong><br>",E_USER_ERROR));
$record = mysql_fetch_array($res);
return $record['position'];
}
// ********************************************************
// Get Prefix Count
// ********************************************************
function get_prefix($position)
{
$prefix = "";
$position_slices = explode(">",$position);
$count = count($position_slices) - 1;
return $count;
}
// ********************************************************
// Fetch Node Record
// ********************************************************
function fetch ($id)
{
$sql = "SELECT *
FROM ".$this->table_name."
WHERE id = '".$id."'";
$res = mysql_query($sql) or die(trigger_error("<br><storng><u>MySQL Error:</u></strong><br>".mysql_error()."<br><br><storng><u>Query Used:</u></strong><br>".$sql."<br><br><storng><u>Info:</u></strong><br>",E_USER_ERROR));
$record = mysql_fetch_array($res);
$record["prefix"] = $this->get_prefix($record['position']);
$position_slices = explode(">",$record['position']);
$key = count($position_slices)-3;
if($key < 0) $key = 0;
$record["parent"] = $position_slices["$key"];
return $record;
}
// ********************************************************
// Build HTML output
// ********************************************************
function html_output($id=0 , $clickable = false)
{
if(!$clickable){
$tree = $this->build_list( 0 , 0 ); // display the full list
}else{
$tree = $this->build_list($id , $clickable); // display clickable list (one sub-level list)
}
$output = "\n<!-- Using OpenTag -->\n";
$output .= $this->HtmlTree["OpenTag"];
if(is_array($tree))
{
$start = 1;
$next_loop_level = 1;
$tree = array_values($tree);
$end = count($tree);
for($i=0; $i<$end ;$i++)
{
$body = "";
$c = $tree[$i];
$i2 = $i + 1;
if($i2 < $end){
$next_loop = $tree[$i2];
$next_loop_level = $next_loop['prefix'];
}
// are we getting into sub-level the next loop ?
if( $next_loop_level > $c['prefix']){
if($c['prefix'] > 1){
// if so then lets use the LevelOpenTag
$body .= "\n<!-- Using LevelOpenTag -->\n";
if($c['id'] == $id) $body .= $this->HtmlTree['LevelOpenTagSelected'];
else $body .= $this->HtmlTree['LevelOpenTag'];
}else{
$body .= "\n<!-- Using FirstLevelOpenTag -->\n";
// we are on the roots.
if($c['id'] == $id) $body .= $this->HtmlTree['FirstLevelOpenTagSelected'];
else $body .= $this->HtmlTree['FirstLevelOpenTag'];
}
}elseif( $next_loop_level < $c['prefix'] AND $next_loop_level >= 1){
if($next_loop_level == 1){
// we are on the roots.
$body .= "\n<!-- Using Node -->\n";
if($c['id'] == $id) $body .= $this->HtmlTree['NodeSelected'];
else $body .= $this->HtmlTree['Node'];
$body .= "\n<!-- Using FirstLevelCloseTag -->\n";
if($c['id'] == $id) $body .= $this->HtmlTree['FirstLevelCloseTagSelected'];
else $body .= $this->HtmlTree['FirstLevelCloseTag'];
}else{
// if so then lets use the LevelCloseTag
$body .= "\n<!-- Using Node -->\n";
if($c['id'] == $id) $body .= $this->HtmlTree['NodeSelected'];
else $body .= $this->HtmlTree['Node'];
for($j = $c['prefix']; $j >= $next_loop_level ; $j--){
$body .= "\n<!-- Using LevelCloseTag -->\n";
if($c['id'] == $id) $body .= $this->HtmlTree['LevelCloseTagSelected'];
else $body .= $this->HtmlTree['LevelCloseTag'];
}
}
}else{
// neither getting in or out of a level .. use the normal node tags
$body .= "\n<!-- Using Node -->\n";
if($c['id'] == $id) $body .= $this->HtmlTree['NodeSelected'];
else $body .= $this->HtmlTree['Node'];
}
foreach($c as $key => $value)
{
$body = str_replace("[$key]" ,$value, $body);
}
$next_loop_level--;
$output .= $body;
}
}
$output .= "\n<!-- Using CloseTag -->\n";
$output .= $this->HtmlTree['CloseTag'];
return $output;
}
// This function output a You-Are-Here like menu.
// Home > Articles > "Progamming"
// it takes the current Node id.
////////////////////////////////////////////////////
function html_row_output($id){
// first we get the position chain
$position_chain = $this->get_position($id);
$positions = explode(">" , $position_chain);
// we loop through the chain and echo every node using the template.
$output = $this->HtmlRow['OpenTag'];
foreach($positions as $nid){
$body = ""; // we initialize the body;
if(!$nid) continue; // thats for the last position its always a null space, so we just ignore it!
$c = $this->fetch($nid); // we fetch the node from the database;
if($id == $nid)
$body .= $this->HtmlRow['NodeSelected'];
else{
$body .= $this->HtmlRow['NodeUnselected'];
$body .= $this->HtmlRow['Seprator'];
}
// now lets replace the keys in the templates with the values
foreach($c as $key => $value)
{
$body = str_replace("[$key]" ,$value, $body);
}
$output .= $body;
}
$output .= $this->HtmlRow['CloseTag'];
return $output;
}
// get the count of sub-nodes of a parent node
// requires an ID of the parent node
//////////////////////////////////////////////////////
function count_nodes($cat_id)
{
$thisPosition = $this->get_position($cat_id);
$sql = "SELECT *
FROM ".$this->table_name."
WHERE position LIKE '".$thisPosition."%'";
$res = mysql_query($sql) or die(trigger_error("<br><storng><u>MySQL Error:</u></strong><br>".mysql_error()."<br><br><storng><u>Query Used:</u></strong><br>".$sql."<br><br><storng><u>Info:</u></strong><br>",E_USER_ERROR));
$count = mysql_num_rows($res);
$count-= 1; // remove the category itself from the count
return $count;
}
// Change the order of a inside its level.
// requires an ID of and the New ORDER ...
// new order must be between 1 and $max ( max = total nodes in the level)
//////////////////////////////////////////////////////
function order_node($id , $new_order){
$myNode = $this->fetch($id);
$thisPosition = $this->get_position($id);
$PositionS = explode(">" , $thisPosition);
array_pop($PositionS);
array_pop($PositionS);
$parentPosition = implode(">", $PositionS).">";
// ok lets count the nodes in the same level;
$sql = "SELECT *
FROM ".$this->table_name."
WHERE position RLIKE '^".$parentPosition."(([0-9])+\>){1}$'";
$res = mysql_query($sql) or die(trigger_error("<br><storng><u>MySQL Error:</u></strong><br>".mysql_error()."<br><br><storng><u>Query Used:</u></strong><br>".$sql."<br><br><storng><u>Info:</u></strong><br>",E_USER_ERROR));
$current_order = $myNode['ord'];
$max = mysql_num_rows($res); // total;
// lets check the new order;
if($new_order > $max or $new_order == -1)
$new_order = $max;
elseif($new_order < 1)
$new_order = 1;
// return false;
// update the replaced Node.
$sql2 = "UPDATE ".$this->table_name."
SET ord = '".$current_order."'
WHERE position RLIKE '^".$parentPosition."(([0-9])+\>){1}$' AND ord = '".$new_order."'";
$res2 = mysql_query($sql2) or die(trigger_error("<br><storng><u>MySQL Error:</u></strong><br>".mysql_error()."<br><br><storng><u>Query Used:</u></strong><br>".$sql2."<br><br><storng><u>Info:</u></strong><br>",E_USER_ERROR));
// update the selected Node.
$sql3 = "UPDATE ".$this->table_name."
SET ord = '".$new_order."'
WHERE position = '".$thisPosition."'";
$res3 = mysql_query($sql3) or die(trigger_error("<br><storng><u>MySQL Error:</u></strong><br>".mysql_error()."<br><br><storng><u>Query Used:</u></strong><br>".$sql3."<br><br><storng><u>Info:</u></strong><br>",E_USER_ERROR));
// done.
}
// Walk through the level and fix false nodes order.
// requires a given level position.
////////////////////////////////////////////////////
function _optimize_orders($position){
$PositionS = explode(">" , $position);
array_pop($PositionS);
array_pop($PositionS);
$parentPosition = implode(">", $PositionS).">";
// ok lets count the nodes in the same level;
$sql = "SELECT *
FROM ".$this->table_name."
WHERE position RLIKE '^".$parentPosition."(([0-9])+\>){1}$' order by ord ASC";
$res = mysql_query($sql) or die(trigger_error("<br><storng><u>MySQL Error:</u></strong><br>".mysql_error()."<br><br><storng><u>Query Used:</u></strong><br>".$sql."<br><br><storng><u>Info:</u></strong><br>",E_USER_ERROR));
$max = mysql_num_rows($res); // total;
// now we got an ordered list of the nodes inside level .. it should be 1 , 2, 3 ... $max , what if something wasn't there ? lets fix that.
for($i = 1; $i <= $max ; $i++){
$node = mysql_fetch_array($res);
if(!$node) break;
if($i != $node['ord']){
$sql2 = "UPDATE ".$this->table_name."
SET ord = '".$i."'
WHERE id = '".$node['id']."' LIMIT 1";
$res2 = mysql_query($sql2) or die(trigger_error("<br><storng><u>MySQL Error:</u></strong><br>".mysql_error()."<br><br><storng><u>Query Used:</u></strong><br>".$sql2."<br><br><storng><u>Info:</u></strong><br>",E_USER_ERROR));
}
}
}
} // Class END
?>