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.

Was it helpful?

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);
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top