PHP Tree Traversal For Sub Forums Within Sub Forums - php

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);

Related

PHP tree menu, bottom-up

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;
}
}
?>

Selecting column value dependent on parent ID without need for PHP loop?

I currently have a table that holds among other things:
ID | pagetitle | parent
1 | Page 1 | 0
2 | Page 2 | 1
3 | Page 3 | 2
I am trying to select all the pages, along with their parent's pagetitle.
Currently I am looping through the results in PHP, grabbing the pagetitles for those pages with parents, and it looks pretty ugly:
function showPageParent($pageid) {
do {
$result = mysql_query("SELECT parent FROM pages WHERE id=" . qt($pageid));
while($row = mysql_fetch_array($result)) {
$crumbs[] = $row['parent'];
$pageid1 = $row['parent'];
}
} while($pageid1!=0);
sort($crumbs);
foreach($crumbs as $crumb) {
$result = mysql_query("SELECT id,pagetitle FROM pages WHERE id=" . $crumb);
while($row = mysql_fetch_array($result)) {
$out .= $row['pagetitle'] . " > ";
}
}
if(count($crumbs) < 2) {
return showPageTitle($pageid);
} else {
return $out;
}
}
It works, but it is very old code, I am in the processes of re-writing a LOT of this sytem, and would love to pretty this thing up with one call to the database to return something like this:
ID | pagetitle | parent-pagetitle
Is this possible using inner selects?
edit
I'd like to point out, I am not the original author of the PHP, I am aware it's poorly written :p
select p.id
, p.title
, pp.id
, pp.title parentitle
from pages p
left
outer
join pages pp
on p.parent = pp.id
You can add another join for each level of depth, if needed.
In short, you're trying to manage hierarchical data in a non-hierarchical database. One of the better ways to model this appropriately is the nested set model, which is very nicely explained here. That article also shows you why hierarchical data in a simple adjacent list model (your current model) is virtually impossible to work with. Also look at materialized paths or nested interval tree models.

Mysql joining 1 row with multiple rows - group concat or php?

Consider the following example:
+----------+--------+-------------+----------+
| Person_id| Person | Language_id | Language |
+----------+--------+-------------+----------+
| 1 | Bob | 5 | English |
| 1 | Bob | 3 | Italiano |
| 1 | Bob | 8 | Deutsch |
+----------+--------+-------------+----------+
and the query is (not that important, just scripting to show you the table structure):
SELECT pl.Person_id, Person, Language_id, Language FROM people as p
LEFT JOIN people_languages as pl ON p.Person_id = pl.Person_id
LEFT JOIN languages as l ON pl.language_id = l.language_id
WHERE pl.Person = 1;
So basically, if the tables are constructed in this way, is it better to retrieve all results as shown above and then create a php function that creates a Person Model with languages_id and languages in an array, or using group_concat to retrieve a single row and then explode the languages and languages_id into an array?
By the way, no matter what I do, at the end I'd like to have a Person Model as the following:
class Person {
public $person_id; // 1
public $person; // Bob
public $language_id; // Array(5, 3, 8)
public $language; // Array(English, Italiano, Deutsch);
.
. // Functions
.
}
I think you should separate the queries into their separate model
There should be a Language model and will keep this simple
class Language
{
function getId() { return $id; }
function getDescription { return $description; }
}
class Person {
public $person_id; // 1
public $person; // Bob
public $languages; //this will store array of Language object
}
//From DataAccess
function getPerson($person_id)
{
$person = new Person();
//select only from Person table
//fill $person properties from records
//$person.person_id = $row['person_id']; etc
//select from people_languages joined to language where person_id=$person_id
$person->languages = getLanguagesByPerson($person->person_id); //returns array of languages
return $person;
}
You can now have
$person = getPerson(123);
$person->langauges[0]->getId(); //language id
$person->langauges[0]->getDescription(); //language id
$person->langauges[1]->getId(); //language id
$person->langauges[1]->getDescription(); //language id
Or loop through the languages
foreach($person->languages as $lang)
{
//use($lang->getId());
//use($lang->getDescription();
}
Here is the answer. You can use both ways but in my opinion it is much much better to use group concat. The reason is that this will increase performance as well as reduce the php code. If you go on with the example you gave you will have to do much coding on the php end. And sometimes it becomes difficult to handle on php end. I had this experience a couple of months ago. Instead using group concat will fetch you single row having everything you need for each person. On the php end simple extract the Group Concated cell and make another loop or put it in array. That is easy to handle.
Consider using a Dictionary
class Person {
public $person_id; // 1
public $person; // Bob
//I don't know php but for your idea
public Dictionary<int,string> languageList; // KeyValuePairs {(5,English),(3,Italiano),(8,Deutsch)}
.
. // Functions
.
}

PHP MySQL - An attempt at recursive functions

I'm trying out my first recursive function (at least I think I am!) and it only half works. First, the code:
function check_title($i,$title) {
$q=mysql_query("SELECT Title FROM posts WHERE Title = '$title'");
$num=mysql_num_rows($q);
if($num==0) {
return $title;
}else {
$title=$title.' ('.$i++.')';
check_title($i,$title);
}
}
What I'm doing is taking a string (title) and checking if that title exists in the db already. If it does, I want to append a number to the newer of the duplicates (e.g. 'I Am A Title' becomes 'I Am A Title-2'). I then need to run the function again to check this new version of my title, and increase the appended value as required ('I Am A Title-3'). Once no duplication is discovered, return the Title in its acceptable form.
It works when no duplication is found (the easy bit), but fails when duplication is found. Instead of appending a number, the entire title variable is emptied.
Any help would by greatly appreciated!
As Mchl stated, the empty title is due to a lack of return in the else branch.
However, there is a problem with the function as it does not do what you intend. Currently, your function is building $title as 'Title-1-2-3-4-etc' the way you currently append the number to the title and check again. Instead of passing a modified title on the recursed call you should just pass the base title. Then, for the query, modify the title.
function check_title($title, $i = 0) {
$qtitle = $title . ($i == 0 ? '' : "-$i");
$q=mysql_query("SELECT Title FROM posts WHERE Title = '$qtitle'");
$num=mysql_num_rows($q);
if($num==0) {
return $title . ($i == 0 ? '' : "-$i");
}else {
return check_title(++$i,$title);
}
}
PS, I also changed the order of parameters that way your initial call doesn't need to specify 0.
$title = check_title($title);
PPS, I should mention this is a solution to do it via recursion. However, a recursive solution is not the proper solution here as it needlessly makes return trips to the DB. Instead, you should use an sql query that selects all titles LIKE "$title%" Order by title asc. Then, iterate through each result and do a regex comparison with the title to see if it matches a pattern <title>|<title>-<#>. If it does you increment a duplicate counter. At the end you spit out the title with an appended counter value. I'll leave that solution as an exercise for the original poster.
Use a loop instead...
$record_exists = true;
$title_base = "I Am A Title";
$title = $title_base;
$i = 0;
while($record_exists) {
$q=mysql_query("SELECT Title FROM posts WHERE Title = '$title'");
$num=mysql_num_rows($q);
if($num==0) {
$record_exists = false;
// Exit the loop.
}
else {
$i++;
$title = $title_base . "-" . $i;
}
}
echo $title; // last existing title
However, optimally you'd do more work with a single SQL query and iterate the result, saving a lot of trips to and from the database.
And just for fun...
$title_base = "I Am A Title";
$title = $title_base;
for ($i=1, $num=1; $num != 0; $i++)
{
$q=mysql_query("SELECT Title FROM posts WHERE Title = '$title'");
$num=mysql_num_rows($q);
$title = $title_base . "-" . $i;
}
echo $title; // next title in sequence (doesn't yet exist in the db)
You lack a return in else branch.
Recursion is not the best idea for this application. Hint: do a query like this SELECT MAX(Title) FROM posts WHERE Title LIKE '$title%');
Your recursive function is fine except for 2 things:
The original title isn't maintained between recursive calls. Hence each time $title = $title . ' (' . $i++ . ')' runs, another parenthesis is appended to the title, like "abc", "abc (1)", "abc (1) (2)" and so on.
You are returning $title when no more matches are found but no title is returned in the ELSE. It is important to do so. When the execution reaches the IF, it returns the title but the returned title is not assigned anywhere and hence is lost.
Here is the revised code:
$orgTitle = 'I am a title';
function check_title($i, $title = '') {
global $orgTitle;
$q = mysql_query("SELECT Title FROM posts WHERE Title = '$title'");
$num = mysql_num_rows($q);
if ($num == 0) {
return $title;
} else {
$title = $orgTitle . ' (' . ++$i .')';
return check_title($i, $title);
}
}
echo check_title(0, $orgTitle);
Note the addition of new variable $orgTitle. I've replaced it in the assignment statement inside the ELSE. This does the fix for point 1 above.
Also note the return added before check_title call in the ELSE. This solves point 2.
Hope it makes sense!
Add-on: Recursions are confusing, logically complex and tricky to debug. Also, recursive calls consume more memory (not in case of simple operations like your example) because the compiler/interpreter had to maintain the state variables for all steps in a recursion.
In order to minimise MySql interactions I'd recommend something similar to the following.
function checkTitle($title)
{
/*Return all iterations of the title*/
$res = mysql_query("SELECT COUNT(title) AS titleCount FROM posts
WHERE SUBSTR(title, 1,". strlen($title) .") = '$title' ");
/*Return the incremented title*/
return $title. (mysql_result($res, 0, "titleCount") + 1);
}
Example:
mysql> select title from posts;
+----------+
| title |
+----------+
| firefox1 |
| firefox2 |
| shoe |
| firefox3 |
+----------+
4 rows in set (0.00 sec)
mysql> SELECT COUNT(title) AS titleCount FROM posts WHERE SUBSTR(title, 1,7) = 'firefox' ;
+------------+
| titleCount |
+------------+
| 3 |
+------------+
1 row in set (0.00 sec)
mysql>
---- Follow up test
Test table structure.
mysql>SHOW COLUMNS FROM posts;
+-------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| title | varchar(12) | YES | | NULL | |
+-------+-------------+------+-----+---------+-------+
/*Test code and output*/
function checkTitle($title)
{
/*Return all iterations of the title*/
$res = mysql_query("SELECT COUNT(title) AS titleCount FROM posts
WHERE SUBSTR(title, 1,". strlen($title) .") = '$title' ");
/*Return the incremented title*/
return $title. (mysql_result($res, 0, "titleCount") + 1);
}
mysql_connect("localhost","root", "password");
mysql_select_db("test");
echo checkTitle("firefox");
Output: firefox4

Using array instead of lots of db queries in PHP

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

Categories