Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In SimpleXML, how can I add an existing SimpleXMLElement as a child element?

Tags:

oop

php

simplexml

I have a SimpleXMLElement object $child, and a SimpleXMLElement object $parent.

How can I add $child as a child of $parent? Is there any way of doing this without converting to DOM and back?

The addChild() method only seems to allow me to create a new, empty element, but that doesn't help when the element I want to add $child also has children. I'm thinking I might need recursion here.

like image 210
thomasrutter Avatar asked Apr 20 '09 08:04

thomasrutter


2 Answers

Unfortunately SimpleXMLElement does not offer anything to bring two elements together. As @nickf wrote, it's more fitting for reading than for manipulation. However, the sister extension DOMDocument is for editing and you can bring both together via dom_import_simplexml(). And @salathe shows in a related answer how this works for specific SimpleXMLElements.

The following shows how this work with input checking and some more options. I do it with two examples. The first example is a function to insert an XML string:

/**
 * Insert XML into a SimpleXMLElement
 *
 * @param SimpleXMLElement $parent
 * @param string $xml
 * @param bool $before
 * @return bool XML string added
 */
function simplexml_import_xml(SimpleXMLElement $parent, $xml, $before = false)
{
    $xml = (string)$xml;

    // check if there is something to add
    if ($nodata = !strlen($xml) or $parent[0] == NULL) {
        return $nodata;
    }

    // add the XML
    $node     = dom_import_simplexml($parent);
    $fragment = $node->ownerDocument->createDocumentFragment();
    $fragment->appendXML($xml);

    if ($before) {
        return (bool)$node->parentNode->insertBefore($fragment, $node);
    }

    return (bool)$node->appendChild($fragment);
}

This exemplary function allows to append XML or insert it before a certain element, including the root element. After finding out if there is something to add, it makes use of DOMDocument functions and methods to insert the XML as a document fragment, it is also outlined in How to import XML string in a PHP DOMDocument. The usage example:

$parent = new SimpleXMLElement('<parent/>');

// insert some XML
simplexml_import_xml($parent, "\n  <test><this>now</this></test>\n");

// insert some XML before a certain element, here the first <test> element
// that was just added
simplexml_import_xml($parent->test, "<!-- leave a comment -->\n  ", $before = true);

// you can place comments above the root element
simplexml_import_xml($parent, "<!-- this works, too -->", $before = true);

// but take care, you can produce invalid XML, too:
// simplexml_add_xml($parent, "<warn><but>take care!</but> you can produce invalid XML, too</warn>", $before = true);

echo $parent->asXML();

This gives the following output:

<?xml version="1.0"?>
<!-- this works, too -->
<parent>
  <!-- leave a comment -->
  <test><this>now</this></test>
</parent>

The second example is inserting a SimpleXMLElement. It makes use of the first function if needed. It basically checks if there is something to do at all and which kind of element is to be imported. If it is an attribute, it will just add it, if it is an element, it will be serialized into XML and then added to the parent element as XML:

/**
 * Insert SimpleXMLElement into SimpleXMLElement
 *
 * @param SimpleXMLElement $parent
 * @param SimpleXMLElement $child
 * @param bool $before
 * @return bool SimpleXMLElement added
 */
function simplexml_import_simplexml(SimpleXMLElement $parent, SimpleXMLElement $child, $before = false)
{
    // check if there is something to add
    if ($child[0] == NULL) {
        return true;
    }

    // if it is a list of SimpleXMLElements default to the first one
    $child = $child[0];

    // insert attribute
    if ($child->xpath('.') != array($child)) {
        $parent[$child->getName()] = (string)$child;
        return true;
    }

    $xml = $child->asXML();

    // remove the XML declaration on document elements
    if ($child->xpath('/*') == array($child)) {
        $pos = strpos($xml, "\n");
        $xml = substr($xml, $pos + 1);
    }

    return simplexml_import_xml($parent, $xml, $before);
}

This exemplary function does normalize list of elements and attributes like common in Simplexml. You might want to change it to insert multiple SimpleXMLElements at once, but as the usage example shows below, my example does not support that (see the attributes example):

// append the element itself to itself
simplexml_import_simplexml($parent, $parent);

// insert <this> before the first child element (<test>)
simplexml_import_simplexml($parent->children(), $parent->test->this, true);

// add an attribute to the document element
$test = new SimpleXMLElement('<test attribute="value" />');
simplexml_import_simplexml($parent, $test->attributes());

echo $parent->asXML();

This is a continuation of the first usage-example. Therefore the output now is:

<?xml version="1.0"?>
<!-- this works, too -->
<parent attribute="value">
  <!-- leave a comment -->
  <this>now</this><test><this>now</this></test>
<!-- this works, too -->
<parent>
  <!-- leave a comment -->
  <test><this>now</this></test>
</parent>
</parent>

I hope this is helpful. You can find the code in a gist and as online demo / PHP version overview.

like image 103
hakre Avatar answered Nov 15 '22 20:11

hakre


I know this isn't the most helpful answer, but especially since you're creating/modifying XML, I'd switch over to using the DOM functions. SimpleXML's good for accessing simple documents, but pretty poor at changing them.

If SimpleXML is treating you kindly in all other places and you want to stick with it, you still have the option of jumping over to the DOM functions temporarily to perform what you need to and then jump back again, using dom_import_simplexml() and simplexml_import_dom(). I'm not sure how efficient this is, but it might help you out.

like image 27
nickf Avatar answered Nov 15 '22 22:11

nickf