» Convert anything to Tree Structures in PHP
3 Tree Manipulation
- str_replace: replaceTree
- ksort: ksortTree
- » Convert anything to Tree Structures in PHP
PHP Recursive functions
In real life I do not consider myself to be a tree-hugger, but as a developer: tree data structures and recursive technology totally get me going ;) Seriously though, they are very powerful ways to solve problems and often lead to elegant & reusable solutions.
In this series I am going to share a couple of functions and ways of approaching recursive problems in PHP.
I recently faced a programming challenge that almost broke my brain. I needed to create a function that could explode any single-dimensional array into a full blown tree structure, based on the delimiters found in it's keys. Tricky part was size of the tree could be infinite. I called the function: explodeTree. And maybe it's best to first look at an example.
The Directory Example
Here I will give an example what the explodeTree function could be used for. Let's say we need a recursive directory listing of /etc/php5, and for that we execute:
if(exec("find /etc/php5", $files)){ // the $files array now holds the path as it's values, // but we also want the paths as keys: $key_files = array_combine(array_values($files), array_values($files)); // show the array print_r($key_files); }
Which will return something like:
Array ( [/etc/php5] => /etc/php5 [/etc/php5/cli] => /etc/php5/cli [/etc/php5/cli/conf.d] => /etc/php5/cli/conf.d [/etc/php5/cli/php.ini] => /etc/php5/cli/php.ini [/etc/php5/conf.d] => /etc/php5/conf.d [/etc/php5/conf.d/mysqli.ini] => /etc/php5/conf.d/mysqli.ini [/etc/php5/conf.d/curl.ini] => /etc/php5/conf.d/curl.ini [/etc/php5/conf.d/snmp.ini] => /etc/php5/conf.d/snmp.ini [/etc/php5/conf.d/gd.ini] => /etc/php5/conf.d/gd.ini [/etc/php5/apache2] => /etc/php5/apache2 [/etc/php5/apache2/conf.d] => /etc/php5/apache2/conf.d [/etc/php5/apache2/php.ini] => /etc/php5/apache2/php.ini )
Now if we want to transform this list into a tree structure with each directory as a nested node, a child of another directory, all we would have to do is run:
// let '/' be our delimiter $tree = explodeTree($key_files, "/"); // show the array print_r($tree);
And that single command would give the totally awesome:
Array ( [etc] => Array ( [php5] => Array ( [cli] => Array ( [conf.d] => /etc/php5/cli/conf.d [php.ini] => /etc/php5/cli/php.ini ) [conf.d] => Array ( [mysqli.ini] => /etc/php5/conf.d/mysqli.ini [curl.ini] => /etc/php5/conf.d/curl.ini [snmp.ini] => /etc/php5/conf.d/snmp.ini [gd.ini] => /etc/php5/conf.d/gd.ini ) [apache2] => Array ( [conf.d] => /etc/php5/apache2/conf.d [php.ini] => /etc/php5/apache2/php.ini ) ) ) )
Wow! So this would make it very easy to visually layout a tree structure of the directory /etc/php5. But remember this is just an example. The function now explodes on the '/' character, but you can use any delimiter to explode a single-dimensional array into a Tree. So how does this explodeTree function work?
The Function: explodeTree()
The key to my original approach was not to use static PHP code, but to generate PHP code, and to later execute it using the eval() function. Though it did the job and posed an fresh approach to the problem, I wouldn't be surprised if someone told me that the piece of code secretly spawned gateways to hell ;)
My blog visitors agreed on the evil part and came with other neat approaches. Now thanks to Lachlan Donald and Takkie, here's the new explodeTree() function.
<?php /** * Explode any single-dimensional array into a full blown tree structure, * based on the delimiters found in it's keys. * * @author Kevin van Zonneveld <kevin@vanzonneveld.net> * @author Lachlan Donald * @author Takkie * @copyright 2008 Kevin van Zonneveld (http://kevin.vanzonneveld.net) * @license http://www.opensource.org/licenses/bsd-license.php New BSD Licence * @version SVN: Release: $Id: explodeTree.inc.php 89 2008-09-05 20:52:48Z kevin $ * @link http://kevin.vanzonneveld.net/ * * @param array $array * @param string $delimiter * @param boolean $baseval * * @return array */ function explodeTree($array, $delimiter = '_', $baseval = false) { if(!is_array($array)) return false; $splitRE = '/' . preg_quote($delimiter, '/') . '/'; $returnArr = array(); foreach ($array as $key => $val) { // Get parent parts and the current leaf $parts = preg_split($splitRE, $key, -1, PREG_SPLIT_NO_EMPTY); $leafPart = array_pop($parts); // Build parent structure // Might be slow for really deep and large structures $parentArr = &$returnArr; foreach ($parts as $part) { if (!isset($parentArr[$part])) { $parentArr[$part] = array(); } elseif (!is_array($parentArr[$part])) { if ($baseval) { $parentArr[$part] = array('__base_val' => $parentArr[$part]); } else { $parentArr[$part] = array(); } } $parentArr = &$parentArr[$part]; } // Add the final part to the structure if (empty($parentArr[$leafPart])) { $parentArr[$leafPart] = $val; } elseif ($baseval && is_array($parentArr[$leafPart])) { $parentArr[$leafPart]['__base_val'] = $val; } } return $returnArr; } ?>
The first to arguments of explodeTree() are clear I guess. But what about that 3rd parameter: $baseval?
The baseval argument
In the first example you see that only leafs (the bottom nodes that don't have any children) maintain their original values (the filepaths in this case). If you want higher nodes (parents) to also maintain their values, you'll have to tell explodeTree to do so like this:
// now the 3rd argument, the baseval, is true $tree = explodeTree($key_files, "/", true);
And then explodeTree will preserve the node's original value in the __base_val items. Like this:
Array ( [etc] => Array ( [__base_val] => [php5] => Array ( [__base_val] => /etc/php5 [cli] => Array ( [__base_val] => /etc/php5/cli [conf.d] => /etc/php5/cli/conf.d [php.ini] => /etc/php5/cli/php.ini ) [conf.d] => Array ( [__base_val] => /etc/php5/conf.d [mysqli.ini] => /etc/php5/conf.d/mysqli.ini [curl.ini] => /etc/php5/conf.d/curl.ini [snmp.ini] => /etc/php5/conf.d/snmp.ini [gd.ini] => /etc/php5/conf.d/gd.ini ) [apache2] => Array ( [__base_val] => /etc/php5/apache2 [conf.d] => /etc/php5/apache2/conf.d [php.ini] => /etc/php5/apache2/php.ini ) ) ) )
See what happens? Baseval creates a placeholder. A semi-node for the original value of it's parent. The value: '/etc/php5' is now saved, without baseval this value would be lost because there was no place to store it in. Now that might come in handy!
So you've got a tree. Now what?
Trees with unlimited levels of nodes require recursive functions that can traverse the entire structure. Recursive functions are functions that call themselves every time they find more items to process. Here's one to layout the directories:
function plotTree($arr, $indent=0, $mother_run=true){ if($mother_run){ // the beginning of plotTree. We're at rootlevel echo "start\n"; } foreach($arr as $k=>$v){ // skip the baseval thingy. Not a real node. if($k == "__base_val") continue; // determine the real value of this node. $show_val = ( is_array($v) ? $v["__base_val"] : $v ); // show the indents echo str_repeat(" ", $indent); if($indent == 0){ // this is a root node. no parents echo "O "; } elseif(is_array($v)){ // this is a normal node. parents and children echo "+ "; } else{ // this is a leaf node. no children echo "- "; } // show the actual node echo $k . " (".$show_val.")"."\n"; if(is_array($v)){ // this is what makes it recursive, rerun for childs plotTree($v, ($indent+1), false); } } if($mother_run){ echo "end\n"; } }
And this would output:
start
O etc ()
+ php5 (/etc/php5)
+ cli (/etc/php5/cli)
- conf.d (/etc/php5/cli/conf.d)
- php.ini (/etc/php5/cli/php.ini)
+ conf.d (/etc/php5/conf.d)
- mysqli.ini (/etc/php5/conf.d/mysqli.ini)
- curl.ini (/etc/php5/conf.d/curl.ini)
- snmp.ini (/etc/php5/conf.d/snmp.ini)
- gd.ini (/etc/php5/conf.d/gd.ini)
+ apache2 (/etc/php5/apache2)
- conf.d (/etc/php5/apache2/conf.d)
- php.ini (/etc/php5/apache2/php.ini)
end
If I overlooked a standard PHP function that can already do this, or you have other improvements/ideas leave a comment!
Thanks again: Lachlan Donald & Tokkie for insightful comments and great effort.
Like this article?
|
Then Dzone it! Or use another bookmark button below to show your support & help me spread the word. |
Hot StuffFlaming articles» Survive heavy traffic with you... | RelatedArticles like this one» PHP Recursive str_replace: rep... |
tags: php, programming, recursion, hierarchy, tree data structure, array, delimiter
category: Programming - PHP - Tree Manipulation
read: 25,850 times






tagcloud
#21. EllisGL on 17 October 2008
#20. Kevin on 10 September 2008
#19. steve on 09 September 2008
I get this:
/var/www/web5/web/code/kvzlib/code/php/functions/explodeTree.inc.php
... [more] instead of the script. Any chance I can peek at this code? ;-)
#18. Robert on 06 May 2008
#17. Emacs on 06 April 2008
#16. http://www.sezgioto.com on 26 December 2007
#15. Takkie on 12 October 2007
#14. Kevin on 12 October 2007
@ Takkie: If you have site you want listed here just say so.
#13. Lachlan on 12 October 2007
Although not quite as succinct, here is a version that works with any array order:
function expand($array,$delim='/')
... [more] {
$newArray = array();
$delim = preg_quote($delim,'/');
foreach($array as $key=>$value)
{
$current = &$newArray;
$stack = preg_split("/$delim/",$key,-1,PREG_SPLIT_NO_EMPTY);
// iterate down the array, leaving $current as the leaf
foreach($stack as $item)
{
// clobber leafs, replace this for baseval support
if(isset($current[$item]) && !is_array($current[$item]))
{
$current[$item] = array();
}
$current =& $current[$item];
}
// don't overwrite existing branches
if(!is_array($current) || count($current) == 0)
{
$current = $value;
}
}
return $newArray;
}
I haven't implemented baseval support, but it would be fairly straightforward, see the comments in the inner for loop.
- Lachlan Donald http://www.lachlandonald.com
#12. Kevin on 10 October 2007
But that's some awesome code you wrote. I've updated the article and replaced the original function with yours.
Thanks a lot you guys, you've really contributed powerful stuff.
#11. Kevin on 10 October 2007
#10. Takkie on 10 October 2007
Also, your code makes use of undefined variables and indices. Just for fun: add shuffle($files); right after if(exec("find /etc/php5", $files)){ and run the code with a configuration that shows notices.
... [more] Another possible solution could be (independend of ordering):
function explodeTree($array, $delimiter = '_', $baseval = false)
{
$splitRE = '/' . preg_quote($delimiter, '/') . '/';
$returnArr = array();
foreach ($array as $key => $val) {
// Get parent parts and the current leaf
$parts = preg_split($splitRE, $key, -1, PREG_SPLIT_NO_EMPTY);
$leafPart = array_pop($parts);
// Build parent structure (might be slow for really deep and large structures)
$parentArr = &$returnArr;
foreach ($parts as $part) {
if (!isset($parentArr[$part])) {
$parentArr[$part] = array();
} elseif (!is_array($parentArr[$part])) {
$parentArr[$part] = $baseval ? array('__base_val' => $parentArr[$part]) : array();
}
$parentArr = &$parentArr[$part];
}
// Add the final part to the structure
if (empty($parentArr[$leafPart])) {
$parentArr[$leafPart] = $val;
} elseif ($baseval && is_array($parentArr[$leafPart])) {
$parentArr[$leafPart]['__base_val'] = $val;
}
}
return $returnArr;
}
#9. Craig Francis on 10 October 2007
<?php
... [more] function printFolder($folder, $prefix = '') {
$folder = str_replace('\\', '/', $folder);
if (substr($folder, -1) != '/') {
$folder .= '/';
}
if ($handle = opendir($folder)) {
while (false !== ($file = readdir($handle))) {
if (!preg_match('/^\./', $file)) {
if (is_dir($folder . $file)) {
echo $prefix . '+ ' . $file . "\n";
printFolder($folder . $file . '/', ' ' . $prefix);
} else {
echo $prefix . '- ' . $file . "\n";
}
}
}
closedir($handle);
}
}
printFolder('/etc/php5/');
?>
#8. Kevin on 10 October 2007
And yes, such a tree could be passed on nicely with serialize, but that's a bit outside this article's scope.
#7. Kevin on 10 October 2007
I would like to include your function in this article however, shall I credit it to: 'lachlan' ?
btw, yours currently does not have a real 'baseval' replacement. In the original function baseval is used to store the values of parent nodes as well as leafs. In the example look at: '/etc/php5', this value isn't saved by expand().
... [more]
Thanks for your effort!
#6. Andrew on 10 October 2007
If you haven't already, take a look at RecursiveIterators (SPL) and SimpleXML.
http://cvs.php.net/viewvc.cgi/php-src/ext/spl/examples/
#5. lachlan on 10 October 2007
Try this:
function expand($array,$delim='/')
... [more] {
$newArray = array();
$delim = preg_quote($delim,'/');
foreach($array as $key=>$value)
{
$current = &$newArray;
$stack = preg_split("/$delim/",$key,-1,PREG_SPLIT_NO_EMPTY);
// iterate down the array, leaving $current as the leaf
foreach($stack as $item)
{
// this actually clobbers non-leafs, replace for baseval
if(isset($current[$item]) && !is_array($current[$item]))
{
$current[$item] = array();
}
$current =& $current[$item];
}
// set the leaf value
$current = $value;
}
return $newArray;
}
#4. Kevin on 09 October 2007
#3. Kevin on 09 October 2007
However the example's main purpose is to illustrate how the function works and just one of it's many possible uses. My first thought was to use the POST example because everyone is familiar with that technology. Thanks for your comment, I'll think about updating the article.
#2. guigouz on 09 October 2007
<input type="text" name="serverinfo[1][hostname]"/>
<input type="text" name="serverinfo[2][ip_address]"/> ?
even if you use multiple name="serverinfo[]" attrs php will turn those in an ordered array
#1. Charles on 09 October 2007
You don't need a function for this.
Since PHP3 or so, PHP automagically turns input into arrays if you use square brackets.
... [more]
<input name="serverinfo[1][hostname]" value="server1.example.com" />
<input name="serverinfo[1][ipaddress]" value="123.123.123.123" />
<input name="serverinfo[2][hostname]" value="server2.another.com" />
<input name="serverinfo[2][ipaddress]" value="234.234.234.234" />