Build hierarchical html tags in PHP from flat data
Question
How do you build a hierarchical set of tags with data in PHP?
For example, a nested list:
<div>
<ul>
<li>foo
</li>
<li>bar
<ul>
<li>sub-bar
</li>
</ul>
</li>
</ul>
</div>
This would be build from flat data like this:
nested_array = array();
nested_array[0] = array('name' => 'foo', 'depth' => 0)
nested_array[1] = array('name' => 'bar', 'depth' => 0)
nested_array[2] = array('name' => 'sub-bar', 'depth' => 1)
It would be nice if it were nicely formatted like the example, too.
Solution
Edit: Added formatting
As already said in the comments, your data structure is somewhat strange. Instead of using text manipulation (like OIS), I prefer DOM:
<?php
$nested_array = array();
$nested_array[] = array('name' => 'foo', 'depth' => 0);
$nested_array[] = array('name' => 'bar', 'depth' => 0);
$nested_array[] = array('name' => 'sub-bar', 'depth' => 1);
$nested_array[] = array('name' => 'sub-sub-bar', 'depth' => 2);
$nested_array[] = array('name' => 'sub-bar2', 'depth' => 1);
$nested_array[] = array('name' => 'sub-sub-bar3', 'depth' => 3);
$nested_array[] = array('name' => 'sub-sub3', 'depth' => 2);
$nested_array[] = array('name' => 'baz', 'depth' => 0);
$doc = new DOMDocument('1.0', 'iso-8859-1');
$doc->formatOutput = true;
$rootNode = $doc->createElement('div');
$doc->appendChild($rootNode);
$rootList = $doc->createElement('ul');
$rootNode->appendChild($rootList);
$listStack = array($rootList); // Stack of created XML list elements
$depth = 0; // Current depth
foreach ($nested_array as $nael) {
while ($depth < $nael['depth']) {
// New list element
if ($listStack[$depth]->lastChild == null) {
// More than one level at once
$li = $doc->createElement('li');
$listStack[$depth]->appendChild($li);
}
$listEl = $doc->createElement('ul');
$listStack[$depth]->lastChild->appendChild($listEl);
array_push($listStack, $listEl);
$depth++;
}
while ($depth > $nael['depth']) {
array_pop($listStack);
$depth--;
}
// Add the element itself
$li = $doc->createElement('li');
$li->appendChild($doc->createTextNode($nael['name']));
$listStack[$depth]->appendChild($li);
}
echo $doc->saveXML();
Your formatting convention is kind of strange. Replace the last line with the following to achieve it:
printEl($rootNode);
function printEl(DOMElement $el, $depth = 0) {
$leftFiller = str_repeat("\t", $depth);
$name = preg_replace('/[^a-zA-Z]/', '', $el->tagName);
if ($el->childNodes->length == 0) {
// Empty node
echo $leftFiller . '<' . $name . "/>\n";
} else {
echo $leftFiller . '<' . $name . ">";
$printedNL = false;
for ($i = 0;$i < $el->childNodes->length;$i++) {
$c = $el->childNodes->item($i);
if ($c instanceof DOMText) {
echo htmlspecialchars($c->wholeText);
} elseif ($c instanceof DOMElement) {
if (!$printedNL) {
$printedNL = true;
echo "\n";
}
printEl($c, $depth+1);
}
}
if (!$printedNL) {
$printedNL = true;
echo "\n";
}
echo $leftFiller . '</' . $name . ">\n";
}
}
OTHER TIPS
I have a question, regarding your question that is too elaborate for a comment field.
How do you want to fit attribute data in that? You'd need a Whory Table™ like
array('html', null, array (
array( 'div' , null , array(
array('ul', array('id'=>'foo'), array(
array('li', null, 'foo' ),
array('li', null, array(
array(null,null, 'bar'),
array('ul', null, array(
array('li', null, 'sub-bar' )
))
))
))
))
))
));
because that is the minimal structure required to accurately represent an HTML dataset programmatically.
I've cheated a bit by eliminating the need for "text-node" elements quite as much, by making the assumption if
array( name, attribute, children )
has a string instead of an array for 'children' then its an implicit text-node, and that nodes with name == null are dont have tags and are thus also text nodes.
What I think you want is a proper programmatic DOM generation tool, which will parse some existing html into a tree to make your life easier
FWIW, the above structure can be serialised into html rather easily.
function tohtml( $domtree ){
if( is_null($domtree[0]) ){
if( !is_array($domtree[2])){
return htmlentities($domtree[2]);
}
die("text node cant have children!");
}
$html = "<" . $domtree[0];
if( !is_null( $domtree[1] ) )
{
foreach( $domtree[1] as $name=>$value ){
$html .= " " . $name . '="' . htmlentities($value) . '"';
}
}
$html .= ">" ;
if( !is_null($domtree[2]) ){
if( is_array($dometree[2]) ){
foreach( $domtree[2] as $id => $item ){
$html .= tohtml( $item ); # RECURSION
}
}
else {
$html .= htmlentities($domtree[2]);
}
}
$html .= "</" . $domtree[1] . ">";
return $html;
}
You mean something like
function array_to_list(array $array, $width = 3, $type = 'ul', $separator = ' ', $depth = 0)
{
$ulSpace = str_repeat($separator, $width * $depth++);
$liSpace = str_repeat($separator, $width * $depth++);
$subSpace = str_repeat($separator, $width * $depth);
foreach ($array as $key=>$value) {
if (is_array($value)) {
$output[(isset($prev) ? $prev : $key)] .= "\n" . array_to_list($value, $width, $type, $separator, $depth);
} else {
$output[$key] = $value;
$prev = $key;
}
}
return "$ulSpace<$type>\n$liSpace<li>\n$subSpace" . implode("\n$liSpace</li>\n$liSpace<li>\n$subSpace", $output) . "\n$liSpace</li>\n$ulSpace</$type>";
}
echo array_to_list(array('gg', 'dsf', array(array('uhu'), 'df', array('sdf')), 'sdfsd', 'sdfd')) . "\n";
produces
<ul>
<li>
gg
</li>
<li>
dsf
<ul>
<li>
<ul>
<li>
uhu
</li>
</ul>
</li>
<li>
df
<ul>
<li>
sdf
</li>
</ul>
</li>
</ul>
</li>
<li>
sdfsd
</li>
<li>
sdfd
</li>
</ul>
I know theres a little gap there if a sub list don't start with an explanation.
Personally I usually don't really care how the HTML looks as long as its easy to work with in PHP.
Edit: OK, it works if you run it through this first ... :P
function flat_array_to_hierarchical_array(array &$array, $depth = 0, $name = null, $toDepth = 0)
{
if ($depth == 0) {
$temp = $array;
$array = array_values($array);
}
if (($name !== null) && ($depth == $toDepth)) {
$output[] = $name;
} else if ($depth < $toDepth) {
$output[] = flat_array_to_hierarchical_array(&$array, $depth + 1, $name, $toDepth);
}
while ($item = array_shift($array)) {
$newDepth = $item['depth'];
$name = $item['name'];
if ($depth == $newDepth) {
$output[] = $name;
} else if ($depth < $newDepth) {
$output[] = flat_array_to_hierarchical_array(&$array, $depth + 1, $name, $newDepth);
} else {
array_unshift($array, $item);
return $output;
}
}
$array = $temp;
return $output;
}
$arr = flat_array_to_hierarchical_array($nested_array);
echo array_to_list($arr);