How to change root of a node with DOMDocument methods?
Asked Answered
W

5

1

How to only change root's tag name of a DOM node?

In the DOM-Document model we can not change the property documentElement of a DOMElement object, so, we need "rebuild" the node... But how to "rebuild" with childNodes property?


NOTE: I can do this by converting to string with saveXML and cuting root by regular expressions... But it is a workaround, not a DOM-solution.


Tried but not works, PHP examples

PHP example (not works, but WHY?):

Try-1

 // DOMElement::documentElement can not be changed, so... 

 function DomElement_renameRoot1($ele,$ROOTAG='newRoot') { 
    if (gettype($ele)=='object' && $ele->nodeType==XML_ELEMENT_NODE) {
   $doc = new DOMDocument();
   $eaux = $doc->createElement($ROOTAG); // DOMElement

       foreach ($ele->childNodes as $node)  
       if ($node->nodeType == 1)  // DOMElement 
               $eaux->appendChild($node);  // error!
       elseif ($node->nodeType == 3)  // DOMText
               $eaux->appendChild($node); // error!
       return $eaux;
    } else
        die("ERROR: invalid DOM object as input");
  }

The appendChild($node) cause an error:

 Fatal error: Uncaught exception 'DOMException' 
 with message 'Wrong Document Error'

Try-2

From @can suggestion (only pointing link) and my interpretation of the poor dom-domdocument-renamenode manual.

 function DomElement_renameRoot2($ele,$ROOTAG='newRoot') {
$ele->ownerDocument->renameNode($ele,null,"h1");
    return $ele;
 }

The renameNode() method caused an error,

Warning: DOMDocument::renameNode(): Not yet implemented

Try-3

From PHP manual, comment 1.

 function renameNode(DOMElement $node, $newName)
 {
     $newNode = $node->ownerDocument->createElement($newName);
     foreach ($node->attributes as $attribute)
        $newNode->setAttribute($attribute->nodeName, $attribute->nodeValue);
     while ($node->firstChild)
        $newNode->appendChild($node->firstChild); // changes firstChild to next!?
     $node->ownerDocument->replaceChild($newNode, $node); // changes $node?
     // not need return $newNode; 
 }

The replaceChild() method caused an error,

Fatal error: Uncaught exception 'DOMException' with message 'Not Found Error'
Wide answered 26/3, 2013 at 20:28 Comment(2)
I added an answer that shows a function how to do it and also lightly points out where the error is in the code in your question. A larger example is on a different answer I linked in there: https://mcmap.net/q/1471232/-how-to-change-root-of-a-node-with-domdocument-methodsReplevy
The root tag is commonly expected to be the document element or root node. So the wording you use in your question might be a little misleading. Do you want to rename the tagname of the root element of the document or do you just want to rename a domelements tagname? (Clarification question)Replevy
R
4

As this has not been really answered yet, the error you get about not found is because of a little error in the renameNode() function you've copied.

In a somewhat related question about renaming different elements in the DOM I've seen this problem as well and used an adoption of that function in my answer that does not have this error:

/**
 * Renames a node in a DOM Document.
 *
 * @param DOMElement $node
 * @param string     $name
 *
 * @return DOMNode
 */
function dom_rename_element(DOMElement $node, $name) {
    $renamed = $node->ownerDocument->createElement($name);

    foreach ($node->attributes as $attribute) {
        $renamed->setAttribute($attribute->nodeName, $attribute->nodeValue);
    }

    while ($node->firstChild) {
        $renamed->appendChild($node->firstChild);
    }

    return $node->parentNode->replaceChild($renamed, $node);
}

You might have spotted it in the last line of the function body: This is using ->parentNode instead of ->ownerDocument. As $node was not a child of the document, you did get the error. And it also was wrong to assume that it should be. Instead use the parent element to search for the child in there to replace it ;)

This has not been outlined in the PHP manual usernotes so far, however, if you did follow the link to the blog-post that originally suggested the renameNode() function you could find a comment below it offering this solution as well.

Anyway, my variant here uses a slightly different variable naming and is more distinct about the types. Like the example in the PHP manual it misses the variant that deals with namespace nodes. I'm not yet booked what would be best, e.g. creating an additional function dealing with it, taking over namespace from the node to rename or changing the namespace explicitly in a different function.

Replevy answered 1/5, 2013 at 8:32 Comment(0)
C
3

First, you need to understand that the DOMDocument is only the hierarchical root of the document-tree. It's name is always #document. You want to rename the root-element, which is the $document->documentElement.

If you want to copy nodes form a document to another document, you'll need to use the importNode() function: $document->importNode($nodeInAnotherDocument)

Edit:

renameNode() is not implemented yet, so you should make another root, and simply replace it with the old one. If you use DOMDocument->createElement() you don't need to use importNode() on it later.

$oldRoot = $doc->documentElement;
$newRoot = $doc->createElement('new-root');

foreach ($oldRoot->attributes as $attr) {
  $newRoot->setAttribute($attr->nodeName, $attr->nodeValue);
}

while ($oldRoot->firstChild) {
  $newRoot->appendChild($oldRoot->firstChild);
}

$doc->replaceChild($newRoot, $oldRoot); 
Crusted answered 4/4, 2013 at 20:15 Comment(7)
Thanks a lot by the clue, and perhaps a solution (!), but I not see yet how DOMDocument::importNode() can be used in my xml_renameNode() function, without a loop, if something like importNode($node->childNodes) can not be used.Wide
These are two different solutions: if you want to rename the root tag's name you should use $document->documentElement->renameNode(); if you want to make another document with another root, use $document->importNode.Crusted
Yes, good practice... And, well, now I understand the context where you and @PointedEars see importNode as a solution to the rename problem... But even when is "another document with another root" you need a loop.Wide
About your edit: thanks, but it is exactly the same algorithm than the mine, with same loop. The difference is that in a function scope I not need replaceChild.Wide
Yes, but it doesn't thorws a DOMException, so it works. The concept is you need to change the documentElement, not the document itself. And: you cannot make it without the loops, until renameNode() is not implemented.Crusted
This does not consider namespaces and nodes that are not root element nodes.Griswold
It's unFUCKAbelievable that one has to do SO MUCH WORK to simply ASSERT CONTROL of how the root element is named.Coriecorilla
W
1

This is an variation of my "Try-3" (see question), and works fine!

  function xml_renameNode(DOMElement $node, $newName, $cpAttr=true) {
      $newNode = $node->ownerDocument->createElement($newName);
      if ($cpAttr && is_array($cpAttr)) {
        foreach ($cpAttr as $k=>$v)
             $newNode->setAttribute($k, $v);
      } elseif ($cpAttr)
        foreach ($node->attributes as $attribute)
             $newNode->setAttribute($attribute->nodeName, $attribute->nodeValue);

      while ($node->firstChild)
          $newNode->appendChild($node->firstChild);
      return $newNode;
  }    

Of course, if you show how to use DOMDocument::renameNode (without errors!), the bounty goes for you!

Wide answered 29/3, 2013 at 11:26 Comment(5)
You have been mislead, and are referring to bogus documentation. DOMDocument::renameNode() as of DOM Level 3 Core is not implemented in PHP as of yet, so any approach using that must fail (or would at least be unreliable as it would be only in trunk builds). However, you do not need DOMDocument::renameNode(). DOM Level 2 Core methods suffice here, see my answer.Griswold
I used the renameNode-link pointed by Kan's answer, that showed us a valid solution before before Methai edit my question with the PHP-tag. After the Methai's scope reduction, @kan show that PHP have a 2-yers-old bug. No DOM Level 2 Core methods is suffice (!), only using a loop. A loop-solution is first showed here by me.Wide
You and @Crusted not show an alternative solution for this one, using appendChild in a loop.Wide
@kan has showed that they have no clue what they are talking about, and you fell for the troll. It's not a bug that renameNode() is not implemented in PHP as of yet. You could call it an oversight, perhaps, considering that textContent from DOM Level 3 is implemented as DOMNode::textContent. Your "solution" is not only overly complicated, it is not a proper implementation of DOMDocument::renameNode(). I have given you hints as how to implement DOMDocument::renameNode() properly already, and you let the bounty expire. ISTM you are much too sure of yourself for your own good.Griswold
+1, I think it's a good shot even if not the 'proper' way. @Griswold You're being a little harsh in a lot of the comments on here, just not needed.Disarrange
G
1

ISTM in your approach you attempt to import nodes from another DOMDocument, so you need to use the importNode() method:

$d = new DOMDocument();

/* Make a `foo` element the root element of $d */
$root = $d->createElement("foo");
$d->appendChild($root);

/* Append a `bar` element as the child element of the root of $d */
$child = $d->createElement("bar");
$root->appendChild($child);

/* New document */
$d2 = new DOMDocument();

/* Make a `baz` element the root element of $d2 */
$root2 = $d2->createElement("baz");
$d2->appendChild($root2);

/* 
 * Import a clone of $child (from $d) into $d2,
 * with its child nodes imported recursively
 */
$child2 = $d2->importNode($child, true);

/* Add the clone as the child node of the root of $d2 */
$root2->appendChild($child2);

However, it is far easier to append the child nodes to a new parent element (thereby moving them), and replace the old root with that parent element:

$d = new DOMDocument();

/* Make a `foo` element the root element of $d */
$root = $d->createElement("foo");
$d->appendChild($root);

/* Append a `bar` element as the child element of the root of $d */
$child = $d->createElement("bar");
$root->appendChild($child);

/* <?xml version="1.0"?>
   <foo><bar/></foo> */
echo $d->saveXML();

$root2 = $d->createElement("baz");

/* Make the `bar` element the child element of `baz` */
$root2->appendChild($child);

/* Replace `foo` with `baz` */
$d->replaceChild($root2, $root);

/* <?xml version="1.0"?>
   <baz><bar/></baz> */
echo $d->saveXML();
Griswold answered 4/4, 2013 at 20:2 Comment(2)
Thanks a lot, you show new clues and possible solution! Can you show in terms of a xml_renameNode() function? My problem is to undertand how to do $root->appendChild($child) if child is a list of nodes... Se my "while ($node->firstChild)" implementation. You can do (with replaceChild) without the loop?Wide
I do not think you can append several children without calling appendChild() in a loop (my client-side script library contains an appendChildren() method do just that). As for a renameNode() implementation in PHP, you just need to instantiate a subclass of DOMDocument. (Too bad you let the bounty expire.)Griswold
A
0

I hope I am not missing anything but I happened to have the similar problem and was able to solve it by using use DomDocument::replaceChild(...).

   /* @var $doc DOMDocument */
   $doc = DOMImplementation::createDocument(NULL, 'oldRoot');

   /* @var $newRoot DomElement */
   $newRoot = $doc->createElement('newRoot');
   /* all the code to create the elements under $newRoot */

   $doc->replaceChild($newRoot, $doc->documentElement);

   $doc->documentElement->isSameNode($newRoot) === true;

What threw me off initially was that $doc->documentElement was readonly, but the above worked and seems to be much simpler solution IF the $newRoot was created with the same DomDocument, otherwise you'll need do the importNode solution as described above. From your question is appears that $newRoot could be created from the same $doc.

Let us know if this worked out for you. Cheers.

EDIT: Noticed in version 20031129 that the DomDocument::$formatOutput, if set, does not format $newRoot output when you finally call $doc->saveXML()

Asphyxia answered 3/10, 2014 at 23:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.