10

is there a way to retrieve only the immediate children found by a call to DOMElement::getElementsByTagName? For example, I have an XML document that has a category element. That category element has sub category elements (which have the same structure), like:

<category>
    <id>1</id>
    <name>Top Level Category Name</name>
    <subCategory>
        <id>2</id>
        <name>Sub Category Name</name>
    </subCategory>
    ...
</category>

If I have a DOMElement representing the top level category,

$topLevelCategoryElement->getElementsByTagName('id');

will return a list with the nodes for all 'id' elements, where I want just the one from the top level. Any way to do this outside of using XPath?

rr.
  • 6,484
  • 9
  • 40
  • 48

4 Answers4

18

I'm afraid not. You'll have to iterate through the children or use XPath.

for ($n = $parent->firstChild; $n !== null; $n = $n->nextSibling) {
    if ($n instanceof DOMElement && $n->tagName == "xxx") {
        //...
    }
}

Example with XPath and your XML file:

$xml = ...;
$d = new DOMDocument();
$d->loadXML($xml);
$cat = $d->getElementsByTagName("subCategory")->item(0);
$xp = new DOMXpath($d);
$q = $xp->query("id", $cat); //note the second argument
echo $q->item(0)->textContent;

gives 2.

Artefacto
  • 96,375
  • 17
  • 202
  • 225
  • this is actually better than my in-SO-textbox function draft. (I forgot the instanceof check) – Kris Jun 15 '10 at 23:03
  • I'd upvote again for the added xpath solution. choice is good – Kris Jun 15 '10 at 23:09
  • is $q an array? I mean, can I go through and do something like foreach($q as $r) { if ($q instanceof DOMNode) { if ($q->hasAttributes()) { foreach ($q->attributes as $attr) { $name = $attr->nodeName; $value = $attr->nodeValue; $scriptAttr[] = array('attr'=>$name, 'value'=>$value); } return $scriptAttr; } } } ??? – EllaJo May 18 '11 at 14:45
  • 2
    @Ella It's not an array, it's a `DOMNodeList` object, which implements `Traversable` and can be iterated with `foreach` – Artefacto May 18 '11 at 15:52
13

Something like this should do

/**
 * Traverse an elements children and collect those nodes that
 * have the tagname specified in $tagName. Non-recursive
 *
 * @param DOMElement $element
 * @param string $tagName
 * @return array
 */
function getImmediateChildrenByTagName(DOMElement $element, $tagName)
{
    $result = array();
    foreach($element->childNodes as $child)
    {
        if($child instanceof DOMElement && $child->tagName == $tagName)
        {
            $result[] = $child;
        }
    }
    return $result;
}

edit: added instanceof DOMElement check

JLRishe
  • 99,490
  • 19
  • 131
  • 169
Kris
  • 40,604
  • 9
  • 72
  • 101
  • Thanks, this was helpful. One thing that I had to change was `$element->children` to `$element->childNodes`. Possibly something changed since this answer was posted. – dontGoPlastic Dec 27 '12 at 16:48
2

I don't use PHP, but if PHP actually implements the DOM API as specified the W3C, there is required to be a childNodes property on any Node object. You should be able to iterate over all of the direct children and test their tag name to see if they're the one you're looking for. Depending on what your tree looks like, this may be slower than getting all the elements by tag name and testing their tree position, or it may be significantly faster.

Nick Bastin
  • 30,415
  • 7
  • 59
  • 78
-1

You can compare elements using ===. This will avoid instantiating a Xpath object.

$dom = new DOMDocument();
$dom->loadXML($xmlString);

$return          = array();

//find your top level element
$topLevelElement = $dom->getElementsByTagName('category');
//find all `id` child elements recursively
$idElements      = $topLevelElement->getElementsByTagName('id');

if ($idElements->length > 0) {
    foreach ($idElements as $idElement) {
        if ($idElement->parentNode === $topLevelElement) {
            $return[]   = $idElement;
       }
    }
}

//$return now holds non nested child elements

Depending on the size of your XML document you might find Xpath performs better. However in terms of elegance this solution is cleaner.

MerlinTheMagic
  • 575
  • 5
  • 16