First loop restarting in recursive function php - php

I'm trying to create a series of <ul> and <li> to create a directory/file structure to navigate some files created from a table in a dB. The table (tb_lib_manual) contains both files and folders.
If a record has a null entry for fileID then it is a folder not a file. each record has a parentID to show which folder is parent, for files and folders in the root this is 0.
The php code is thus:
class library_folders extends system_pageElement
{
private $html = '';
private $i = 0;
private $stmtArray = array();
private $objectArray = array();
function __construct()
{
parent::__construct();
$this->nextList();
}
function nextList($parentID = 0)
{
$qSQL = 'SELECT * FROM tb_lib_manual WHERE parentID=:parentID';
$stmtArray[$this->i] = $this->dbConnection->prepare($qSQL);
$stmtArray[$this->i]->bindValue(':parentID', $parentID, PDO::PARAM_INT);
$stmtArray[$this->i]->execute();
if($stmtArray[$this->i]->rowCount() > 0)
{
$display ='';
if($parentID != 0)
{
$display = ' style="display:none"';
}
$this->html .= '<ul' . $display . '>';
}
while ($this->objectArray[$this->i] = $stmtArray[$this->i]->fetchObject())
{
$this->html .= '<li>' . $this->objectArray[$this->i]->title;
if($this->objectArray[$this->i]->fileID == null)
{
//we have a folder!
$manualID = $this->objectArray[$this->i]->manualID;
$this->i ++;
$this->nextList($manualID);
$this->i--;
}
$this->html .= '</li>';
}
if($stmtArray[$this->i]->rowCount() > 0)
{
$this->html .= '</ul>';
}
echo $this->html;
}
function __destruct()
{
parent::__destruct();
}
}
The problem is when the code returns back to the while loop after calling itself it restarts the loop rather than carrying on where it left off, causing a repeat in the child folder. Is there either a better way to do this or am I doing something wrong!?
Table looks like this:
output like this:
'A ManualA ManualFolder 1Nasa exerciseA ManualA ManualFolder 1Nasa exercise'

Fixed the code, very silly mistake, echo in the wrong place...
class library_folders extends system_pageElement
{
private $html = '';
private $i = 0;
private $stmtArray = array();
private $objectArray = array();
function __construct()
{
parent::__construct();
$this->nextList();
echo $this->html;
}
function nextList($parentID = 0)
{
$qSQL = 'SELECT * FROM tb_lib_manual WHERE parentID=:parentID';
//echo $this->i;
$stmtArray[$this->i] = $this->dbConnection->prepare($qSQL);
$stmtArray[$this->i]->bindValue(':parentID', $parentID, PDO::PARAM_INT);
$stmtArray[$this->i]->execute();
if($stmtArray[$this->i]->rowCount() > 0)
{
$this->html .= '<ul>';
}
while ($this->objectArray[$this->i] = $stmtArray[$this->i]->fetchObject())
{
$this->html .= '<li>' . $this->objectArray[$this->i]->title;
if($this->objectArray[$this->i]->fileID == null)
{
//we have a folder!
$manualID = $this->objectArray[$this->i]->manualID;
$this->i ++;
$this->nextList($manualID);
$this->i--;
}
$this->html .= '</li>';
}
if($stmtArray[$this->i]->rowCount() > 0)
{
$this->html .= '</ul>';
}
}
function __destruct()
{
parent::__destruct();
}
}

Yes, there are better ways of doing this.
If you fetch the whole tree every time, you might as well load all the nodes into a big array (of objects), put each node's id as index and then loop over the array once to create the parent references by for example
// create a fake root node to start your traversal later on
// only do this if you don't have a real root node
// which I assume you don't
$root = (object) ["children" => []];
// loop over all nodes
foreach ($nodes as $node)
{
// if the node has a parent node that is not root
if ($node->parentId > 0)
{
// put it into it's parent's list of children
$nodes[ $node->parentId ]->children[] = $node;
}
else
{
// otherwise put it into root's list of children
$root->children[] = $node;
}
}
Complexity: You do one query and you have to iterate all your nodes once.
For this to work your nodes need to be objects. Otherwise each assignment to $node->children will create a copy of the assigned node where you wanted a reference.
If you do not want to fetch all nodes, you can go through your tree level by level by creating a list of node ids from the previous level.
function fetchLevel ($nodes, $previousLevelIds)
{
// get all children from the previous level's nodes
// I assume $previousLevelIds to be an array of integers. beware of sql injection
// I refrained from using prepared statements for simplicity
$stmt = $pdo->query("SELECT id, parentId FROM nodes WHERE parentId IN (".implode(",",$previousLevelIds).")");
// fetch nodes as instances of stdclass
$currentLevelNodes = $stmt->fetchAll(PDO::FETCH_OBJ);
$nextLevelIds = [];
foreach ($currentLevelNodes as $node)
{
// ids for next level
$nextLevelIds[] = $node->id;
// parent <-> child reference
$nodes[ $node->parentId ]->children[] = $node;
}
// fetch the next level only if we found any nodes in this level
// this will stop the recursion
if ($nextLevelIds)
fetchLevel($nodes, $nextLevelIds);
}
// start by using a fake root again
$root = (object) ["id" => 0, "children" => []];
$nodes = [0 => $root];
fetchLevel($nodes, [0]);
// or start with a specific node in the tree
$node = $pdo->query("SELECT id, parentId FROM nodes WHERE id = 1337")->fetch(PDO::FETCH_OBJ);
$nodes = [$node->id => $node];
fetchLevel($nodes, [$node->id]);
// or a number of nodes which don't even have to
// be on the same level, but you might fetch nodes multiple times
// if you it this way
Complexity: Number of queries <= height of your tree. You only iterate each fetched node once.
For displaying the tree as html list you iterate once more:
class Foo {
public $html;
public function getList ($nodes)
{
// outer most ul
$this->html = '<ul>';
$this->recurseList($nodes);
$this->html .= '</ul>';
}
protected function recurseList ($nodes)
{
foreach ($nodes as $node)
{
$this->html .= "<li><span>".$node->name."</span>";
if ($node->children)
{
if ($node->parentId > 0)
$this->html .= '<ul style="display:none">';
else
$this->html .= '<ul>';
$this->recurseList($node->children);
$this->html .= "</ul>";
}
$this->html .= "</li>";
}
}
}
Some unrelated remarks:
instead of a hard-coded style="display:none" you could just use a css rule like ul li ul {display:none} to hide all lists below root
I split fetching the data from the database and converting the data for displaying into two separate scripts, which is the standard when you develop using MVC. If you don't want to do this run the scripts in succession.
PHP itself is a template engine, but consider using another template engine to display your html like Smarty. I personally prefer smarty for it's simpler syntax.
stackoverflow answer on how to bind a PHP array to a MySQL IN-operator using prepared statements
if you need to fetch subtrees often consider using a special table structure to reduce the number of queries

Related

dinamic nested menu php

I have a table menu like this
i have problem to display like this with ul and li tag like 2nd picture.
please help me for the solution
menu_code|desc_code
1 | menu 1
1.1 | menu 1.1
1.2 | menu 1.2
1.2.1| menu 1.2.1
2 | menu 2
i want to display my table menu with concept "unlimited level menu".
I would consider changing your table structure. You will need to loop through each parent and children, you wouldnt want to do that with string splitting. I advice you to create an extra column parent_id and binding your items like that. After that its easy to crawl recursively through its children to create a ul > li structure.
Example recursive function:
public static function buildTree($items, $parent_id = null) {
$result = [];
foreach($items as $item) {
if($item->parent_id == $parent_id) {
$children = self::buildTree($items, $item->id);
if($children) {
$item->children = $children;
}
$result[$item->id] = $item;
}
}
return $result;
}
After that you can use the result of the build function to recursively create your menu structure:
public static function treeToHtml($tree, $level = 0) {
$result = '';
$result .= '<ul>';
foreach($tree as $item) {
$has_children = isset($item->children) && count($item->children) > 0;
if($has_children) {
$result .= '<li><a href="#">';
$result .= self::treeToHtml($item->children, $level + 1);
$result .= '</li>';
} else {
$result .= '<li><a href="#"></li>';
}
}
$result .= '</ul>';
return $result;
}
Edit function accordingly and statically all depends on the context you're using it in.

Parent-child navigation generation from tree array in PHP

Following function arranges the array totally wrong. Have you noticed any wrong piece of code in following function?
function buildHtmlList($array)
{
$maxlevel = 0;
foreach ($array as $key => $value)
{
$previousparent = isset($array[$key - 1]['parent']) ? $array[$key - 1]['parent'] : null;
$nextparent = isset($array[$key + 1]['parent']) ? $array[$key + 1]['parent'] : null;
if ($value['parent'] != $previousparent)
{
echo "\n<ul>";
++$maxlevel;
}
echo "\n<li>" . $value['name'];
if ($nextparent == $value['parent'])
echo "</li>";
}
for ($i = 0; $i < $maxlevel; ++$i)
{
echo "\n</li>\n</ul>";
}
}
It arranges the array totally wrong. Have you noticed any wrong piece of code in following function?
The wrong piece is the whole logic of the function. You treat the array as a flat list (as it is!), however, you'd like to display a tree.
As a flat list can't be displayed as a tree, you need to change the flat list to a tree first and then write a function that displays a tree.
An example how to convert a flat array to a tree/multidimensional one is available in a previous answer.
Try something like this (where $array is formatted like your example):
$corrected_array = array();
// This loop groups all of your entries by their parent
foreach( $array as $row)
{
$corrected_array[ $row['parent'] ][] = $row['name'];
}
// This loop outputs the children of each parent
foreach( $corrected_array as $parent => $children)
{
echo '<ul>';
foreach( $children as $child)
{
echo '<li>' . $child . '</li>';
}
echo '</ul>';
}
Demo

PHP Multidimensional array to unordered list, building up url path

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

how to change a while sql query loop into an array loop

i record number of queries of my website and in page the below script runs , 40 extra queries added to page .
how can I change this sql connection into a propper and light one
function tree_set($index)
{
//global $menu; Remove this.
$q=mysql_query("select id,name,parent from cats where parent='$index'");
if(mysql_num_rows($q) === 0)
{
return;
}
// User $tree instead of the $menu global as this way there shouldn't be any data duplication
$tree = $index > 0 ? '<ul>' : ''; // If we are on index 0 then we don't need the enclosing ul
while($arr=mysql_fetch_assoc($q))
{
$subFileCount=mysql_query("select id,name,parent from cats where parent='{$arr['id']}'");
if(mysql_num_rows($subFileCount) > 0)
{
$class = 'folder';
}
else
{
$class = 'file';
}
$tree .= '<li>';
$tree .= '<span class="'.$class.'">'.$arr['name'].'</span>';
$tree .=tree_set("".$arr['id']."");
$tree .= '</li>'."\n";
}
$tree .= $index > 0 ? '</ul>' : ''; // If we are on index 0 then we don't need the enclosing ul
return $tree;
}
//variable $menu must be defined before the function call
$menu = '....<ul id="browser" class="filetree">'."\n";
$menu .= tree_set(0);
$menu .= '</ul>';
echo $menu;
i heard , this can be done by changing it into an array , but i don't know how to do so
thanks in advance
Try this (untested code):
function tree_set($index)
{
//global $menu; Remove this.
$q=mysql_query("select id,name,parent from cats where parent='$index'");
if(mysql_num_rows($q) === 0)
return;
$cats = array();
$cat_ids = array();
while($arr=mysql_fetch_assoc($q))
{
$id = intval($arr['id']);
$cats[$id] = $arr;
}
$subFilesCountQuery="select parent,count(*) as subFileCount from cats where parent=".
join(" OR parent=",array_keys($cats))." GROUP BY parent";
$subFileCountResult=mysql_query($subFilesCountQuery);
while($arr=mysql_fetch_assoc($subFileCountResult))
{
$id = intval($arr['parent']);
$cats[$id]['subFileCount'] = $arr['subFileCount'];
}
// If we are on index 0 then we don't need the enclosing ul
$tree = $index > 0 ? '<ul>' : '';
foreach($cats as $id => $cat)
{
if($cat['subFileCount'] > 0)
$class = 'folder';
else
$class = 'file';
$tree .= '<li>';
$tree .= '<span class="'.$class.'">'.$arr['name'].'</span>';
$tree .=tree_set("".$arr['id']."");
$tree .= '</li>'."\n";
}
$tree .= $index > 0 ? '</ul>' : '';
What I'm doing is two queries: One to fetch all the categories (your original first query) followed by a second query to fetch all the subcategory counts in one fell swoop. I am also storing all categories in an array which you can loop through, rather than displaying as you fetch from the database.
It can be done by copying your data out into an array, and then using that copy: i.e.
while($arr=mysql_fetch_assoc($q))
{
$results[] = $arr;
}
later on, you then do whatever op you want on $results
The main problem with your code is you are mixing your display logic all in with your SQL query.
Select whole tree in single query, like "select id,name,parent from cats". Iterate on result, build array in PHP that will represent your tree, then draw HTML using array as source

how to loop over looped elements in Php?

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

Categories