Search code examples
phpxmlsimplexmlnodes

XML File - Get specific child nodes in unlimited node depths


I'm working on a website that uses hierarchical data. After struggling to do it with MySQL databases (really complicated...), I decided to dive into XML because it sounds like XML works perfectly for my needs.

Now I'm experimenting with an XML File and SimpleXML. But first of all, here is what my XML File looks like:

<?xml version="1.0" encoding="ISO-8859-1" ?>

<content>
    <parent>
        <child id="1">
            <title>child 1</title>

            <child id="1">
                <title>child 1.1</title>

                <child id="1">
                    <title>child 1.1.1</title>
                </child>
            </child>

            <child id="2">
                <title>child 1.2</title>

                <child id="1">
                    <title>child 1.2.1</title>

                        <child id="1">
                            <title>child 1.2.1.1</title>
                        </child>
                </child>

                <child id="2">
                    <title>child 1.2.2</title>
                </child>
            </child>

            <child id="3">
                <title>child 1.3</title>
            </child>
        </child>
    </parent>
</content>

As you can see, it has a variing "depth" of child nodes. I also don't know the depth of childs, as they are created by the web app. This depth or "number of layers" can get quite high.

Now I want to read this XML File in my website. For example, I want to visualize it as a tree, with all the child nodes represented as circles connected to their parent circle.

I've managed to have a foreach getting all the first-layer "child" elements an then another foreach in it getting all second-layer "child" elements. The problem is, that this limits the number of layers I can visualize because I cannot have a dozen nested foreach'es.

Now I already have a headache thinking of a way of "unlimited nested foreach structures" to get all the layers of "child" nodes. But I'm not able to find a way of doing it.

Do you have an idea how to do it? Please help me! Thanks in advance.

PS: Sorry for my english, I'm an german teenager student :)

EDIT: Here is the code in my test.php:

<?php
    if (file_exists('mydata.xml'))
    {
        $xml = simplexml_load_file('mydata.xml');
?>

<ul>
<?php 
        foreach($xml->parent->child as $item) // Go through first layer
        {
            echo "<li>".$item->title;

            echo "<ul>"; // Open second layer <ul>
            foreach($item->child as $item) // Go through second layer
            {
                echo "<li>".$item->title."</li>";
            }
            echo "</ul>"; // Close second layer <ul>

            echo "</li>"; // Close child <li>
        }
    }
    else
    {
       exit('Konnte Datei nicht laden.');
    }
?>
</ul>

This is the result, just what I was expecting:

- child 1

    - child 1.1
    - child 1.2
    - child 1.3

So this works fine, but as mentioned in the comments, I need this not only for layer 1 to 2, but for layer 1 to n. Would really appreciate if someone has an idea :)


Solution

  • Two essentially identical examples are below. In each we define a function renderNode() that gets called recursively to render the nested lists. There's not a lot of code so there's not a lot to say.

    One is based on SimpleXML, because that's what you're currently experimenting with.

    The other is based on the DOM extension, because I personally find it to be a better API to work with (for all the reasons listed here and then some.)

    For what you're doing here it's not terribly relevant which you use, but options are always nice.


    DOM Example:

    $dom = new DOMDocument();
    $dom->load('mydata.xml');
    $xpath = new DOMXPath($dom);
    
    echo "<ul>";
    foreach ($xpath->query('/content/parent/child') as $node) {
        renderNode($node, $xpath);
    }
    echo "</ul>";
    
    function renderNode(DOMElement $node, DOMXPath $xpath) {
        echo "<li>", $xpath->evaluate('string(title)', $node);
        $children = $xpath->query('child', $node);
        if ($children->length) {
            echo "<ul>";
            foreach ($children as $child) {
                renderNode($child, $xpath);
            }
            echo "</ul>";
        }
        echo "</li>";
    };
    

    SimpleXML Example:

    $xml = simplexml_load_file('mydata.xml');
    
    echo "<ul>";
    foreach ($xml->parent->child as $node) {
        renderNode($node);
    }
    echo "</ul>";
    
    function renderNode($node) {
        echo "<li>", $node->title;
        if ($node->child) {
            echo "<ul>";
            foreach ($node->child as $child) {
                renderNode($child);
            }
            echo "</ul>";
        }
        echo "</li>";
    }
    

    Output (beautified, identical for both examples):

    <ul>
        <li>child 1
            <ul>
                <li>child 1.1
                    <ul><li>child 1.1.1</li></ul>
                </li>
                <li>child 1.2
                    <ul>
                        <li>child 1.2.1
                            <ul><li>child 1.2.1.1</li></ul>
                        </li>
                        <li>child 1.2.2</li>
                    </ul>
                </li>
                <li>child 1.3</li>
            </ul>
        </li>
    </ul>
    

    And just for kicks, here's a bonus option using XSLT. The beautified output is the same as above.

    XSLT Example:

    PHP:

    $xmldoc = new DOMDocument();
    $xmldoc->load('mydata.xml');
    
    $xsldoc = new DOMDocument();
    $xsldoc->load('example.xsl');
    
    $xsl = new XSLTProcessor();
    $xsl->importStyleSheet($xsldoc);
    echo $xsl->transformToXML($xmldoc);
    

    example.xsl:

    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
        <xsl:output method="html" encoding="UTF-8" indent="no"/>
    
        <xsl:template match="/content/parent">
            <ul>
                <xsl:apply-templates select="child"/>
            </ul>
        </xsl:template>
        <xsl:template match="child">
            <li>
                <xsl:value-of select="title"/>
                <xsl:if test="child">
                    <ul>
                        <xsl:apply-templates select="child"/>
                    </ul>
                </xsl:if>
            </li>
        </xsl:template>
    </xsl:stylesheet>