Recently I've written recursive PHP function which generates website navigation based on parent-child structure like this
<ul>
<li>parent
<li>child</li>
</li>
</ul>
Code looks like that
function generateMenu($parent, $level, $db){
$q = $db->query("select id, name FROM menu WHERE parent = '$parent'");
if($level > 0 && $q->num_rows > 0) echo "\n<ul>\n";
while($row=$q->fetch_object()){
echo "<li>";
echo '' . $row->name . '';
//display this level's children
generateMenu($row->id, $level++, $menu, $db);
echo "</li>\n\n";
}
if($level > 0 && $q->num_rows > 0) echo "</ul>\n";
}
The piece of code above simply echoes <ul><li> structure (like given above example) from db table.
Now the questions is, how to create navigation menu like on this website?
Please take a look at left sidebar.
http://www.smithsdetection.com/continuous_vapour_sampling.php
Now i think that:
First of all we need to echo all parents
Function must get current pages id as an input value (for ex. $current)
Function must echo til' current pages level
I can't figure out how to modify my function, to get output like on given website. PLease help.
BTW
My db table looks like that
NOTE Please don't post answers about sql injection holes, I've already taken care about them: checking with in_array (if variable listed in column names array) and passing through real_escape.
Have a look at this: http://www.ferdychristant.com/blog/archive/DOMM-7QJPM7
You should try to fetch the whole hierarchy with one query to fix performance issue.
Assuming the current page id is in the var $current and that $db is an open MySQLi DB connection:
// first get your current page's path back to root:
// $stack will be a stack of menus to show
$stack = array();
// always expand current menu:
$stack[] = $current;
// now starting at $current, we go through the `menu` table adding each parent
// menu id to the $stack until we get to 0:
$i = $current;
while ( $i > 0 ) {
// get parent of $i
$query = sprintf('SELECT `parent` FROM `menu` WHERE id=%d LIMIT 1', $i);
$result = $db->query($query);
if (!$result) {
// do error handling here
}
$row = $result->fetch_assoc();
// save parent id into $i...
$i = $row['parent'];
// ...and push it onto $stack:
$stack[] = $i;
}
/**
* #param int $parent the parent ID of the menu to draw.
* #param array $stack the stack of ids that need to be expanded
* #param string $indent string for pretty-printing html
* #param MySQLi $db Open db connection
*/
function generateMenu($parent, $stack, $indent, $db){
// $next is the next menu id that needs expanding
$next = array_pop($stack);
$query = sprintf('SELECT `id`, `name` FROM `menu` WHERE `parent`=%d', $parent);
$result = $db->query($query);
if ( ! $result ) {
// do error handling here
}
if ($result->num_rows > 0) {
echo "\n$indent<ul>\n";
while($row = $result->fetch_object()){
echo "$indent <li>\n";
echo "$indent {$row->name}\n";
//display this level's children, if it's the $next menu to need to be drawn:
if ($row->id == $next)
generateMenu($next, $stack, "$indent ", $db);
echo "$indent </li>\n\n";
}
echo "$indent</ul>\n";
}
$result->free();
}
$first = array_pop($stack); // should always be 0
generateMenu($first, $stack, '', $db);
Related
I am in the process of making a quick PHP based forum, and each post in a forum will appear under its "parent" post, but slightly more indented.
To get all the posts in that order, I have the following function:
private function getData($pid, $offset)
{
$sql = 'SELECT id, subject, date
FROM post
WHERE forum_id = ? AND parent_id = ?';
$sth = $this->db->prepare($sql);
$sth->bind_param("ii", $this->id, $pid);
$sth->bind_result($id, $subject, $date);
$sth->execute();
$data = array();
while ( $sth->fetch() )
{
$row['id'] = $id;
$row['subject'] = $subject;
$row['date'] = $date;
$row['offset'] = $offset;
//Add this 'parent' post to the data array
$data[] = $row;
//Before moving on to next post, get all its children
$data[] = $this->getData($id, $offset+1);
}
$sth->close();
return $data;
}
This isn't working because I am executing another query before closing and fetching all the data from my current statement handler.
Is there a way to maybe separate the queries so they don't conflict with each other? Or any other way to by-pass this? Or will I simply have to restructure how I get my data?
Fetch all the rows into an array, then loop over them.
$rows = array();
while ( $sth->fetch() ) {
$row['id'] = $id;
$row['subject'] = $subject;
$row['date'] = $date;
$row['offset'] = $offset;
// the cool way is $rows[] = compact('id', 'subject', 'date', 'offset');
$rows[] = $row;
}
$sth->close();
foreach ($rows as $row) {
//Add this 'parent' post to the data array
$data[] = $row;
//Before moving on to next post, get all its children
$data[] = $this->getData($id, $row['offset'] + 1);
}
I will give you a example to show how to do this, this is the table i will use for the example (after just add the forum_id):
CREATE TABLE msgs (
id INT NOT NULL AUTO_INCREMENT,
date DATETIME,
name VARCHAR(100),
message TEXT,
parent_id INT NOT NULL DEFAULT 0
);
Then, with one query do:
$query = mysql_query("SELECT * FROM msgs ORDER BY id");
Some arrays to build the "posts tree", all parent_id = 0 will be root posts:
$all_messages = array(); // Will store all messages
$root_messages = array(); // Will store only the root (or more than one if you allow)
while($row=mysql_fetch_assoc($query)){ // Main loop
$all_messages[$row['id']] = array(
'inner_messages'=>array(),
'date'=> $row['date'],
'name'=> $row['name'],
'message'=>$row['message'],
'id'=>$row['id']
);
if($row['parent_id']=='0'){ // If is a root post
$root_messages[] = &$all_messages[$row['id']];
}else{ // If not a root post, places within parent message
$all_messages[$row['parent_id']]['inner_messages'][] = &$all_messages[$row['id']];
}
}
Now to print, use a recursion:
function writeTree($msgs){
foreach($msgs as $m){
echo '<div>';
echo '<h2>'.$m['name'].' ('.$m['date'].')</h2>';
echo '<div class="text">'.$m['message'].'</div>';
writeTree($m['inner_messages']);
echo '</div>';
}
}
writeTree($root_messages);
I am having a table like the following,need to display as Parent and child format
--------------------------------------------------------
id role_name role_id parent_id
--------------------------------------------------------
1 NSM 1 0
2 MR 5 2
3 ASM 4 3
4 ZSM 3 4
5 RSM 2 1
---------------------------------------------------------
the result is like to be the following
NSM
---RSM
-----ZSM
-----NSM
-----MR
NSM->ROOT
RSM->FIRST CHILD
ZSM->SECOND CHILD
NSM->THIRD CHILD
MR->LEAF
// Fetch all the roles
$result = mysql_query("select * from roles");
$roles = array();
while( $role = mysql_fetch_assoc($result) ) {
$roles[] = $role;
}
// Function that builds a tree
function build_tree($roles, $parent_id=0) {
$tree = array();
foreach ($roles as $role) {
if ($role['parent_id'] == $parent_id) {
$tree[] = array(
'role' => $role,
'children' => build_tree($roles, $role['parent_id'])
);
}
}
return $tree;
}
// Function that walks and outputs the tree
function print_tree($tree) {
if (count($tree) > 0) {
print("<ul>");
foreach($node in $tree) {
print("<li>");
htmlspecialchars($node['role']['role_name']);
print_tree($node['children']);
print("</li>");
}
print("</ul>");
}
}
SQL Results are always flat - you'll not be able to return a hierarchy view of that data in a query.
Instead, I would suggest using whichever client components you are using to show that (is it a tree? what exactly?) that knows how to go thru a flat list and build a hierarchy out of that.
If you want to print a view like that in a console (why would you ever want to do that?), you could do like this:
$data = array();
$query = mysql_query("SELECT * FROM table ORDER BY parent_id");
while($array = mysql_fetch_assoc($query))
{
$data[$array['parent_id']][] = $array;
}
function output_hierarchy($id, $prepend)
{
$current = $data[$id];
foreach($current as $item)
{
print $prepend . " " . $item['role_name'];
if(count($data[$item['id']]) > 0)
{
output_hierarchy($item['id'], $prepend . "--");
}
}
}
output_hierarchy(0, '');
If you want to use this on your website, you can easily adapt it. Code should be self-explanatory.
I have a multidimensional array in PHP produced by the great examples of icio and ftrotter (I am use ftrotterrs array in arrays variant):
Turn database result into array
I have made this into a unordered list width this method:
public function outputCategories($categories, $startingLevel = 0)
{
echo "<ul>\n";
foreach ($categories as $key => $category)
{
if (count($category['children']) > 0)
{
echo "<li>{$category['menu_nl']}\n";
$this->outputCategories($category['children'], $link
, $start, $startingLevel+1);
echo "</li>\n";
}
else
{
echo "<li>{$category['menu_nl']}</li>\n";
}
}
echo "</ul>\n";
}
So far so good.
Now I want to use the url_nl field to build up the url's used as links in the menu. The url has to reflect the dept of the link in de tree by adding up /url_nl for every step it go's down in the tree.
My goal:
- item 1 (has link: /item_1)
* subitem 1 (has link: /item_1/subitem_1)
* subitem 2 (has link: /item_1/subitem_1)
* subsubitem 1 (has link: /item_1/subitem_2/subsubitem_1)
- item 2 (has link: /item_2)
the table
id
id1 (parent id)
menu_nl
url_nl
title_nl
etc
What I have so far:
public function outputCategories($categories, $link, $start, $startingLevel = 0)
{
// if start not exists
if(!$start)
$start = $startingLevel;
echo "<ul>\n";
foreach ($categories as $key => $category)
{
$link.= "/".$category['url_nl'];
if($start != $startingLevel)
$link = strrchr($link, '/');
if (count($category['children']) > 0)
{
echo "<li>".$start." - ".$startingLevel.
"<a href='$link'>{$category['menu_nl']}</a> ($link)\n";
$this->outputCategories($category['children'], $link
, $start, $startingLevel+1);
echo "</li>\n";
}
else
{
$start = $startingLevel+1;
echo "<li>".$start." - ".$startingLevel.
"<a href='$link'>{$category['menu_nl']}</a> ($link)</li>\n";
}
}
echo "</ul>\n";
}
As you see in the example I have used a url_nl field which is recursively added so every level of the list has a link with a path which is used as a url.
Anyhow, I have problems with building up these links, as they are not properly reset while looping to the hierarchical list. After going down to the child in de list the first one is right but the second one not.
I'm stuck here...
It looks like you modify the $link variable inside the foreach loop, So you add item1 to $link, loop thru its subitems and return to the first iteration and add item2 to the variable...
replace this
$link .= "/".$category['url_nl'];
with
$insidelink = $link . "/".$category['url_nl'];
(and change remaining $link inside the loop to $insidelink)
Adding: This is also true for $startingLevel. Do not modify it, use +1 inline:
echo "<li>".$start." - ".$startingLevel +1.
"<a href='$link'>{$category['menu_nl']}</a> ($link)</li>\n";
Here is an easier way:
$inarray = your multi-dimensional array here. I used directory_map in codeigniter to get contents of directory including it's subdirectories.
$this->getList($filelist2, $filelist);
foreach ($filelist as $key => $val) {
echo $val;
}
function getList($inarray, &$filelist, $prefix='') {
foreach ($inarray as $inkey => $inval) {
if (is_array($inval)) {
$filelist = $this->getList($inval, $filelist, $inkey);
} else {
if ($prefix)
$filelist[] = $prefix . '--' . $inval;
else
$filelist[] = $inval;
}
}
return $filelist;
}
I'm creating a tree-structure of categories with parentid's which can be called from children in such a way:
ID | Name | ParentID
1 1 0
2 2 1
3 3 2
4 4 1
Resulting in this:
1 = 1
2 = 1 -> 2
3 = 1 -> 2 -> 3
4 = 1 -> 4
which means 3 is a child of 2, which is a child of 1.
when trying to get this idea (with the -> to show what relations are set) I only get to the second grade (1 -> 2) but not to the third (1->2->3) because of the looping function I use for it.
//put all ID's in an array
while ($row2 = $connector->fetchArray($result2)){
$id = $row2['ID'];
$parents[$id] = $row2['name'];
}
// show the tree-structure
while ($row = $connector->fetchArray($result)){
if($row['parentid']!=0)echo $parents[$row['parentid']].' -> ';
echo $row['name'].' - ';
echo '<br>';
}
I'd like two things to change:
have the code automatically generate a tree sized as necessary.
in the while-loops i have to select the $result twice (once as $result, once as $result2) to make it work. these $result's have exactly the same database-query:SELECT ID,name,parentid FROM categories
to fetch results from. I'd like to only declare this once.
Thanks for all the good answers. I've gone with the easiest, less-code-to-implement approach:
$result = $connector->query('SELECT ID,name,parentid FROM categories');
// Get an array containing the results.
$parents = array();
while ($row = $connector->fetchArray($result)){
$id = $row['ID'];
$parents[$id] = array('ID' => $row['ID'],'name' => $row['name'],'parentid' => $row['parentid']);
}
foreach ($parents as $id => $row){
$pid=$id;
$arrTmp= array();
do { // iterate through all parents until top is reached
$arrTmp[]=$pid;
$pid = $parents[$pid]['parentid'];
}while ($pid != 0);
$arrTmp = array_reverse($arrTmp);
foreach($arrTmp as $id){
echo $parents[$id]['name'].' -> ';
}
echo '<br>';
}
Rather than have PHP organize the items into a tree, why not ask the database to do it for you? I found this article on hierarchical data to be very good and the examples are almost identical to yours.
EDIT
The SQL for getting the full tree using the Adjacency Model is not ideal. As the article explains it requires rather a lot of joins for even a small hierarchy. Is it not possible for you to use the Nested Set approach? The SQL stays the same regardless of the size of the hierarchy and INSERT and DELETE shouldn't be very difficult either.
If you really want to do hierachies with parent ids(suitable only for small number of items/hierachies)
I modified your code a little bit(I did not test it so there may be some syntax errors):
//put all recordsets in an array to save second query
while ($row2 = $connector->fetchArray($result2)){
$id = $row2['ID'];
$parents[$id] = array('name' => $row2['name'],'parent' => $row2['parentid']);
}
// show the tree-structure
foreach ($parents as $id => $row){
$pid = $row['parentid'];
while ($pid != 0){ // iterate through all parents until top is reached
echo $parents[$pid]['name'].' -> ';
$pid = $parents[$pid]['parentid'];
}
echo $parents[$id]['name'].' - ';
echo '<br>';
}
To answer your comment:
$parents = array();
$parents[2] = array('ID'=>2,'name'=>'General','parentid'=>0);
$parents[3] = array('ID'=>3,'name'=>'Gadgets','parentid'=>2);
$parents[4] = array('ID'=>4,'name'=>'iPhone','parentid'=>3);
foreach ($parents as $id => $row){
$pid=$id;
$arrTmp= array();
do { // iterate through all parents until top is reached
$arrTmp[]=$pid;
$pid = $parents[$pid]['parentid'];
}while ($pid != 0);
$arrTmp = array_reverse($arrTmp);
foreach($arrTmp as $id){
echo $parents[$id]['name'].' -> ';
}
echo '<br>';
}
Prints out:
General ->
General -> Gadgets ->
General -> Gadgets -> iPhone ->
Maybe easier with OOP. Just sort the query by parentId
Note: The listChildren method and the printout at the bottom is just there to show it is listed correctly. I did not interpret the question that the display was important.
class Element {
public $id;
public $name;
public $parent = null;
public $children = array();
public function __construct($id, $name)
{
$this->id = $id;
$this->name = $name;
}
public function addChild($element)
{
$this->children[$element->id] = $element;
$element->setParent($this);
}
public function setParent($element)
{
$this->parent = $element;
}
public function hasChildren()
{
return !empty($this->children);
}
public function listChildren()
{
if (empty($this->children)) {
return null;
}
$out = array();
foreach ($this->children as $child) {
$data = $child->id . ':' . $child->name;
$subChildren = $child->listChildren();
if ($subChildren !== null) {
$data .= '[' . $subChildren . ']';
}
$out[] = $data;
}
return implode(',', $out);
}
}
$elements = array();
$noParents = array();
while ($row = $connector->fetchArray($result)) {
$elements[$row['id']] = $element = new Element($row['id'], $row['name']);
if (isset($elements[$row['parent']])) {
$elements[$row['parent']]->addChild($element);
} else {
$noParents[] = $element;
}
}
foreach ($noParents as $element) {
if ($element->hasChildren()) {
echo "Element {$element->id} has children {$element->listChildren()}.\n";
} else {
echo "Element {$element->id} has no children.\n";
}
}
If you are using PostgreSQL as the database, you can use the connectby() function to create the record set:
SELECT *
FROM connectby('tableName', 'id', 'parent_id')
AS t(keyid text, parent_keyid text, level int);
I love this function, and use all the time in my code. It can do some very powerful things, very quickly, and you don't have maintain the left/right values like the (adjacency model).
What's the best way to:
Get the data from the db using a single query
Loop through the results building e.g. a nested unordered list
My table has id, name and parent_id columns.
Here's an update to my last answer, with a counter that gives each ul a nesting 'level' class, and some comments.
Could anyone suggest how to adapt this to use table rows, without nesting, but with some kind of class numbering hierarchy for css/js hooks?
<?
//
// Get the data
//
include_once("inc/config.php");
$query = "SELECT c.*
FROM categories AS c
ORDER BY c.id
LIMIT 1000";
$result = pg_query($db, $query);
//
// Load all the results into the row array
//
while ($row = pg_fetch_array($result, NULL, PGSQL_ASSOC))
{
//
// Wrap the row array in a parent array, using the id as they key
// Load the row values into the new parent array
//
$categories[$row['id']] = array(
'id' => $row['id'],
'description' => $row['description'],
'parent_id' => $row['parent_id']
);
}
// print '<pre>';
// print_r($category_array);
// ----------------------------------------------------------------
//
// Create a function to generate a nested view of an array (looping through each array item)
// From: http://68kb.googlecode.com/svn-history/r172/trunk/upload/includes/application/controllers/admin/utility.php
//
function generate_tree_list($array, $parent = 0, $level = 0)
{
//
// Reset the flag each time the function is called
//
$has_children = false;
//
// Loop through each item of the list array
//
foreach($array as $key => $value)
{
//
// For the first run, get the first item with a parent_id of 0 (= root category)
// (or whatever id is passed to the function)
//
// For every subsequent run, look for items with a parent_id matching the current item's key (id)
// (eg. get all items with a parent_id of 2)
//
// This will return false (stop) when it find no more matching items/children
//
// If this array item's parent_id value is the same as that passed to the function
// eg. [parent_id] => 0 == $parent = 0 (true)
// eg. [parent_id] => 20 == $parent = 0 (false)
//
if ($value['parent_id'] == $parent)
{
//
// Only print the wrapper ('<ul>') if this is the first child (otherwise just print the item)
// Will be false each time the function is called again
//
if ($has_children === false)
{
//
// Switch the flag, start the list wrapper, increase the level count
//
$has_children = true;
echo '<ul class="level-' . $level . '">';
$level++;
}
//
// Print the list item
//
echo '<li>' . $value['description'] . '';
//
// Repeat function, using the current item's key (id) as the parent_id argument
// Gives us a nested list of subcategories
//
generate_tree_list($array, $key, $level);
//
// Close the item
//
echo '</li>';
}
}
//
// If we opened the wrapper above, close it.
//
if ($has_children === true) echo '</ul>';
}
// ----------------------------------------------------------------
//
// generate list
//
generate_tree_list($categories);
?>
function generate_list($array,$parent,$level)
{
foreach ($array as $value)
{
$has_children=false;
if ($value['parent_id']==$parent)
{
if ($has_children==false)
{
$has_children=true;
echo '<ul>';
}
echo '<li>'.$value['member_name'].' -- '.$value['id'].' -- '.$value['parent_id'];
generate_list($array,$value['id'],$level);
echo '</li>';
}
if ($has_children==true) echo '</ul>';
echo $value['parent_id'];
}
}
MySQL have created a good article on this subject: Managing Hierarchical Data in MySQL
You can create a breadcrumb view style by using arrays, without using a recursive function.
Here is my working code:
First, make a SQL query like this:
$category = CHtml::listData(TblCategory::model()->findAllCategory(array(
'distinct'=>true,
'join'=>'LEFT JOIN tbl_category b on b.id = t.cat_parent',
'join'=>'LEFT JOIN tbl_category c on c.cat_parent = 0',
'order' => 'cat_name')),'id','cat_name');
I am using yii related code so you can use normal join queries, then form an array in a foreach() function
public function findAllCategory($condition='',$params=array())
{
Yii::trace(get_class($this).'.findAll()','system.db.ar.CActiveRecord');
$criteria=$this->getCommandBuilder()->createCriteria($condition,$params);
$category = array();
$cat_before;
$parent_id = array();
$cat_before = $this->query($criteria,true);
//echo "<br><br><br><br><br><br><br>";
foreach($cat_before as $key => $val)
{
$category[$key] = $val;
$parent_id[$key]['cat_parent'] =$val['cat_parent'];
$parent_id[$key]['cat_name'] =$val['cat_name'];
foreach($parent_id as $key_1=> $val_1)
{
if($parent_id[$key]['cat_parent'] == $category[$key_1]['id'])
{
$category[$key]['cat_name']= $category[$key_1]['cat_name'] .' > '. $parent_id[$key]['cat_name'];
}
}
}
return $cat_before;
}
Then you can get result using Main cat >> subcat 1 >> subcat_1 inner >> ...