recursive function for dynamic multilevel menu php - php

I've a mysql table structure like this:
CREATE TABLE IF NOT EXISTS menu(
id INT(5) NOT NULL AUTO_INCREMENT PRIMARY KEY,
p_id INT(5), -- parent id
sort_id INT(5) NOT NULL, -- for position
title VARCHAR(50) NOT NULL,
etc ...);
data structure would be something like this:
id | p_id | sort_id | title
1 | NULL | 1 | root_1
2 | 1 | 1 | sub1 of root_1
3 | 1 | 2 | sub2 of root_1
4 | NULL | 2 | root_2
5 | 2 | 1 | sub1 of root_2
6 | 2 | 2 | sub2 of root_2
7 | 3 | 1 | sub1 of `sub2 of root_1`
I've created a php script to show up one level sub menu, but I can't make up my mind how to get other levels. I think a recursive function is needed, for example, to get sub1 of sub2 of root_1 element in this task.
If anyone has any idea how to start creating a recursive function in this situation , please advise me, thanks :)

It might be best to first turn this into a tree type structure:
Menu Top
|
Nodes with NULL p_id
|
Children
You could do this by creating a MenuNode class that has an array of children. You don't have to do it that way, but it will make it much easier to create a recursive function to output the menu.

function generate_menu_list($parent_id)
{
$result = mysql_query("SELECT * FROM menu WHERE p_id ='$parent_id' order by sort_id ");
if (result)
{
while ($row = mysql_fetch_array($result))
{
$count = mysql_query("SELECT count(0) as cnt FROM menu_master WHERE parent_id='".$row['id']."'");
$countrow = mysql_fetch_array($count);
echo '<li><span class="fa fa-user"></span>'.$row['title '].' ';
if($countrow['cnt']>0)
{
echo '<ul>';
$this->generate_menu_list($row['id']);
echo '</ul>';
}
echo '</li>';
}
}
}

Related

Count descendants in hierarchical query

I was told that PostgreSQL is a better choice than MySQL for displaying hierarchical data, so I installed PostgreSQL and I'm ready to go.
This is the schema from my title (copied from pgAdmin):
CREATE TABLE public.gz_life_mammals (
id integer NOT NULL,
taxon text NOT NULL,
parent text NOT NULL,
parent_id smallint NOT NULL,
slug text,
name_common text,
plural text,
extinct smallint NOT NULL,
rank smallint NOT NULL,
key smallint NOT NULL,
CONSTRAINT "Primary Key" PRIMARY KEY (id)
);
This is my database connection and first query:
$dbh = pg_connect("host=localhost dbname=geozoo user=postgres");
if (!$dbh) {
die("Error in connection: " . pg_last_error());
}
$sql = "SELECT * FROM gz_life_mammals";
$result = pg_query($dbh, $sql);
while ($row = pg_fetch_array($result)) {
echo "ID: " . $row[0] . " | ";
echo "Taxon: " . $row[1] . " | ";
echo "ParentID: " . $row[3] . "<br>";
}
// free memory
pg_free_result($result);
// close connection
pg_close($dbh);
The most important table fields for this exercise are the first four (id, taxon, parent and parent_id. The data looks like this:
ID | TAXON | PARENT | PARENT_ID
1 | Mammalia | Chordata | 1
2 | Carnivora | Mammalia | 2
3 | Canidae | Carnivora | 3
4 | Canis | Canidae | 4
5 | Canis-lupus | Canis | 5
6 | Canis-latrans | Canis | 5
Where the last two rows represent the wolf (Canis lupus) and coyote (Canis latrans). Eventually, I'd like to be able to display the names of children, grandchildren, parents, great grandparents, etc. But right now I'm just trying to display the number of descendants. For example, if I navigated to MySite/life/mammalia, I might see the following display:
Orders: 19
Families: 58
Genera: 688
Species: 8,034
If I navigated to MySite/life/canidae, it might display something like this:
Genera: 6
Species: 37
Can anyone show me the best way to write that kind of query and display the results (with PHP)?
Given the table:
select * from gz_life_mammals;
id | taxon | parent | parent_id
----+---------------+-----------+-----------
1 | Mammalia | Chordata | 1
2 | Carnivora | Mammalia | 2
3 | Canidae | Carnivora | 3
4 | Canis | Canidae | 4
5 | Canis-lupus | Canis | 5
6 | Canis-latrans | Canis | 5
(6 rows)
and the function to translate parent_id into taxonomic rank name:
create function tax_rank(id integer) returns text as $$
select case id
when 1 then 'Classes'
when 2 then 'Orders'
when 3 then 'Families'
when 4 then 'Genera'
when 5 then 'Species'
end;
$$ language sql;
you can query number of descendants with the following recursive query:
with recursive hier(taxon,parent_id) as (
select m.taxon, null::integer
from gz_life_mammals m
where taxon='Mammalia' --<< substitute me
union all
select m.taxon, m.parent_id
from hier, gz_life_mammals m
where m.parent=hier.taxon
)
select tax_rank(parent_id),
count(*) num_of_desc
from hier
where parent_id is not null
group by parent_id
order by parent_id;
tax_rank | num_of_desc
----------+-------------
Orders | 1
Families | 1
Genera | 1
Species | 2
(4 rows)
The interesting part is inside with recursive. The first part of the query selects the root row(s) of hierarchy. The second part (after union all) is called recursively and each time adds direct descendants to the previous result set. Read this to understand how it works in details.
After hierarchy is constructed, it can be represented as you like. In the above example only number of descendants are shown. You can get names as well:
with recursive hier(taxon,parent_id) as (
...
)
select tax_rank(parent_id),
taxon as name
from hier
where parent_id is not null
order by parent_id;
tax_rank | name
----------+---------------
Orders | Carnivora
Families | Canidae
Genera | Canis
Species | Canis-lupus
Species | Canis-latrans
(5 rows)
The same on one line:
with recursive hier(taxon,parent_id) as (
...
)
select tax_rank(parent_id),
string_agg(taxon,', ') as names
from hier
where parent_id is not null
group by parent_id
order by parent_id;
tax_rank | names
----------+----------------------------
Orders | Carnivora
Families | Canidae
Genera | Canis
Species | Canis-lupus, Canis-latrans
(4 rows)
And so on...

Retrieving Adjacency-List-Model data excluding an array of defined nodes

Data input format is:
+-----------+------------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-----------+------------------+------+-----+---------+-------+
| parent_id | int(10) unsigned | NO | | NULL | |
| child_id | int(10) unsigned | NO | | NULL | |
+-----------+------------------+------+-----+---------+-------+
This data defines relations that are used to draw a tree graph, using a DOT script. The DOT is sophisticated enough to handle all the recursion given a link between every node, e.g.
1 -> 2;
1 -> 3;
2 -> 4;
4 -> 5;
4 -> 6;
6 -> 7;
Will generate:
I need to exclude branches defined by a blacklist array, e.g. if the blacklist array is [4] the DOT script would need to become:
1 -> 2;
1 -> 3;
2 -> 4;
If your DBMS supports recursive queries you could trim the tree by a recursive query (which could be put into a view or even a function) This example works for Postgres, but could be adapted to MS or Oracle.
DROP SCHEMA tmp CASCADE;
CREATE SCHEMA tmp ;
SET search_path=tmp;
CREATE TABLE family
( parent_id INTEGER NOT NULL
, child_id INTEGER NOT NULL
, PRIMARY KEY (parent_id,child_id)
);
INSERT INTO family(parent_id,child_id) VALUES
(1 , 2) ,(1 , 3)
,(2 , 4)
,(4 , 5) ,(4 , 6)
,(6 , 7)
;
CREATE TABLE black_sheep (id INTEGER NOT NULL PRIMARY KEY);
INSERT INTO black_sheep(id) VALUES(4);
-- INSERT INTO black_sheep(id) VALUES(6);
WITH RECURSIVE tree AS (
SELECT parent_id AS opa
, parent_id as dad
, child_id AS kid
FROM family f0
WHERE NOT EXISTS (
SELECT *
FROM family nx
WHERE nx.child_id = f0.parent_id
)
UNION ALL
SELECT tr.opa AS opa
, f1.parent_id AS dad
, f1.child_id AS kid
FROM family f1
JOIN tree tr ON tr.kid = f1.parent_id
WHERE NOT EXISTS (
SELECT *
FROM black_sheep nx
WHERE nx. id = f1.parent_id
)
)
SELECT * FROM tree;
Result:
opa | dad | kid
-----+-----+-----
1 | 1 | 2
1 | 1 | 3
1 | 2 | 4
(3 rows)
This is the most efficient approach that I managed to come up with.
$relations = $db->query("SELECT `parent_id`, `child_id` FROM `relations` ORDER BY `parent_id` ASC LIMIT 500;")->fetchAll(PDO::FETCH_ASSOC);
function filterALMData ($data, array $exclude) {
while (count($exclude)) {
$new_exclude = [];
foreach ($data as $i => $node) {
if (in_array($node['parent_id'], $exclude)) {
$new_exclude[] = $node['child_id'];
unset($data[$i]);
}
}
$exclude = $new_exclude;
}
return $data;
};
$data = filterALMData($relations, [4]);
I am posting this to show what I have tried so far. It is not a favourable answer.

multilevel menu item deletion recursion function

I want to delete category from multilevel menu with recursion function. To understand situation more carefully , lets take a look first at mysql table structure
CREATE TABLE IF NOT EXISTS menu(
id INT(5) NOT NULL AUTO_INCREMENT PRIMARY KEY,
p_id INT(5),
sort_id INT(5) NOT NULL,
title VARCHAR(50) CHARSET utf8 COLLATE 'utf8_unicode_ci' NOT NULL,
);
it looks like this when it is retrieved
id | p_id | sort_id | title |
1 | 0 | 1 | root1 |
2 | 1 | 1 | sub of root1
3 | 0 | 2 | root2 |
4 | 2 | 1 | sub of "sub of root1"
... | ... | ... | ....
etc ...
I've written php script for delete category, here it is =>
function del_cat($connection,$id){
if (!$connection->connect_errno){
if ($connection->set_charset("utf8")){
if ($r = $connection->query("SELECT id FROM menu WHERE p_id=" . $id . "")){
if ($r->num_rows>0){
while ($row = $r->fetch_assoc()){
del_cat($connection,$row['id']);
}
} else {
$connection->query("DELETE FROM menu WHERE id=" . $id . "");
}
$r->free();
}
}
}
}
$connection variable is a just mysql connection object, and $id is id in table.
It works just fine when I'm deleting one row ( I mean when category doesn't have a child , sub categories), for example 1 | 0 | 1 | root1 |, but when I want to delete for example 4 | 2 | 1 | sub of "sub of root1" it doesn't delete category with sub categories . Any idea how to solve this problem ? thanks
The delete is only in else so you never actually delete the parent category. I think you need something like:
del_cat($connection, $row['id']);
$connection->query("DELETE ...");
} else {
$connection->query("DELETE ...");
By the way you should escape the ID input.

MySQL recursive tree search

I have a database with a tree of names that can go down a total of 9 levels deep and I need to be able to search down a signal branch of the tree from any point on the branch.
Database:
+----------------------+
| id | name | parent |
+----------------------+
| 1 | tom | 0 |
| 2 | bob | 0 |
| 3 | fred | 1 |
| 4 | tim | 2 |
| 5 | leo | 4 |
| 6 | sam | 4 |
| 7 | joe | 6 |
| 8 | jay | 3 |
| 9 | jim | 5 |
+----------------------+
Tree:
tom
fred
jay
bob
tim
sam
joe
leo
jim
For example:
If I search "j" from the user "bob" I should get only "joe" and "jim". If I search "j" form "leo" I should only get "jim".
I can't think of any easy way do to this so any help is appreciated.
You should really consider using the Modified Preorder Tree Traversal which makes such queries much easier. Here's your table expressed with MPTT. I have left the parent field, as it makes some queries easier.
+----------------------+-----+------+
| id | name | parent | lft | rght |
+----------------------+-----+------+
| 1 | tom | 0 | 1 | 6 |
| 2 | bob | 0 | 7 | 18 |
| 3 | fred | 1 | 2 | 5 |
| 4 | tim | 2 | 8 | 17 |
| 5 | leo | 4 | 12 | 15 |
| 6 | sam | 4 | 9 | 16 |
| 7 | joe | 6 | 10 | 11 |
| 8 | jay | 3 | 3 | 4 |
| 9 | jim | 5 | 13 | 14 |
+----------------------+-----+------+
To search j from user bob you'd use the lft and rght values for bob:
SELECT * FROM table WHERE name LIKE 'j%' AND lft > 7 AND rght < 18
Implementing the logic to update lft and rght for adding, removing and reordering nodes can be a challenge (hint: use an existing library if you can) but querying will be a breeze.
There isn't a nice/easy way of doing this; databases don't support tree-style data structures well.
You will need to work on a level-by-level basis to prune results from child-to-parent, or create a view that gives all 9 generations from a given node, and match using an OR on the descendants.
Have you thought about using a recursive loop? i use a loop for a cms i built on top of codeigniter that allows me to start anywhere in the site tree and will then subsequently filter trhough all the children> grand children > great grand children etc. Plus it keeps the sql down to short rapid queries opposed to lots of complicated joins. It may need some modifying in your case but i think it could work.
/**
* build_site_tree
*
* #return void
* #author Mike Waites
**/
public function build_site_tree($parent_id)
{
return $this->find_children($parent_id);
}
/** end build_site_tree **/
// -----------------------------------------------------------------------
/**
* find_children
* Recursive loop to find parent=>child relationships
*
* #return array $children
* #author Mike Waites
**/
public function find_children($parent_id)
{
$this->benchmark->mark('find_children_start');
if(!class_exists('Account_model'))
$this->load->model('Account_model');
$children = $this->Account_model->get_children($parent_id);
/** Recursively Loop over the results to build the site tree **/
foreach($children as $key => $child)
{
$childs = $this->find_children($child['id']);
if (count($childs) > 0)
$children[$key]['children'] = $childs;
}
return $children;
$this->benchmark->mark('find_children_end');
}
/** end find_children **/
As you can see this is a pretty simplfied version and bear in mind this has been built into codeigniter so you will need to modyfy it to suite but basically we have a loop that calls itself adding to an array each time as it goes. This will allow you to get the whole tree, or even start from a point in the tree as long as you have the parent_id avaialble first!
Hope this helps
The new "recursive with" construct will do the job, but I don't know id MySQL supports it (yet).
with recursive bobs(id) as (
select id from t where name = 'bob'
union all
select t.id from t, bobs where t.parent_id = bobs.id
)
select t.name from t, bobs where t.id = bobs.id
and name like 'j%'
There is no single SQL query that will return the data in tree format - you need processing to traverse it in the right order.
One way is to query MySQL to return MPTT:
SELECT * FROM table ORDER BY parent asc;
root of the tree will be the first item of the table, its children will be next, etc., the tree being listed "breadth first" (in layers of increasing depth)
Then use PHP to process the data, turning it into an object that holds the data structure.
Alternatively, you could implement MySQL search functions that given a node, recursively search and return a table of all its descendants, or a table of all its ancestors. As these procedures tend to be slow (being recursive, returning too much data that is then filtered by other criteria), you want to only do this if you know you're not querying for that kind of data again and again, or if you know that the data set remains small (9 levels deep and how wide?)
You can do this with a stored procedure as follows:
Example calls
mysql> call names_hier(1, 'a');
+----+----------+--------+-------------+-------+
| id | emp_name | parent | parent_name | depth |
+----+----------+--------+-------------+-------+
| 2 | ali | 1 | f00 | 1 |
| 8 | anna | 6 | keira | 4 |
+----+----------+--------+-------------+-------+
2 rows in set (0.00 sec)
mysql> call names_hier(3, 'k');
+----+----------+--------+-------------+-------+
| id | emp_name | parent | parent_name | depth |
+----+----------+--------+-------------+-------+
| 6 | keira | 5 | eva | 2 |
+----+----------+--------+-------------+-------+
1 row in set (0.00 sec)
$sqlCmd = sprintf("call names_hier(%d,'%s')", $id, $name); // dont forget to escape $name
$result = $db->query($sqlCmd);
Full script
drop table if exists names;
create table names
(
id smallint unsigned not null auto_increment primary key,
name varchar(255) not null,
parent smallint unsigned null,
key (parent)
)
engine = innodb;
insert into names (name, parent) values
('f00',null),
('ali',1),
('megan',1),
('jessica',3),
('eva',3),
('keira',5),
('mandy',6),
('anna',6);
drop procedure if exists names_hier;
delimiter #
create procedure names_hier
(
in p_id smallint unsigned,
in p_name varchar(255)
)
begin
declare v_done tinyint unsigned default(0);
declare v_dpth smallint unsigned default(0);
set p_name = trim(replace(p_name,'%',''));
create temporary table hier(
parent smallint unsigned,
id smallint unsigned,
depth smallint unsigned
)engine = memory;
insert into hier select parent, id, v_dpth from names where id = p_id;
/* http://dev.mysql.com/doc/refman/5.0/en/temporary-table-problems.html */
create temporary table tmp engine=memory select * from hier;
while not v_done do
if exists( select 1 from names n inner join tmp on n.parent = tmp.id and tmp.depth = v_dpth) then
insert into hier select n.parent, n.id, v_dpth + 1
from names n inner join tmp on n.parent = tmp.id and tmp.depth = v_dpth;
set v_dpth = v_dpth + 1;
truncate table tmp;
insert into tmp select * from hier where depth = v_dpth;
else
set v_done = 1;
end if;
end while;
select
n.id,
n.name as emp_name,
p.id as parent,
p.name as parent_name,
hier.depth
from
hier
inner join names n on hier.id = n.id
left outer join names p on hier.parent = p.id
where
n.name like concat(p_name, '%');
drop temporary table if exists hier;
drop temporary table if exists tmp;
end #
delimiter ;
-- call this sproc from your php
call names_hier(1, 'a');
call names_hier(3, 'k');

Contitionally structured and ordered MySQL query

I have a table in a MySQL Database.
It is structured as such:
CREATE TABLE `wall` (
`wall_id` int(10) NOT NULL auto_increment,
`user_id` int(10) NOT NULL,
`wall_content` varchar(1024) NOT NULL,
`time_posted` varchar(64) NOT NULL,
`is_reply` int(10) NOT NULL,
PRIMARY KEY (`wall_id`)
) ENGINE=MyISAM
The column 'is_reply' will be the id of 'wall_id' to which it is a reply of. How would I structure a query to get all the rows based on an inner join of another table to cross reference the user_id, and to group the wall posts with the comments below it whilst ordering the wall posts by 'time_posted'
My current query does that without grouping the comments. It is:
SELECT wall.*, user_wall.*, users.username, users.avatar_id
FROM `wall`
INNER JOIN user_wall ON user_wall.wall_id = wall.wall_id
INNER JOIN users ON users.user_id = wall.user_id
WHERE user_wall.user_id=15
I hope you can understand this.
Edit:
The table 'user_wall' is a table that stores what values are on the users wall, and the 'wall' table stores what is actually posted. The user_id in the 'wall' table is a reference to who posted that post.
The current query as stated above is fully functional and returns data as such:
wall_id | user_id | wall_content | time_posted | is_reply | user_id | wall_id | username | avatar_id
1 | 1 | *content* | *time* | 0 | 2 | 1 | User1 | 1
2 | 1 | *content2* | *time2* | 0 | 2 | 2 | User1 | 1
3 | 1 | *content3* | *time3* | 1 | 1 | 3 | User1 | 1
Whereas my question is, how do you structure the query so the result is like so:
wall_id | user_id | wall_content | time_posted | is_reply | user_id | wall_id | username | avatar_id
1 | 1 | *content* | *time* | 0 | 2 | 1 | User1 | 1
3 | 1 | *content3* | *time3* | 1 | 1 | 3 | User1 | 1
2 | 1 | *content2* | *time2* | 0 | 2 | 2 | User1 | 1
Where the row with 'wall_id' 3 which has and 'is_reply' of 1 to be beneath the row with 'wall_id'. Similarly a row with an 'is_reply' of 2 will be under the row with the row with a 'wall_id' of 2.
Now that you've edited it I understand what you mean. This should do it:
ORDER BY IF(wall.is_reply, wall.is_reply, wall.wall_id), wall.wall_id
Format: IF(EXPRESSION, IF_TRUE, IF_FALSE)
SQL can't return multiple rows from one table (e.g. the wall_comments) and only one from the ones it is joined with. In other words, that can't be done. There is an alternative that will get the same results but use two SQL queries and some PHP code.
Query #1:
SELECT wall_comments.*
FROM `wall_comments`
INNER JOIN user_wall ON wall_comments.wall_id = user_wall.wall_id
WHERE user_wall.user_id=15
Query #2:
SELECT wall.*, user_wall.*, users.username, users.avatar_id
FROM `wall`
INNER JOIN user_wall ON user_wall.wall_id = wall.wall_id
INNER JOIN users ON users.user_id = wall.user_id
WHERE user_wall.user_id=15
PHP:
<?php
$result1 = mysql_query($query1);
$result2 = mysql_query($query2);
$comments = array();
while($row = mysql_fetch_assoc($result1))
{
$comments[$row['wall_id']][] = $row;
}
$walls = array();
while($row = mysql_fetch_assoc($result2))
{
$walls[] = array_merge(
$row,
array(
'comments' => isset($comments[$row['wall_id']]) ? $comments[$row['wall_id']] : array(),
),
);
}
?>

Categories