Search code examples
phplistdomdomdocument

PHP DOM How to get items and sub-items from UL


I'm trying to get all items and sub-items with anchor tag form the following menu:

<nav class="header-nav" id="headerLara">
	<div class="menu-hauptmenu-container">
		<ul id="head_nav_ul" class="menu">
			<li class="menu-item menu-item-type-custom menu-item-object-custom menu-item-has-children menu-item-4">
				<a>First Menu</a>
				<ul class="sub-menu">
					<li class="menu-item menu-item-type-post_type menu-item-object-page menu-item-14002">
						<a href="http://example.com/fm1">F menu 1</a>
					</li>
					<li class="menu-item menu-item-type-post_type menu-item-object-post menu-item-12718">
						<a href="http://example.com/fm2">F menu 2</a>
					</li>
				</ul>
			</li>
			<li class="menu-item menu-item-type-custom menu-item-object-custom menu-item-has-children menu-item-6">
				<a>Second Menu</a>
				<ul class="sub-menu">
					<li class="menu-item menu-item-type-post_type menu-item-object-page menu-item-1257">
						<a href="http://example.com/sm1">S menu 1</a>
					</li>
					<li class="menu-item menu-item-type-post_type menu-item-object-page menu-item-5420">
						<a href="http://example.com/sm2">S menu 2</a>
					</li>
				</ul>
			</li>
			<li class="menu-item menu-item-type-custom menu-item-object-custom menu-item-12821">
				<a href="http://example.com/m3">Third Menu</a>
			</li>
		</ul>
	</div>
</nav>

Now i want outpul like :

<nav class="header-nav" id="headerLara">
	<div class="menu-hauptmenu-container">
		<ul>
			<li>
				<a class="has-child">First Menu</a>
				<ul>
					<li>
						<a href="http://example.com/fm1">F menu 1</a>
					</li>
					<li>
						<a href="http://example.com/fm2">F menu 2</a>
					</li>
				</ul>
			</li>
			<li>
				<a class="has-child">Second Menu</a>
				<ul>
					<li>
						<a href="http://example.com/sm1">S menu 1</a>
					</li>
					<li>
						<a href="http://example.com/sm2">S menu 2</a>
					</li>
				</ul>
			</li>
			<li>
				<a href="http://example.com/m3">Third Menu</a>
			</li>
		</ul>
	</div>
</nav>

I've done some R&D and tried with following PHP code :

    <?php
$doc = new DomDocument;
$doc->validateOnParse = true;
$doc->loadHtml(file_get_contents('http://example.com/blabla.php'));
$header = $doc->getElementById('headerLara');

$mainUls = $header->getElementsByTagName('ul');
foreach ($mainUls as $mainUl) {
    echo '<ul>';
    $mainLis = $mainUl->getElementsByTagName('li');
    foreach ($mainLis as $mainLi) {
    echo '<li>';
    $mainAnc = $mainLi->getElementsByTagName('a');
    $href = $mainAnc->item(0)->getAttribute('href');
    echo '<a class="has-child" href="'.$href.'">'.$mainAnc->item(0)->nodeValue.'</a>';   
    $secUls = $mainLi->getElementsByTagName('ul');
    if($secUls->length < 2){
        foreach ($secUls as $secUl) {
            echo '<ul>';
            $secLis = $secUl->getElementsByTagName('li');
            foreach ($secLis as $secLi) {
                echo '<li>';
                $secAnc = $mainLi->getElementsByTagName('a');
                $shref = $secAnc->item(0)->getAttribute('href');
                echo '<a href="'.$shref.'">'.$secAnc->item(0)->nodeValue.'</a>';  
                echo '</li>';
            }
            echo '</ul>';
        }
    }
    echo '</li>';
    }
    echo '</ul>';
}
?> 

But this is not working for me as i want and return output like:

<ul>
	<li>
		<a class="has-child" href="">First Menu</a>
		<ul>
			<li>
				<a href="">First Menu</a>
			</li>
			<li>
				<a href="">First Menu</a>
			</li>
		</ul>
	</li>
	<li>
		<a class="has-child" href="http://example.com/fm1">F menu 1</a>
	</li>
	<li>
		<a class="has-child" href="http://example.com/fm2">F menu 2</a>
	</li>
	<li>
		<a class="has-child" href="">Second Menu</a>
		<ul>
			<li>
				<a href="">Second Menu</a>
			</li>
			<li>
				<a href="">Second Menu</a>
			</li>
		</ul>
	</li>
	<li>
		<a class="has-child" href="http://example.com/sm1">S menu 1</a>
	</li>
	<li>
		<a class="has-child" href="http://example.com/sm2">S menu 2</a>
	</li>
</ul>

I've checked many links which seems similar to my problem but found nothing helpful.

How can i get the proper output, Thanks in advance.


Solution

  • There are a few minor errors (picking up from the wrong node) but there are two main problems.

    The first is getElementsByTagName() selects all child elements with that tag name, this isn't limited to immediate child nodes, so each time it would be more tags than you are expecting. In this code it uses XPath as DOMDocument doesn't have a convenient way of doing a just immediate child nodes called, so XPath just uses the context node as your start point and something like a to say only <a> tags who are direct descendants of the context node.

    The other (main thing) is that you are building the output using echo statements. Which may work, but is also prone to typos, invalid structure etc. This code uses the DOM API calls to create the document.

    $doc = new DomDocument;
    $doc->validateOnParse = true;
    $doc->loadHtml($html);
    $xp = new DOMXPath($doc);
    
    $header = $doc->getElementById('headerLara');
    $mainUls = $xp->query('div/ul', $header);
    foreach ($mainUls as $mainUl) {
        $mainULE = $doc->createElement("ul");
        $mainLis = $xp->query('li', $mainUl);
        foreach ($mainLis as $mainLi) {
            $li = $doc->createElement("li");
            $mainAnc = $xp->query('a', $mainLi)[0];
    
            $href = $mainAnc->getAttribute('href');
            $a = $doc->createElement("a", htmlspecialchars($mainAnc->nodeValue));
            $href = $mainAnc->getAttribute('href');
            if ( !empty($href) )    {
                $a->setAttribute("href", $href);
            }
            $li->appendChild($a);
            $secUls = $xp->query('ul', $mainLi);
            if($secUls->length < 2){
                foreach ($secUls as $secUl) {
                    $a->setAttribute("class", "has-child");
                    $secULE = $doc->createElement("ul");
                    $secLis = $xp->query('li', $secUl);
                    foreach ($secLis as $secLi) {
                        $secLIE = $doc->createElement("li");
                        $secAnc = $xp->query('a', $secLi);
                        $shref = $secAnc[0]->getAttribute('href');
                        $secA = $doc->createElement("a", htmlspecialchars($secAnc[0]->nodeValue));
                        $secA->setAttribute("href", $shref);
                        $secLIE->appendChild($secA);
                        $secULE->appendChild($secLIE);
                    }
                    $li->appendChild($secULE);
                }
            }
            $mainULE->appendChild($li);
        }
        echo PHP_EOL.PHP_EOL.">>>>".$doc->saveHTML($mainULE);
        // Next line replaces existing HTML
        //$mainUl->parentNode->replaceChild($mainULE,$mainUl);
    }