I wrote a recursive function, which returns an array with the paths to all files/folders in a given path. An array is already sorted and returns the exact information i want, but i struggle to display it properly in html lists.
Array_of_paths = (
[0] => /path/to/folderA/
[1] => /path/to/folderA/subfolderAA/
[2] => /path/to/folderB/
[3] => /path/to/folderB/subfolderBB/
[4] => /path/to/folderB/subfolderBB/fileBB.txt
[5] => /path/to/folderB/fileB.txt
[6] => /path/to/folderC/
...
)
I want to put these paths in <ul>,<li> tags to see something like this:
<ul>
<li>/path/to/folderA/
<ul>
<li>/path/to/folderA/folderAA/</li>
</ul>
</li>
<li>/path/to/folderB
<ul>
<li>/path/to/folderB/subfolderBB/
<ul>
<li>/path/to/folderB/subfolderBB/fileBB.txt</li>
</ul>
</li>
<li>/path/to/folderB/fileB.txt</li>
</ul>
</li>
<li>/path/to/folderC/</li>
</ul>
=>
<ul>
<li>/path/to/folderA/
<ul>
<li>/path/to/folderA/folderAA/</li>
</ul>
</li>
<li>/path/to/folderB
<ul>
<li>/path/to/folderB/subfolderBB/
<ul>
<li>/path/to/folderB/subfolderBB/fileBB.txt</li>
</ul>
</li>
<li>/path/to/folderB/fileB.txt</li>
</ul>
</li>
<li>/path/to/folderC/</li>
</ul>
I managed to find a couple of similars questions, but the answers were in Ruby language. So, what's the problem solving idea behind this?
$lastD = 0;
foreach ($p as $e)
{
$depth = substr_count($e, '/');
//if this is a file, then add one to the depth count
if (substr($e,-1) != '/')
$depth++;
if ($depth > $lastD)
{
echo "<ul>";
$lastD = $depth;
}
if ($depth < $lastD)
{
echo "</ul>";
$lastD = $depth;
}
echo "<li>$e";
}
Returns:
/path/to/folderA//path/to/folderA/subfolderAA//path/to/folderB//path/to/folderB/subfolderBB//path/to/folderB/subfolderBB/fileBB.txt/path/to/folderB/fileB.txt/path/to/folderC/
If your are in PHP5, use RecursiveDirectoryIterator and RecursiveIteratorIterator to do the job.
$dir = new RecursiveDirectoryIterator("/path");
$it = new RecursiveIteratorIterator($dir);
foreach ($it as $key => $value) {
// Use $it->getDepth() and $value->getRealpath()
// with Byron's code to generate your list
}
I'm using this bit of code you published.
The structure of nested UL's is not quite right, so I just added a quick fix in order to have the closing ul tags so it can be used with more levels.
....
if ($depth < $lastD)
{
$closingULs=$lastD-$depth;
for($i=0;$i<$closingULs;$i++)
{
$uls.="</ul>";
}
echo $uls;
$lastD = $depth;
}
IMHO it's better to store the data in a more efficient and more similar format, something hierarchical. You can explode() your array by / and create the tree via arrays, then it'll be easy to foreach the array and build the HTML list.
foreach ( $paths as $path )
{
$pieces = explode('/', $path);
foreach ( $pieces as $piece )
{
$pathtree[$piece] = '';
}
}
This new $pathtree array is much smaller, probably 1/4th as small as your $paths array. From this point you just need to foreach it to build your HTML list tree.
Related
Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 7 years ago.
Improve this question
Lets say I have the array down here. I left out a few maps and files as this should be enough to make my point. There is no max depth to the array so there could be even more.
Array
(
[media] => Array
(
[documents] => Array
(
[0] => add.php
)
[music] => Array
(
[albums] => Array
(
[0] => add.php
)
)
[overview] => Array
(
[0] => overview.php
)
)
What I would like to get is something like the following:
<ul>
<li>Media
<ul>
<li>Documents
<ul>
<li>Add</li>
</ul>
</li>
<li>Music
<ul>
<li>Albums
<ul>
<li>Add</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</li>Overview
<ul>
<li>Overview</li>
</ul>
</li>
</ul>
I found php create navigation menu from multidimensional array dynamically but imo the accepted answer has quite a lot garbage and the result isn't quite of what I need. If you would like to know how the array is generated please let me know.
Thanks in advance for helping me
You need to use a recursive function that loops through your array. Something like this:
function outputMenu(array $array, $baseUrl = '/')
{
$html = '';
foreach ($array as $key => $item)
{
if (is_array($item))
{
$html .= '<li>'.$key.'<ul>';
$html .= outputMenu($item, $baseUrl.$key.'/');
$html .= '</ul></li>';
}
else
{
$html .= '<li>'.ucfirst(substr($item, 0, -4)).'</li>';
}
}
return $html;
}
echo outputMenu($array);
$array = array(
'media'=>array('documents'=>array('add.php'),
'music'=>array('albums'=>array('add.php'))),
'overview'=>array('overview.php')
);
print_link($array);
function print_link($arre){
foreach($arre as $key => $arr){
if(is_array($arr)){
echo '<li>'. $key .'<ul>';
print_link($arr);//echo '<li>'.$arr.'</li>';
echo '</ul><li>';
} else {
echo '<li>'.$arr.'</li>';
}
}
}
you will need a function for this for this task
I having some problems generating a full tree of my db(sitemap). I reached a level that i want to write better code. Thats why i choose a recursive function for this puzzle.
I want to generate a endless unordered list.
Like so:
<ul>
<li><a></a>
<ul>
<li><a></a>(subs)
etc etc etc.....
</ul>
</li>
</ul>
Like i already said i tried the following:
<?php
function traverseArray($array, $sub=false)
{
foreach($array as $cat)
{
if(isset($cat['childeren']) && is_array($cat['childeren']))
{
//a category with subs
echo('<ul id="'.$cat['parent_cat_id'].'" class="lv0">');
echo('<li id="'.$cat['parent_cat_id'].'"><a href=#>'.$cat['name'].'</a>'."\n");
traverseArray($cat['childeren'], true);
}else{
if($sub){
//a sub category of category
echo('</ul></li>');
echo('<li id="'.$cat['parent_cat_id'].'"><a href=#>'.$cat['name'].'</a></li>'."\n");
}else{
//category with no subs
echo('<ul id="'.$cat['parent_cat_id'].'" class="lv0">');
echo('<li id="'.$cat['parent_cat_id'].'"><a href=#>'.$cat['name'].'</a></li>'."\n");
echo('</ul>');
}
}
}
}
traverseArray($sitemap);
?>
but this is a puzzle i cant figure out properly,
this the result so far(messy):
<ul id="0" class="lv0">
<li id="0"><a href=#>Headsets</a>
</ul></li>
<li id="1"><a href=#>(USB) headsets ....</a></li>
</ul></li>
<li id="1"><a href=#>... headsets</a></li>
</ul></li>
<li id="1"><a href=#>.. USB headsets</a></li>
</ul></li>
<li id="1"><a href=#>Bluetooth headsets</a></li>
<ul id="0" class="lv0">
<li id="0"><a href=#>Unified Communications</a></li>
A big mess!
The $sitemap array looks like this:
Array
(
[0] => Array
(
[category_id] => 1
[parent_cat_id] => 0
..........etc
........etc
[type] => cat
[childeren] => Array
(
[0] => Array
(
[category_id] => 2
[parent_cat_id] => 1
.......etc
[type] => cat
[childeren] => Array
(
[0] => Array
(
[category_id] => 32
[parent_cat_id] => 16
.......etc
[type] => series
)
)
So childeren in childeren in childeren,
Is this the best way to do this/ on the right track?
Or are there beter ways out there?
Like sql tree's or something? or just again the old foreach in foreach ..(which i am trying to avoid this time).
Any help would be much appreciated!!!
Thanks in advance,
Jacob
"What is wrong with my code?" is not a question for stackoverflow.
Think about what 'traverseArray' function should do.
Your code is a mess.
Here is a cleaned example of array traversing.
Please fit it to your needs:
function traverseArray($array)
{
echo '<ul>';
foreach($array as $cat)
{
echo '<li>';
if(isset($cat['childeren']) && is_array($cat['childeren']))
{
echo '' . $cat['name'] . '';
traverseArray($cat['childeren']);
}
else
{
echo '' . $cat['name'] . '';
}
echo '</li>';
}
echo '</ul>';
}
traverseArray($sitemap);
Here is what I have so far:
My string:
$str = "<ul>
<li><a name="valuehere1" title="titlehere" href="/channel/london/">Link1</a></li>
<li><a name="valuehere2" title="titlehere" href="/channel/games/">Link1</a></li>
<li><a name="valuehere3" title="titlehere" href="/channel/sport/">Link1</a></li>
</ul>";
My PHP so far (and I am stuck):
$dom = new domDocument;
$dom->loadHTML($str);
$children = $dom->getElementsByTagName('li')->item(0)->childNodes->getAttribute('name');
$out = array();
foreach ($children as $child) {
$out[] = $dom->saveXML($child);
}
I am trying to extract the NAME attribute value of the A tag in the LI base on a match (in this example they are "london", "games", "sport") . WHen I pass "games" it should give me the output as "valuehere2". This has to be done at the server side due to some restrictions I have. Can someone help me with this please?
Thanks,
L
You've almost got it. But your code is fetching an attribute of the first li it finds, and tries to use that attribute value as an array to loop on. What you want is:
$children = $dom->getElementsByTagName('li');
$out = array();
foreach ($children as $child) {
if ($child->item(0)->childNodes->getAttribute('name')) {
$out[] = $dom->saveXML($child);
}
}
getElementsByTagName returns an DOMElementList (or whatever), which is an iterable array. Doing the getAttribute() stuff simply returns a string.
Regular expressions to the rescue?
[~]% cat test.php
<?php
$str = '<ul>
<li><a name="valuehere1" title="titlehere" href="/channel/london/">Link1</a></li>
<li><a name="valuehere2" title="titlehere" href="/channel/games/">Link1</a></li>
<li><a name="valuehere3" title="titlehere" href="/channel/sport/">Link1</a></li>
</ul>';
preg_match_all('/<li><a name="(.*)" title/', $str, $m);
print_r($m);
?>
[~]% php test.php
Array
(
[0] => Array
(
[0] => <li><a name="valuehere1" title
[1] => <li><a name="valuehere2" title
[2] => <li><a name="valuehere3" title
)
[1] => Array
(
[0] => valuehere1
[1] => valuehere2
[2] => valuehere3
)
)
I have a table in a database that contains a variety of paths to pages of my website. Each path is listed only one time. I currently have a very long and convoluted series of queries and PHP to pull all these and rewrite the data into an unordered list (to create a menu for my website). It seems that there is probably a relatively simple looping approach that would work MUCH more efficiently, but I cannot seem to get anything working. I have found TONS of PHP scripts that create UL lists from file trees, but all of them either don't work or can't handle the inherently not recursive nature of my query results (some require multi-dimensional arrays of my paths, which would be fine except for my trouble with creating those). I did find a script that works pretty close, but it formats the <ul> portion incorrectly by placing sub-lists outside of the <li> section (I will explain below)
Here is a sample:
DB returns the following in results array:
about/contact/
about/contact/form/
about/history/
about/staff/
about/staff/bobjones/
about/staff/sallymae/
products/
products/gifts/
products/widgets/
and I want to create the following output:
<ul>
<li>about/
<ul>
<li>about/contact/
<ul>
<li>about/contact/form/</li>
</ul>
</li>
<li>about/history/</li>
<li>about/staff/
<ul>
<li>about/staff/bobjones/</li>
<li>about/staff/sallymae/</li>
</ul>
</li>
</ul>
</li>
<li>products/
<ul>
<li>products/gifts/</li>
<li>products/widgets/</li>
</ul>
</li>
</ul>
So I got very close with a script found here: http://www.daniweb.com/forums/thread285916.html but I have run into a problem. It turns out that the script that I found creates improperly formatted UL lists. In a CORRECT situation, a sub-list is contained within the <li> of the parent element. In this scripting, the parent <li> is closed and then a <ul> block is inserted. The overall script is actually fairly elegant in the way that it keeps up with the levels and such, but I cannot wrap my head around it enough to figure out how to fix it. I have the whole thing in a function here:
function generateMainMenu()
{
global $db;
$MenuListOutput = '';
$PathsArray = array();
$sql = "SELECT PageUrlName FROM `table`";
$result = mysql_query($sql, $db) or die('MySQL error: ' . mysql_error());
while ($PageDataArray = mysql_fetch_array($result))
{
$PathsArray[] = rtrim($PageDataArray['PageUrlName'],"/"); //this function does not like paths to end in a slash, so remove trailing slash before saving to array
}
sort($PathsArray);// These need to be sorted.
$MenuListOutput .= '<ul id="nav">'."\n";//get things started off right
$directories=array ();
$topmark=0;
$submenu=0;
foreach ($PathsArray as $value) {
// break up each path into it's constituent directories
$limb=explode("/",$value);
for($i=0;$i<count($limb);$i++) {
if ($i+1==count($limb)){
// It's the 'Leaf' of the tree, so it needs a link
if ($topmark>$i){
// the previous path had more directories, therefore more Unordered Lists.
$MenuListOutput .= str_repeat("</ul>",$topmark-$i); // Close off the Unordered Lists
$MenuListOutput .= "\n";// For neatness
}
$MenuListOutput .= '<li>'.$limb[$i]."</li>\n";// Print the Leaf link
$topmark=$i;// Establish the number of directories in this path
}else{
// It's a directory
if($directories[$i]!=$limb[$i]){
// If the directory is the same as the previous path we are not interested.
if ($topmark>$i){// the previous path had more directories, therefore more Unordered Lists.
$MenuListOutput .= str_repeat("</ul>",$topmark-$i);// Close off the Unordered Lists
$MenuListOutput .= "\n";// For neatness
}
// (next line replaced to avoid duplicate listing of each parent)
//$MenuListOutput .= "<li>".$limb[$i]."</li>\n<ul>\n";
$MenuListOutput .= "<ul>\n";
$submenu++;// Increment the dropdown.
$directories[$i]=$limb[$i];// Mark it so that if the next path's directory in a similar position is the same, it won't be processed.
}
}
}
}
$MenuListOutput .= str_repeat("</ul>",$topmark+1);// Close off the Unordered Lists
return $MenuListOutput."\n\n\n";
}
and it returns something like this:
<ul id="nav">
<li>about</li>
<ul>
<li>history</li>
<li>job-opportunities</li>
<li>mission</li>
<li>privacy-policy</li>
</ul>
<li>giftcards</li>
<li>locations</li>
<ul>
<li>main-office</li>
<li>branch-office</li>
</ul>
<li>packages</li>
</ul>
Anyone have an idea of where I need to add in some additional logic and how I can accomplish this? Other ideas on a better way to do this? It seems like this is such a common issue that there would be a simple/standard method of handling something like this. Maybe if I could figure out how to create a multi-dimensional array from my paths then those could be iterated to make this work?
EDIT: More Complex :-(
I tried casablanca's response and it worked perfectly...except I then realized that now I have a follow-up to make things more difficult. In order to display the "name" of the page, I need to also have that info in the array, thus the path probably works better as the array key and the name in the value. Any thoughts on changing like this:
$paths = array(
"about/contact/ " => "Contact Us",
"about/contact/form/ " => "Contact Form",
"about/history/ " => "Our History",
"about/staff/ " => "Our Staff",
"about/staff/bobjones/ " => "Bob",
"about/staff/sallymae/ " => "Sally",
"products/ " => "All Products",
"products/gifts/ " => "Gift Ideas!",
"products/widgets/ " => "Widgets"
);
and then using something like this line within the buildUL function:
echo ''.$paths[$prefix.$key].'';
Edit:
Changed to cater for updated question.
I'm using an array index of __title to hold the page title. As long as you never have a directory in your tree called __title this should be fine. You're free to change this sentinel value to anything you wish however.
I have also changed it so the list building function returns a string, so that you can store the value for use later in your page. (You can of course just do echo build_list(build_tree($paths)) to output the list directly.
<?php
$paths = array(
'about/contact/' => 'Contact Us',
'about/contact/form/' => 'Contact Form',
'about/history/' => 'Our History',
'about/staff/' => 'Our Staff',
'about/staff/bobjones/' => 'Bob',
'about/staff/sallymae/' => 'Sally',
'products/' => 'All Products',
'products/gifts/' => 'Gift Ideas!',
'products/widgets/' => 'Widgets'
);
function build_tree($path_list) {
$path_tree = array();
foreach ($path_list as $path => $title) {
$list = explode('/', trim($path, '/'));
$last_dir = &$path_tree;
foreach ($list as $dir) {
$last_dir =& $last_dir[$dir];
}
$last_dir['__title'] = $title;
}
return $path_tree;
}
function build_list($tree, $prefix = '') {
$ul = '';
foreach ($tree as $key => $value) {
$li = '';
if (is_array($value)) {
if (array_key_exists('__title', $value)) {
$li .= "$prefix$key/ ${value['__title']}";
} else {
$li .= "$prefix$key/";
}
$li .= build_list($value, "$prefix$key/");
$ul .= strlen($li) ? "<li>$li</li>" : '';
}
}
return strlen($ul) ? "<ul>$ul</ul>" : '';
}
$tree = build_tree($paths);
$list = build_list($tree);
echo $list;
?>
Indeed a multi-dimensional would help here. You can build one by splitting each path into components and using those to index into the array. Assuming $paths is your initial array, the code below will build a multi-dimensional array $array with keys corresponding to the path components:
$array = array();
foreach ($paths as $path) {
$path = trim($path, '/');
$list = explode('/', $path);
$n = count($list);
$arrayRef = &$array; // start from the root
for ($i = 0; $i < $n; $i++) {
$key = $list[$i];
$arrayRef = &$arrayRef[$key]; // index into the next level
}
}
You can then iterate over this array using a recursive function, which you can use to naturally build a recursive UL list as in your example. In each recursive call, $array is a sub-array of the entire array this is being currently processed and $prefix is the path from the root to the current sub-array:
function buildUL($array, $prefix) {
echo "\n<ul>\n";
foreach ($array as $key => $value) {
echo "<li>";
echo "$prefix$key/";
// if the value is another array, recursively build the list
if (is_array($value))
buildUL($value, "$prefix$key/");
echo "</li>\n";
}
echo "</ul>\n";
}
The initial call would simply be buildUL($array, '').
I am rendering navigation from a 2 dimensional array, using lists, as shown:
<ul> <li>parent
<ul> <li>level 1</li> <li>level 1
<ul> <li>level 3</li> </ul>
</li> </ul>
<li> </ul>
</li> </ul>
Anyway to close the <li> and </ul> correctly I find I need some data from the next array in the foreach sequence. How can I retrieve this?
Although I've had choppy results with them in the past you can use the below functions to move the current array pointer.
prev() - http://www.php.net/manual/en/function.prev.php
next() - http://www.php.net/manual/en/function.next.php
end() - http://www.php.net/manual/en/function.end.php
reset() - http://www.php.net/manual/en/function.reset.php
Hopefully that'll help.
Here's a solution:
$arr = array(1,2,3,4,5);
foreach ($arr as $foo) {
if (empty($isrewind)) {
reset($arr);
$isrewind = true;
}
echo "node: $foo\n";
$copy = $arr; // make a copy of the array
$next = next($copy);
echo "next: $next\n";
$copy = $arr; // make a copy of the array
$prev = prev($copy);
echo "prev: $prev\n";
next($arr); // don't forget to advance the pointer on the original array
}
I've demonstrated the prev bit just for the example. You can easily do that without prev() by saving the element at the end of each iteration.
The if empty bit resets the array pointer to the beginning, because foreach will advance the pointer once when it makes a copy of the array.
The above example yields:
node: 1
next: 2
prev:
node: 2
next: 3
prev: 1
node: 3
next: 4
prev: 2
node: 4
next: 5
prev: 3
node: 5
next:
prev: 4
If you have to do something like this though, there might be a better way just by rearranging your data structure (hard to tell without code).
IHMO it's not possible with a simple foreach loop on an array that has non-numeric indexes. But you can extend the SPL class CachingIterator to wrap your array in this. In your extended class you can implement methods to return the previous and next index/value without advancing the internal element pointer.
If you have an array with numeric indexes use a for loop instead of a foreach. You can then use $i+1 and $i-1 to look at different array indexes than the current index.
Not sure if this is what he asking for, but here's my shot
$list = array (
'Home page' => null,
'Articles' => array (
'Diving' => null,
'Skydiving' => null,
'Stackoverflowing' => null,
),
'Nobody cares' => array (
'Hey' => null,
'It is just a test' => null,
),
);
function render_menu ($list)
{
echo "<ul>\n";
foreach ($y = new RecursiveArrayIterator ($list, RecursiveIteratorIterator::SELF_FIRST) as $key => $item)
{
if ($y->hasChildren ())
{
echo "<li>$key\n";
render_menu ($item);
echo "</li>\n";
}
else
{
echo "<li>$key</li>\n";
}
}
echo "</ul>\n";
}
render_menu ($list);
Found the answer staring me in the face. Retrieved the arrays I need like this:
foreach ($tree as $key => $link) {
$level = $link['level'];
$next_key = $key+1;
$prev_key = $key-1;
$next_array = $tree[$next_key];
$prev_array = $tree[$prev_key];
And then I could test the level property (indicating deph inside the navigation tree) with a buch of if statements to correctly insert the closing markup.
Messy code and I'm not proud of it, but it works!
Quite angry with myself for not seeing it before.
I assume you are using php. I dont think it's possible, you must know the index of element and index the array. Sth like this:
$i=0;
foreach ($array as $element){
echo $element;
//do sth with $array[$i+1];
//do sth with $array[$i-1];
$i++;
}
Have no other clue