I am trying to get my data which is hierarchically set up with a tree traversal model into an < ul> in order to show on my site.
Here is my code:
function getCats($) {
// retrieve all children of $parent
$query = "SELECT max(rght) as max from t_categories";
$row = C_DB::fetchSingleRow($query);
$max = $row["max"];
$result ="<ul>";
$query = "SELECT * from t_categories where lft >=0 and rght <= $max";
if($rs = C_DB::fetchRecordset($query)){
$p_right ="";
$p_left ="";
$p_diff="";
while($row = C_DB::fetchRow($rs)){
$diff = $row["rght"] -$row["lft"];
if($diff == $p_diff){
$result.= "<li>".$row['title']."</li>";
}elseif (($row["rght"] - $row["lft"] > 1) && ($row["rght"] > $p_right)){
$result. "<ul>";
$result.= "<li>".$row['title']."</li>";
}else{
$result.= "<li>".$row['title']."</li>";
}
$p_right = $row["rght"];
$p_left = $row["lft"];
$p_diff = $diff;
}
}
$result.= "</ul>";
return $result;
}
Here is my sample table:
|ID | TITLE | lft| rght |
|1 | Cat 1 | 1 | 16 |
|18 | Cat 2 | 3 | 4 |
|22 | Cat 3 | 5 | 6 |
|28 | Cat 4 | 7 | 8 |
|34 | Cat 5 | 9 | 9 |
|46 | Cat 6 | 11 | 10 |
|47 | Cat 7 | 13 | 12 |
|49 | Cat 8 | 15 | 14 |
Now it outputs something like:
<ul>
<li>Cat 1</li>
<li>Cat 2</li>
<li>Cat 3</li>
<li>Cat 4</li>
<li>Cat 5</li>
<li>Cat 6</li>
<li>Cat 7</li>
<li>Cat 8</li>
</ul>
Can anyone tell me why or how it will output the list in hierarchical a structure?
7 Answers 7
Ok, let's do some bounty hunting ;)
Step 0 - Sanitize example:
As already mentioned, your example data is broken, as it does not define a valid nested set. If you took this data from an app, you should check the insert/delete logic.
So for testing, I used a sanitized version like so:
(MySQL here, as it was the first at hand)
CREATE TABLE t_categories`(
`id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
`title` VARCHAR(45) NOT NULL,
`lft` INTEGER UNSIGNED NOT NULL,
`rght` INTEGER UNSIGNED NOT NULL,
PRIMARY KEY (`id`)
);
INSERT INTO t_categories (title, lft, rght) VALUES ('Cat 1',1,16);
INSERT INTO t_categories (title, lft, rght) VALUES ('Cat 2',2,3);
INSERT INTO t_categories (title, lft, rght) VALUES ('Cat 3',4,7);
INSERT INTO t_categories (title, lft, rght) VALUES ('Cat 4',5,6);
INSERT INTO t_categories (title, lft, rght) VALUES ('Cat 5',8,13);
INSERT INTO t_categories (title, lft, rght) VALUES ('Cat 6',9,12);
INSERT INTO t_categories (title, lft, rght) VALUES ('Cat 7',10,11);
INSERT INTO t_categories (title, lft, rght) VALUES ('Cat 8',14,15);
Step 1 - Let the database do the ordering
Nested sets where primarily invented as a convenient way of storing trees in databases, as they make it pretty easy to query for subtrees, parent relations and, especially interesting in this case, for order and depth:
SELECT node.title, (COUNT(parent.title) - 1) AS depth
FROM t_categories AS node
CROSS JOIN t_categories AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rght
GROUP BY node.title
ORDER BY node.lft
This will return your set neatly ordered, starting with the root node and continuing to the end in preorder. Most importantly, it will add the depth of each node as a positive integer, indicating how many levels the node is below root (level 0). For the above example data, the result will be:
title, depth
'Cat 1', 0
'Cat 2', 1
'Cat 3', 1
'Cat 4', 2
'Cat 5', 1
'Cat 6', 2
'Cat 7', 3
'Cat 8', 1
In code:
// Grab ordered data
$query = '';
$query .= 'SELECT node.title, (COUNT(parent.title) - 1) AS depth';
$query .= ' FROM t_categories AS node';
$query .= ' CROSS JOIN t_categories AS parent';
$query .= ' WHERE node.lft BETWEEN parent.lft AND parent.rght';
$query .= ' GROUP BY node.title';
$query .= ' ORDER BY node.lft';
$result = mysql_query($query);
// Build array
$tree = array();
while ($row = mysql_fetch_assoc($result)) {
$tree[] = $row;
}
The resulting array will look like this:
Array
(
[0] => Array
(
[title] => Cat 1
[depth] => 0
)
[1] => Array
(
[title] => Cat 2
[depth] => 1
)
...
)
Step 2 - Output as HTML list fragment:
Using while loop:
// bootstrap loop
$result = '';
$currDepth = -1; // -1 to get the outer <ul>
while (!empty($tree)) {
$currNode = array_shift($tree);
// Level down?
if ($currNode['depth'] > $currDepth) {
// Yes, open <ul>
$result .= '<ul>';
}
// Level up?
if ($currNode['depth'] < $currDepth) {
// Yes, close n open <ul>
$result .= str_repeat('</ul>', $currDepth - $currNode['depth']);
}
// Always add node
$result .= '<li>' . $currNode['title'] . '</li>';
// Adjust current depth
$currDepth = $currNode['depth'];
// Are we finished?
if (empty($tree)) {
// Yes, close n open <ul>
$result .= str_repeat('</ul>', $currDepth + 1);
}
}
print $result;
Same logic as recursive function:
function renderTree($tree, $currDepth = -1) {
$currNode = array_shift($tree);
$result = '';
// Going down?
if ($currNode['depth'] > $currDepth) {
// Yes, prepend <ul>
$result .= '<ul>';
}
// Going up?
if ($currNode['depth'] < $currDepth) {
// Yes, close n open <ul>
$result .= str_repeat('</ul>', $currDepth - $currNode['depth']);
}
// Always add the node
$result .= '<li>' . $currNode['title'] . '</li>';
// Anything left?
if (!empty($tree)) {
// Yes, recurse
$result .= renderTree($tree, $currNode['depth']);
}
else {
// No, close remaining <ul>
$result .= str_repeat('</ul>', $currNode['depth'] + 1);
}
return $result;
}
print renderTree($tree);
Both will output the following structure:
<ul>
<li>Cat 1</li>
<li>
<ul>
<li>Cat 2</li>
<li>Cat 3</li>
<li>
<ul>
<li>Cat 4</li>
</ul>
</li>
<li>Cat 5</li>
<li>
<ul>
<li>Cat 6</li>
<li>
<ul>
<li>Cat 7</li>
</ul>
</li>
</ul>
</li>
<li>Cat 8</li>
</ul>
</li>
</ul>
Nitpickers corner: Questioner explicitly asked for <ul>
, but ordered unordered lists!? Come on...
;-)
-
is there a similar function to put the data into a nested array?Alex L– Alex L2009年09月24日 19:42:45 +00:00Commented Sep 24, 2009 at 19:42
-
@Alex L: Sure, but it would depend how you'd want to do the nesting. You'd basically use the same logic as for the list generation, but you'd need to add a 'tracking' of the current parent/child chain to allow for the 'backtracking' to a parent array, as you can not just 'close' it like the nested ul tags.Henrik Opel– Henrik Opel2009年10月18日 20:00:07 +00:00Commented Oct 18, 2009 at 20:00
-
Did you figure out how to build arrays from this result set? I know that I have to add a 'tracking', but how? I've extended this question: stackoverflow.com/questions/3668702/…Keyne Viana– Keyne Viana2010年09月08日 23:00:13 +00:00Commented Sep 8, 2010 at 23:00
-
2@Henrik Opel.. i tested your recursive function and it doesn't works well... sub tree (UL) must be inside <li> tag. not outside of it... in your code "$result .= '<li>' . $currNode['title'] . '</li>'" <li> tag is closed, where it was opened.user474470– user4744702012年06月25日 09:31:04 +00:00Commented Jun 25, 2012 at 9:31
-
How should I Insert it dynamically? See my question: stackoverflow.com/questions/11716731/…J.K.A.– J.K.A.2012年07月30日 14:36:19 +00:00Commented Jul 30, 2012 at 14:36
Better Render Tree Function that worked for me (php function to prepare html source for use in jsTree jQuery plugin) instead of the Henrik Opel's one:
function MyRenderTree ( $tree = array(array('name'=>'','depth'=>'')) ){
$current_depth = 0;
$counter = 0;
$result = '<ul>';
foreach($tree as $node){
$node_depth = $node['depth'];
$node_name = $node['name'];
$node_id = $node['category_id'];
if($node_depth == $current_depth){
if($counter > 0) $result .= '</li>';
}
elseif($node_depth > $current_depth){
$result .= '<ul>';
$current_depth = $current_depth + ($node_depth - $current_depth);
}
elseif($node_depth < $current_depth){
$result .= str_repeat('</li></ul>',$current_depth - $node_depth).'</li>';
$current_depth = $current_depth - ($current_depth - $node_depth);
}
$result .= '<li id="c'.$node_id.'"';
$result .= $node_depth < 2 ?' class="open"':'';
$result .= '><a href="#"><ins> </ins>'.$node_name.'</a>';
++$counter;
}
$result .= str_repeat('</li></ul>',$node_depth).'</li>';
$result .= '</ul>';
return $result;}
Result HTML:
<ul>
<li id="c1" class="open"><a href="#"><ins> </ins>ELECTRONICS</a>
<ul>
<li id="c2" class="open"><a href="#"><ins> </ins>TELEVISIONS</a>
<ul>
<li id="c3"><a href="#"><ins> </ins>TUBE</a></li>
<li id="c4"><a href="#"><ins> </ins>LCD</a></li>
<li id="c5"><a href="#"><ins> </ins>PLASMA</a>
<ul>
<li id="c14"><a href="#"><ins> </ins>PLASMA1</a></li>
<li id="c15"><a href="#"><ins> </ins>PLASMA2</a></li>
</ul>
</li>
</ul>
</li>
<li id="c6" class="open"><a href="#"><ins> </ins>PORTABLE ELECTRONICS</a>
<ul>
<li id="c7"><a href="#"><ins> </ins>MP3 PLAYERS</a>
<ul>
<li id="c8"><a href="#"><ins> </ins>FLASH</a></li>
</ul>
</li>
<li id="c9"><a href="#"><ins> </ins>CD PLAYERS</a></li>
<li id="c10"><a href="#"><ins> </ins>2 WAY RADIOS</a></li>
</ul>
</li>
</ul>
</li>
</ul>
-
1I prefer this answer since it respects W3C Wiki on lists. Source: stackoverflow.com/questions/5899337/…tampe125– tampe1252014年08月08日 09:34:24 +00:00Commented Aug 8, 2014 at 9:34
-
Could be rewritten to use recursive function. This solution might be faster though.Jan Vorisek– Jan Vorisek2017年08月31日 22:45:00 +00:00Commented Aug 31, 2017 at 22:45
There's a PEAR package for dealing with nested sets: DB_NestedSet.
You might also be interested in the article Managing Hierarchical Data in MySQL.
This should be what you're looking for:
function getCats($left = null, $right = null)
{
$sql = array();
$result = null;
if (isset($left) === true)
{
$sql[] = 'lft >= ' . intval($left);
}
if (isset($right) === true)
{
$sql[] = 'rght <= ' . intval($right);
}
if (empty($sql) === true)
{
$sql[] = 'lft = 1';
}
$sql = 'SELECT * FROM t_categories WHERE ' . implode(' AND ', $sql) . ';';
if ($rs = C_DB::fetchRecordset($sql))
{
// you need to make sure that the query returns
// something to correctly display the ULs
if (empty($rs) === false)
{
$result .= '<ul>' . "\n";
while ($row = C_DB::fetchRow($rs))
{
$result .= '<li>' . $row['title'] . '</li>' . "\n";
$result .= getCats($row['lft'], $row['rght']);
}
$result .= '</ul>' . "\n";
}
}
return $result;
}
To get the HTML for your nested tree you should do:
echo getCats();
Please note that your nested set sample doesn't look right, also you should make sure if I didn't made any mistake invoking your C_DB class, I don't know since I'm not familiarized with it.
-
This does not work. In the given state, it recurses endlessly, as the recursion is always called with the initial parameters. When changing the '<=' / '>=' comparisons to '<' / '>', it works a little better, but produces duplicate output for each 'backtracking' in the tree, as the while loop does not recognize children that got processed in the recursion and prints them again.Henrik Opel– Henrik Opel2009年08月26日 22:28:44 +00:00Commented Aug 26, 2009 at 22:28
Simply loop thru the result will do:
$sql = "SELECT node.name, (COUNT(parent.name) - 1) AS depth
FROM nested_category AS node,
nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.name
ORDER BY node.lft";
$query_result = mysql_query($sql)
$result = "<ul>";
$currDepth = 0;
while($row = mysql_fetch_assoc($query_result))
{
if($row['depth'] > $currDepth)
{
$result .= "<li><ul>"; // open sub tree if level up
}
if($row['depth'] < $currDepth)
{
$result .= str_repeat("</ul></li>", $currDepth - $row['depth']); // close sub tree if level down
}
$result .= "<li>$row['name']</li>"; // Always add node
$currDepth = $row['depth'];
}
$result .= "</ul>";
echo $result;
$linaje='';
$lastnode='';
$sides['izq']=array();
$sides['der']=array();
$print = '<ul>';
foreach ($array as $key1 => $value1){ //Proyectos
if(strpos($info[$key1]['linaje'],'-') !== false)
$compare = strstr($info[$key1]['linaje'],'-',true);
else
$compare = $info[$key1]['linaje'];
if($linaje != ''){
if ($linaje != $compare){
$linaje= $compare;
$sides['izq']=array();
$sides['der']=array();
//for($i=1;$i <= substr_count($lastnode,'`')-substr_count($value1,'`');$i++)
//$print .= '</ul></li>';
}
}
if ($lastnode != '')
for($i=1;$i<= substr_count($lastnode,'`')-substr_count($value1,'`');$i++)
$print .= '</ul></li>';
if (count($sides['der'])>0)
if ($sides['der'][count($sides['der'])-1] > $info[$key1]['der'])
$print .= '<ul>';
$print .= '<li><a href="#'.$info[$key1]['id'].'#'.$info[$key1]['linaje'].'">'.substr($value1,substr_count($value1,'`')).'</a>';
if ($info[$key1]['der'] - $info[$key1]['izq'] == 1)
$print .= '</li>';
if ($key1 == count($info)-1)
for($i=1;$i <= substr_count($lastnode,'`')-1;$i++)
$print .= '</ul></li>';
$sides['der'][] = $info[$key1]['der'];
$sides['izq'][] = $info[$key1]['izq'];
if ($linaje =='')
$linaje = $info[$key1]['linaje'];
$lastnode = $value1;
}
$print .= '</ul>';
echo $print;
the difference in this is that you can render X numbers of trees, this applies to one of my projects. and I use a char as a depth reference when I fetch the rows from the DB
i`m using CROSS JOIN query displaying jsTree jQuery menu; Everything works just great ! The existing table I added a column for the position. However, when I define position and ordered all by position, the corresponding items are not grouped properly. I guess it's a query issue, tried many combinations, but no success.
links
andrechts
, orlft
andright
? And does your code reflect what you actually have?