user1325159
user1325159

Reputation: 187

How do I dynamically create a PHP SimpleXMLElement Object while keeping current properties?

I am reading in a an xml file which returns me a SimpleXMLElement Object representation of the xml. I am going to take an array and feed in new values to that object. I don't know what I am going to be in that array.

if I were to brute force this I would do something like this.

//Solution 1: Brute Force

    //Just creating an array and value for purposes of demonstration.
    $arOfData = array( [0]=>"theFirstNode", [1]=>"theSecondNode",[2]=>"theThirdNode" );
    $value = "The XML Node Value";

    $simpleXml->$arOfData[0]->$arOfData[1]->$arOfData[2] = $value;

    //The next best thing I can think of doing is something like this.
    //Solution 2: Semi-brute force
    //
    foreach($this->arrayData as $key => $value) {
                $xmlNodes = explode( '-', $key);
                $numNodes = count($xmlNodes);
                switch($numNodes) {
                    case 1:
                        $simpleXml->$xmlNodes[0] = $value;
                        break;
                    case 2:
                        $simpleXml->$xmlNodes[0]->$xmlNodes[1] = $value;
                        break;
                    case 3:
                        $simpleXml->$xmlNodes[0]->$xmlNodes[1]->$xmlNodes[2] = $value;
                        break;
                    case 4:
                        $simpleXml->$xmlNodes[0]->$xmlNodes[1]->$xmlNodes[2]->$xmlNodes[3] = $value;
                        break;
                    case 5:
                        $simpleXml->$xmlNodes[0]->$xmlNodes[1]->$xmlNodes[2]->$xmlNodes[3]->$xmlNodes[4] = $value;
                        break;
                }
            }

*note This solution uses the array key and explodes it to an array delimited by a dash and then uses the array value as the new xml value. So don't let that distract you.

The problem with solution #2 is: what happens when we get a xml node that is deeper than 5? Its not going to be stuffed into our new object we are creating. Oh oh. It's also not very elegant ;). I am not sure how to do this in a more recursive manner.

Upvotes: 1

Views: 1205

Answers (1)

M8R-1jmw5r
M8R-1jmw5r

Reputation: 4996

Like you already wrote in your question, you need to have this dynamically because you do not know about the number of parent elements.

You need to dig a little deeper into how simpexml works to get this done.

But first let me suggest you to have a different notation, not with the minus sign you have but with a slash like in a path.

first/second/third

This is also common with Xpath and I think it's pretty well speaking for itself. Also the minus sign can be part of an element name, but the slash can not. So this is just a bit better.

Before I show you how you can easily access that <third> element node to set its value, first lets look at some assignment basics in simplexml.

To access and set this element-node in a SimpleXMLElement see the following example:

$xml = new SimpleXMLElement('<root><first><second><third/></second></first></root>');    

$element = $xml->first->second->third;
$element[0] = "value";

This is pretty straight forward but you can see two things here:

  1. The <third> element already exists in the document.
  2. The code uses as simplexml-self-reference ([0]) which allows to set the XML value of the element variable (and not the variable). This is specific to how SimpleXMLElement works.

The second point also contains the solution to the problem how to deal with non-existent elements. $element[0] is NULL in case the element does not exists:

$xml = new SimpleXMLElement('<root><first><second/></first></root>');

$element = $xml->first->second->third;
var_dump($element[0]); # NULL

So let's try to conditionally add the third element in case it does not exists:

if ($xml->first->second->third[0] === NULL) {
    $xml->first->second->third = "";
}

This does solve that problem. So the only thing left to do is to do that in an iterative fashion for all parts of the path:

first/second/third

To keep this easy, create a function for this:

/**
 * Modify an elements value specified by a string-path.
 *
 * @param SimpleXMLElement $parent
 * @param string           $path
 * @param string           $value (optional)
 *
 * @return SimpleXMLElement the modified element-node
 */
function simplexml_deep_set(SimpleXMLElement $parent, $path, $value = '') 
{
    ### <mocked> to be removed later: ###

    if ($parent->first->second->third[0] === NULL) {
        $parent->first->second->third = "";
    }

    $element = $parent->first->second->third;

    ### </mocked> ###

    $element[0] = $value;

    return $element;
}

Because the function is mocked, it can be used directly:

$xml = new SimpleXMLElement('<root><first><second/></first></root>');

simplexml_deep_set($xml, "first/second/third", "The XML Node Value");

$xml->asXML('php://output');

And this works:

<?xml version="1.0"?>
<root><first><second><third>The XML Node Value</third></second></first></root>

So now removing the mock. First insert the explode like you have it as well. Then all that needs to be done is to go along each step of the path and create the element conditionally if it yet does not exist. In the end $element will be the element to modify:

$steps   = explode('/', $path);

$element = $parent;
foreach ($steps as $step)
{
    if ($element->{$step}[0] === NULL) {
        $element->$step = '';
    }

    $element = $element->$step;
}

This foreach is needed to replace the mock with a working version. Compare with the full function definition at a glance:

function simplexml_deep_set(SimpleXMLElement $parent, $path, $value = '')
{
    $steps   = explode('/', $path);

    $element = $parent;
    foreach ($steps as $step)
    {
        if ($element->{$step}[0] === NULL) {
            $element->$step = "";
        }

        $element = $element->$step;
    }

    $element[0] = $value;

    return $element;
}

Lets modify more crazy things to test it out:

$xml = new SimpleXMLElement('<root><first><second/></first></root>');

simplexml_deep_set($xml, "first/second/third", "The XML Node Value");

simplexml_deep_set(
    $xml, "How/do/I/dynamically/create/a/php/simplexml/object/while/keeping/current/properties"
    , "The other XML Node Value"
);

$xml->asXML('php://output');

Example-Output (beautified):

<?xml version="1.0"?>
<root>
  <first>
    <second>
      <third>The XML Node Value</third>
    </second>
  </first>
  <How>
    <do>
      <I>
        <dynamically>
          <create>
            <a>
              <php>
                <simplexml>
                  <object>
                    <while>
                      <keeping>
                        <current>
                          <properties>The other XML Node Value</properties>
                        </current>
                      </keeping>
                    </while>
                  </object>
                </simplexml>
              </php>
            </a>
          </create>
        </dynamically>
      </I>
    </do>
  </How>
</root>

See it in action.

Upvotes: 1

Related Questions