2

I want to insert a node with children at a specific location in the XML file. How do I do it?

For eg. If I have an XML like:

<myvalues>
 <image name="img01">
    <src>test</src>
 </image>

 <image name="img02">
    <src>test</src>
 </image>

 <image name="img03">
    <src>test</src>
 </image>
</myvalues>

I want to insert:

<image name="img11">
  <src>test2</src>
</image>

between <image name="img01"> & <image name="img02">. How do I do this? I am using SimpleXML right now to read the XML.

Thanks.

EDIT: I tried the following code. But, the new node is added at the bottom of the XML outside the XML structure.

$xml = new DomDocument();


$xml->preserveWhitespace = false;
 $xml->load('myXMLFile.xml');

 $newNode = $xml->createElement('tryimage');

 $xpath = new DOMXpath($xml);
 $elements = $xpath->query('/myvalues/image[name="img01"]');

 $refNode = $elements->item(0);

 $xml->insertBefore($newNode, $refNode->nextSibling);


 header('Content-Type: text/plain');
 echo $xml->saveXML();

The output is something like this:

<xml....>
   <myvalues>
     <image name="01">
     </image>
     .
     .
     .
   </myvalues>
<tryimage />

2 Answers 2

3

Well, there's no easy way that I can see with SimpleXML (it's supposed to be simple after all).

One way, would be to move the myvalues node over to DOMDocument, add the node there, then replace it with the dom node. Given that $myvalues is your <myvalues> node in SimpleXML:

$domMyValues = dom_import_simplexml($myvalues);
$newNode = $domMyValues->ownerDocument->createElement('mynewelement');
//Apply attributes and whatever to $newNode

//find the node that you want to insert it before (from the $domMyValues class
$nodes = $domMyValues->getElementsByTagName('image');
$refNode = null;
foreach ($nodes as $node) {
    if ($node->getAttribute('name') == 'img02') {
        $refNode = $node;
    }
}
$domMyValues->insertBefore($newNode, $refNode);

Note, there's no need to convert back to SimpleXML, since any changes to the DOMElement will be applied automatically to the SimpleXML version... It will automatically append the new child if it can't find the $refNode (because it doesn't exist, etc)...

EDIT: Adding XPath

Replace the foreach block with this (Functionally equivalent, if I got the query right):

$xpath = new DOMXpath($domMyValues->ownerDocument);
$elements = $xpath->query('//image[@name="img02"]');
$refNode = $elements->item(0);

Since DOMNodeList::item() returns null for a non-existent offset, we don't even need to check to see if there are items in it.

Now, you may need/want to adjust the xpath query to be more/less specific. Here's a decent tutorial...

Edit 2

I forgot that xpath needed an @ character to tell it to check an attribute.

Here's my working code (since I don't know your exact schema):

$x = '<?xml version="1.0" ?>
<myvalues>
        <images>
                <image name="01">Foo</image>
                <image name="02">Bar</image>
        </images>
</myvalues>';

$dom = new DomDocument();
$dom->loadXML($x);
$xpath = new DOMXpath($dom);

$elements = $xpath->query('//images/image[@name="01"]');
$elements = $xpath->query('//image[@name="01"]');
$elements = $xpath->query('/myvalues/images/image[@name="01"]');
Sign up to request clarification or add additional context in comments.

10 Comments

I'd use an XPath to get to the image node by name directly, but apart from that +1
@Gordon: how using the XPath? @ircmaxell: Thanks. What in case I don't know before which node I need to add but I know after which node I need to add. In that case can I use insertBefore? Probably something like insertBefore($newNode, $refNode->nextSibling)? Does this makes sense?
Sure. DomNode->nextSibling will return null if it's the last in the list, so the operation will still be consistent and deterministic. I'll edit my answer with the XPath bit...
Edited the code but somehow the new node is added at the bottom of the XML, outside the XML structure. :(
You're calling insertBefore on the document node. That's fine so long as $refNode is not null (which it will be when xpath fails to find the element). So either check if it's null (and if it is, don't add the element), or find the parent node to where you want to add it and call ->insertBefore() on that node. Your xpath should be: //images/image[name="img01"] OR /xml/images/image[name="img01"] (considering your root node is named "xml"...
|
2

You can, as mentioned in ircmaxel's answer make use of the DOM classes/methods to do what you want. Below is a concise (probably less code than you would really want to use) example of inserting a SimpleXMLElement after another one.

function insertAfter(SimpleXMLElement $new, SimpleXMLElement $target) {
    $target = dom_import_simplexml($target);
    $new    = $target->ownerDocument->importNode(dom_import_simplexml($new), true);
    if ($target->nextSibling) {
        $target->parentNode->insertBefore($new, $target->nextSibling);
    } else {
        $target->parentNode->appendChild($new);
    }
}

$sxe = new SimpleXMLElement($xml);
$img = $sxe->xpath('//image[@name="img01"]'); // xpath returns an array
if (!empty($img)) {
    $new = new SimpleXMLElement('<image name="img11" foo="bar"><src>test</src></image>');
    insertAfter($new, $img[0]);
}

foreach ($sxe as $image) {
    echo $image['name'] . PHP_EOL;
}
/*
    img01
    img11
    img02
    img03
*/

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.