tags:

views:

45

answers:

2

Hi Guys,

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 />
+1  A: 

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"]');
ircmaxell
I'd use an XPath to get to the image node by name directly, but apart from that +1
Gordon
@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?
Blueboye
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...
ircmaxell
Edited the code but somehow the new node is added at the bottom of the XML, outside the XML structure. :(
Blueboye
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"...
ircmaxell
The $refNode = $elements->item(0); is setting the $refNode to NULL. If I try $refNode = $elements;, it is not NULL anymore but it adds the new node at the bottom again. I guess I am not able to use the right XPath Query. I am using $elements = $xpath->query('/resources/images/image[name="halo"]'); where the resource tag looks like this: <resources xmlns="http://mydomain/external/myID">.
Blueboye
Well, then you'd need to play around with your XPATH query until you get it to return the proper elements. When you set `$refNode = $elements`, you're setting it to a DOMNodeList, which is not valid (but it extends DomNode, which is why it lets you) so it treats it as `NULL`...
ircmaxell
For some reason the xpath doesn't seem to work at all. I tried $elements = $xpath->query('//image'); and still the $refNode is NULL. Do, I need to have xpath enabled or something like that? I believe it is a part of the core PHP system.
Blueboye
Ok. Finally, I was able to make the other code work (non xpath). Is there a way to format the XML output? Right now all the data is being added in a single line. I want it to be added with breaks and indents.Thanks a lot for your help :).
Blueboye
Check the docs: http://www.php.net/manual/en/class.domdocument.php#domdocument.props.formatoutput
ircmaxell
A Big Thanks to you !!! You saved my day :).
Blueboye
Can you hit the check mark by my answer then to "accept" the answer? Thanks...
ircmaxell
+1  A: 

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
*/
salathe