My function looks like that. It works but does lots of work (recursively calls itself and does lots of db queries). There must be another way to do same thing but with array (with one query). I can't figure out how to modify this function to get it work with array.
function genMenu($parent, $level, $menu, $utype) {
global $db;
$stmt=$db->prepare("select id, name FROM navigation WHERE parent = ? AND menu=? AND user_type=?") or die($db->error);
$stmt->bind_param("iii", $parent, $menu, $utype) or die($stmt->error);
$stmt->execute() or die($stmt->error);
$stmt->store_result();
/* bind variables to prepared statement */
$stmt->bind_result($id, $name) or die($stmt->error);
if ($level > 0 && $stmt->num_rows > 0) {
echo "\n<ul>\n";
}
while ($stmt->fetch()) {
echo "<li>";
echo '' . $name . '';
//display this level's children
genMenu($id, $level+1, $menu, $utype);
echo "</li>\n\n";
}
if ($level > 0 && $stmt->num_rows > 0) {
echo "</ul>\n";
}
$stmt->close();
}
You can build a tree-based array fairly easily, so it'd be one single query and then a bunch of PHP logic to do the array building:
$tree = array();
$sql = "SELECT id, parent, name FROM menu WHERE parent ... etc.... ";
$results = mysql_query($sql) or die(mysql_error());
while(list($id, $parent, $name) = mysql_fetch_assoc($results)) {
$tree[$id] = array('name' => $name, 'children' => array(), 'parent' => $parent);
if (!array_key_exists($tree[$parent]['children'][$id])) {
$tree[$parent]['children'][$id] = $id;
}
}
For this, I'm assuming that your tree has a top-level '0' node. if not, then you'll have to adjust things a bit.
This'd give you a double-linked tree structure. Each node in the tree has a list of its children in the ['children'] sub-array, and each node in the tree also points to its parent via the ['parent'] attribute.
Given a certain starting node, you can traverse back up the tree like this:
$cur_node = 57; // random number
$path = array();
do {
$parent = $tree[$cur_node]['parent'];
$path[] = $parent;
$cur_node = $parent;
} while ($parent != 0);
I think the first thing you can fix is removing the WHERE parent = ? clause and then work on the resulting query result, this will make you work a bit more in managing the result but will definitely safe you IO operations.
Using parts of Marc B Solution
$tree = array();
$sql = "select id, parent, name FROM navigation AND menu=? AND user_type=?";
$results = mysql_query($sql) or die(mysql_error());
while(list($id, $parent, $name) = mysql_fetch_assoc($results)) {
$tree[$id] = array('name' => $name, 'children' => array(), 'parent' => $parent);
if (!array_key_exists($tree[$parent]['children'][$id])) {
$tree[$parent]['children'][$id] = $id;
}
}
print_r($tree);
Replace the ? with the actual values, and give that a run, what is your output?
Maybe not what you wanted, but it is great when it comes to trees.
You would have to rebuild your table and had some code to output the html, but you would have only one query. It could be worth the effort on the long run.
ie.If you have this menu
# Menu hierarchy:
- Home
- Product
|- Tv
|- Radio
- About us
It would looks like this in the db.
+----+----------+-----------+-----+-----+
| id | menu | parent_id | lft | rgt |
+----+----------+-----------+-----+-----+
| 1 | Home | null | 1 | 2 |
+----+----------+-----------+-----+-----+
| 2 | Product | null | 3 | 8 |
+----+----------+-----------+-----+-----+
| 3 | Tv | 2 | 4 | 5 |
+----+----------+-----------+-----+-----+
| 4 | Radio | 2 | 6 | 7 |
+----+----------+-----------+-----+-----+
| 5 | About us | null | 9 | 10 |
+----+----------+-----------+-----+-----+
The data could be fetch using a similar query
$select = "SELECT * FROM table_name WHERE lft BETWEEN 3 AND 8;"
To output a specific menu:
- Product
|- Tv
|- Radio
I know its not exactly the answer your were looking for, but FYI, there are other ways to use hierarchical tree data.
Good luck.
I wrote in the past an ugly way but with simple SELECT:
I store in text/varchar field strings like this:
/001
/001/001
/001/002
/002
/002/001
/002/001/001
Ignore the hebrew and look in window.aMessages array, to look how it works:
http://www.inn.co.il/Forum/Forum.aspx/t394009#4715854
Related
I have a table comments, thats look like this, added some mockup content as well:
+------------+---------+----------+-------------------+------------------------------------+---------------------------+
| comment_id | user_id | movie_id | comment_parent_id | comment_content | comment_creation_datetime |
+------------+---------+----------+-------------------+------------------------------------+---------------------------+
| 26 | 1 | 16329 | 0 | Första | 2016-01-24 10:42:49 |
| 27 | 1 | 16329 | 26 | Svar till första | 2016-01-24 10:42:55 |
| 28 | 1 | 16329 | 26 | Andra svar till förta | 2016-01-24 10:43:06 |
| 29 | 1 | 16329 | 28 | Svar till "andra svar till första" | 2016-01-24 10:43:23 |
+------------+---------+----------+-------------------+------------------------------------+---------------------------+
Im trying to display the comments Reddit style, like this image:
Im trying to fetch all comments SELECT * FROM comments WHERE movie_id = :movie_id ORDER BY comment_creation_datetime DESC and then recursively echo them out.
I have tried a bunch of foreachloops, but none is working as expected
foreach($this->comments as $value){ ?>
<div class="comment">
Comment content <?php echo $value->comment_content; ?>
<?php if($value->comment_parent_id > 0){
foreach($value as $sub_comment){ ?>
<div class="comment">
comment comment on comment: <?php echo $value->comment_content; ?>
</div>
<?php }} ?>
</div>
<?php }
My question:
How do I echo out the comments in a nested Reddit style with foreach loop?
You need to both make a list of root comments, and hierarchically organize all of them. You can do both in one go:
$roots = [];
$all = [];
foreach($comments as $comment)
{
// Make sure all have a list of children
$comment->comments = [];
// Store all by ID in associative array
$all[$comment->comment_id] = $comment;
// Store the root comments in the roots array, and the others in their parent
if(empty($comment->comment_parent_id))
$roots[] = $comment;
else
$all[$comment->comment_parent_id]->comments[] = $comment;
}
// Check it's all done correctly!
print_r($roots);
You presorted the list by date, that's preserved in this approach. Also, as you only reorganized by reference this is lightning fast, and ready to be used in templating engines or anything - no need to print out inline like the other answers.
Working with the adjacency list model can be more problematic with SQL. You need to retrieves all the rows with a single query and store a reference of any parent's child in a lookup table.
$sth = $pdo->prepare("SELECT * FROM comments WHERE movie_id = ? ORDER BY comment_creation_datetime DESC");
$sth->execute([$movie_id]);
$comments = $sth->fetchAll(PDO::FETCH_ASSOC);
$lookup_table = [];
foreach ($comments as $comment_key => $comment) {
$lookup_table[$comment['comment_parent_id']][$comment_key] = $comment['comment_id'];
}
Now you can display them with
function recursive_child_display($comments, $lookup_table, $root = 0, $deep = 0)
{
if (isset($lookup_table[$root])) {
foreach ($lookup_table[$root] as $comment_key => $comment_id) {
// You can use $deep to test if you're in a comment of a comment
echo '<div class="comment">';
echo 'Comment content ', $comments[$comment_key]['comment_content'];
recursive_child_display($comments, $lookup_table, $comment_id, $deep+1);
echo '</div>';
}
}
}
Example:
// display all the comments from the root
recursive_child_display($comments, $lookup_table, 0);
// display all comments that are parent of comment_id 26
recursive_child_display($comments, $lookup_table, 26);
I would use some recursive function, you start with the ones with parent_id == 0 and recursively print all those who are their direct children.
This code is not tested, but you can get the idea:
function printComment($comment, $comments)
{
foreach($comments as $c)
{
if($c->parent_id == $comment->comment_id)
{
$output .= "<li>".printCommment($c)."</li>";
}
}
$output = "<ul>".$comment->comment_content."</ul>".$output;
return $output;
}
foreach($this->comments as $comment)
{
if($comment->parent_id == 0)
{
echo printComment($comment,$this->comments);
}
}
I'm having some issues getting a tree menu to work from bottom-up.
I already have a script to work from top-down, which works fine.
This is a very simplified version of my table:
+-----+-----------+--------------------+
| uid | parent_id | page_address |
+-----+-----------+--------------------+
| 1 | 0 | index.php |
| 2 | 0 | login.php |
| 3 | 2 | dashboard.php |
| 4 | 3 | bookings.php |
| 5 | 3 | documents.php |
| 6 | 4 | changebookings.php |
| 7 | 4 | activities.php |
+-----+-----------+--------------------+
The page_address field is unique.
I can work out what page the user is currently on, for example changebookings.php
I would then like a menu to look like this:
login.php
dashboard.php
bookings.php
changebookings.php
activities.php
documents.php
However, the closest I've got so far is the following tree:
login.php
bookings.php
changebookings.php
As you can see, my script currently only returns the actual parent, and not a list of links currently in the parent.
For those interested, the script I use in total is at the bottom of this post.
Is there any easier way to get the bottom-up tree as required?
Many thanks
Phil
EDIT:
I've finally got the code to work, for future users who stumble upon this post, I have added the functionality below:
$dataRows = $databaseQuery->fetchAll(); // Get all the tree menu records
$dataRows = $result->fetchAll(PDO::FETCH_ASSOC);
foreach($dataRows as $row)
{
if($row['link_address']==substr($_SERVER['PHP_SELF'], 1, strlen($_SERVER['PHP_SELF'])-1))
{
$startingId = $row['parent_id'];
}
}
$menuTree = $this->constructChildTree($dataRows, $startingId);
private function constructChildTree(array $rows, $parentId, $nesting = 0)
{
$menu = array();
if(!in_array($nesting, $this->nestingData))
{
$this->nestingData[] = $nesting;
}
foreach($rows as $row)
{
if($row['parent_id']==$parentId && $parentId!=0)
{
$menu[] = $row['link_address'];
$newParentId = $this->getNextParent($rows, $row['parent_id']);
$parentChildren = $this->constructChildTree($rows, $newParentId, ($nesting+1));
if(count($parentChildren)>0)
{
foreach($parentChildren as $menuItem)
{
$menu[] = 'NESTING' . $nesting . '::' . $menuItem;
}
}
}
}
return $menu;
}
private function getNextParent($rows, $parentId)
{
foreach($rows as $row)
{
if($row['uid']==$parentId)
{
return $row['parent_id'];
}
}
}
Without reading your code you should be doing:
1) Get current page, look at parent ID.
2) Load all with that parent ID.
3) Get next Parent ID using current Parent ID as ID.
4) If new parent ID != 0, goto step 2 passing in the new Parent ID.
Sounds like you just need to edit your script to include ALL pages with the given ID as their parent ID.
<?PHP
$sql = "SELECT * FROM TABLE WHERE table parent_id=0";
$result = mysql_query($sql);
while($perant_menu = mysql_fetch_array($result))
{
echo display_child($perant_menu["uid"],$perant_menu["page_address"]);
}
// Recursive function
function display_child($parent_id,$name)
{
$sql= "SELECT * FROM table where parent_id = $parent_id";
$result = mysql_query($sql);
if(mysql_num_rows($result)>0)
{
while($menu = mysql_fetch_array($result))
{
echo display_child($menu["id"],$menu["page_address"]);
}
}
else
{
echo $name;
}
}
?>
I have an table structure like this
mysql> SELECT id, name, parent_id FROM categories;
+-------+------------+-----------+
| id | name | parent_id |
+-------+------------+-----------+
| 15790 | Test | 0 |
| 15791 | Test2 | 0 |
| 16079 | Subtest | 15790 |
| 16080 | Subtest 2 | 15790 |
| 16081 | Subsubtest | 16079 |
+-------+------------+-----------+
Now I want to look up the parent for every children and sibling and give it back in the right order for deletion.
So my output in this case would be:
Array
(
16081,
16080,
16079,
15791,
15790
)
I can't delete just by reversing the parent ids, because this should be solid walking back the tree.
Also I am not able/allowed to change the structure of the table. So building kind of an index is necessary.
Assuming you don't have access to TRUNCATE, SET (so you could do SET FOREIGN_KEY_CHECKS=0;), ALTER, etc. etc., and absolutely must use a script:
Since the question is tagged with php, this should do the trick:
function reversetree($src_arr, $currentid = 0)
{
$cats = array();
foreach($src_arr as $id => $parent)
{
if($parent == $currentid)
{
$cats[] = $id;
$cats = array_merge($cats, reversetree($src_arr, $id));
}
}
return !$currentid ? array_reverse($cats) : $cats;
}
$rs = array();
foreach($pdo->query('SELECT id, parent_id FROM categories') as $row)
$rs[$row['id']] = $row['parent_id'];
$stmt = $pdo->prepare('DELETE FROM categories WHERE id = ?');
$pdo->beginTransaction();
foreach(reversetree($rs) as $v)
$stmt->execute(array($v));
$pdo->commit();
I don't understand why you need the IDs in a particular order. You can delete them with a transaction and they will all be deleted simultaneously.
DELETE FROM categories WHERE ID IN (15790,15791,16079,16080,16081);
You could add FOREIGN KEY constraint with CASCADE on DELETE.
The foreign key will point to the same table on the parent id field.
When you delete the parent, all the children (no matter what level) are removed automatically.
<?php
// housekeeping
$pdo = new PDO($dsn, $user, $password);
$select = $pdo->prepare(
"SELECT parent.id AS parent_id, child.id AS child_id
FROM categories AS parent
JOIN categories AS child ON parent.id = child.parent_id
WHERE parent.id = ?"
);
$delete = $pdo->prepare('DELETE FROM categories WHERE id = ?');
// deletes $node_id, deletes its children first if required
function delete_node($node_id){
$select->execute( array($node_id) );
$children = $select->fetchAll(PDO::FETCH_NUM);
if (count($children) !== 0) { // if 0, then the category does not exist, or it has no child
foreach ($children as $child) { // call delete_node() recursively on each child
delete_node ($child[1]);
}
}
$delete->execute( array($node_id) ); // then delete this node (or do nothing if $node_id does not exist)
}
// to delete one category and all its sub-categories
delete_node(15790);
// to delete all contents
$allTopLevel = $pdo->exec('SELECT id FROM categories WHERE parent_id = 0')->fetchAll(PDO::FETCH_NUM);
foreach ($allTopLevel as $node) {
delete_node($node[0]);
}
Not tested, not even sure if it "compiles", but you get the idea. Make sure to lock the table (or start a transaction) before calling delete_node().
Sorry, i can't help much, because SQL is not my thing. But perhaps someone could transfer java pseudo code to the solution
delete(table.getFirstRow().get(id));
delete(id_a){
for(data_set : table)
if(data_set.get(parent_id) == id_a)delete(data_set.get(id));
}
table.remove(id_a);
}
edit: no iteration about elements? So something like this?
delete(list){
if(list.size() == 0)return;
idList = list.getAll(id);
plist = table.getAllWhichEquals(parent_id, idList);
delete(plist);
table.remove(idList);
}
ah, forget it, i'm deleting not all at the same time, was just a try ^^
I have created a recursive function to get parent products of a product. I'm almost sure I'm nearly there, but need help snapping out of recursion.
The result I am looking for is something like:
Product 1 (id:1, parent:none)
Product 2 (id:2, parent:1)
--- --- Product 3 (id:3, parent:2)
--- --- Product 4 (id:4, parent:2)
--- Product 5 (id:5, parent:1)
--- --- Product 6 (id:6, parent:5)
--- --- Product 7 (id:7, parent:5)
My updated function is as follows:
function get_parents ($pid, $found = array()) {
array_push ($found, $pid);
$sql = "SELECT * FROM products WHERE child_id = '$pid'";
$result = mysql_query($sql) or die ($sql);
if(mysql_num_rows($result)){
while($row = mysql_fetch_assoc($result)){
$found[] = get_parents($row['pid'], $found);
}
}
return $found;
}
I call it using a simple:
$parents = get_parents($pid);
The problem I am having is that when I run it, it creates an infinite loop, which doesn't break.
I don't want to get done for spamming so I have saved the result of my array to a text file, which can be seen here http://vasa.co/array.txt
Any help would be seriously appreciated :-)
Hmm.. judging by your structure of your DB, it would seem that something is amiss unless I'm missing something
The statement
$sql = "SELECT * FROM products WHERE child_id = '$pid'";
Tells me that for each product, you are storing the ID of the child. Typically, in a tree based structure, it's the reverse, you store the parent ID not the child - unless you want a child node to have many parents. If that is the case, then the function could easily run into problems. Consider the following:
| ID | Child_ID |
+----+----------+
| 1 | 2 |
| 2 | 1 |
This would cause an infinite loop. If you store the parent_id, then by that nature, you are encoding the graph to be hierarchical. Since every product has A parent, then the logic can be written recursively.
The could then be written as such?
function get_parents ($pid, $found = array()) {
array_push ($found, $pid);
$sql = "SELECT * FROM products WHERE id = '$pid'";
$result = mysql_query($sql) or die ($sql);
if(mysql_num_rows($result)){
while($row = mysql_fetch_assoc($result)){
$found[] = get_parents($row['parent_id'], $found);
}
}
return $found;
}
I have been developing my own forum for about a week now and I am almost done with all of the code, however, I am stuck on one single issue that I have not been able to figure out.
Well, simply said I have sub forums that can be within any amount of other sub forums.
How would I create a path dynamically to any of those sub forums on the spot with PHP.
After the path is created I would use it within href's and other things.
I am guessing I would somehow need to traverse the database based on a ID column and another column that would link one sub forum to another sub forum.
Let's assume that my database table looks like this:
ID | Name | Link |
---+-------------+-------
1 | Forum-One | Top |
2 | Forum-Two | 1 |
3 | Forum-Three | 2 |
4 | Forum-Four | 2 |
5 | Forum-Five | 3 |
6 | Forum-Six | 3 |
How would I go about doing this - or is there something else that must be done instead?
I hope I was clear enough for everyone to understand.
EDIT:
include("inc/config.php");
function generateBreadcrumb($startingID){
$result = mysql_query("SELECT * FROM temp_table WHERE ID='$startingID'");
while($row = mysql_fetch_array($result))
{
$db_id=$row['ID'];
$db_name=$row['Name'];
}
if($db_id!='Top'){
return generateBreadCrumb($db_id);
} else {
return $db_name;
}
}
$startID='6';
echo generateBreadcrumb($startID);
First you need a terminating condition. So set your top level Forum[link] to null, or 'top', or something. Then its simply a matter of using a recursive function put your bread-crumb together.
So lets assume you wanted to go to display the breadcrumb to Forum-One:Forum-Three:Forum-Six , better known as Forum-Six.
Example code:
<?php
$yourForumId = 6; // replace this dynamically with your forum;
$breadcrumb = generateBreadcrumb($yourForum);
function generateBreadcrumb($startingForumId){
$sql= "SELECT Name ,link FROM Forums WHERE ID = ".$startingForumId;
//run your $sql however you do to get results
//assuming you get associative arrays back
if($res['link'] != 'top'){
return generateBreadCrumb($res['link']).":".$res['Name'];
} else {
return $res['Name'];
}
}
echo $breadcrumb;
?>
It's recursion, which if you're new to it may seem complicated, but I hope that helps!
EDIT: here's your code with the needed edit...
include("inc/config.php");
function generateBreadcrumb($startingID){
$result = mysql_query("SELECT * FROM temp_table WHERE ID='$startingID'");
$row = mysql_fetch_array($result);
$db_id=$row['link'];
$db_name=$row['Name'];
if($db_id!='Top'){
return generateBreadCrumb($db_id).":".$db_name;
} else {
return $db_name;
}
}
$startID='6';
echo generateBreadcrumb($startID);