Search code examples
phpxmldomsoapsimplexml

How do you access SimpleXML nodes in a namespace environment?


How do you access PHP SimpleXML nodes in a namespace environment?

I want to pragmatically add <param>value</param> under <request>. Not just edit it in a string.

$xml = \SimpleXMLElement(
    '<?xml version="1.0" encoding="utf-8"?>' . PHP_EOL
  . '<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">' . PHP_EOL
  . '  <soap:Body>' . PHP_EOL
  . '    <Function xmlns="https://webservices.sveaekonomi.se/webpay">' . PHP_EOL
  . '      <request />' . PHP_EOL
  . '    </Function>' . PHP_EOL
  . '  </soap:Body>' . PHP_EOL
  . '</soap:Envelope>'
);

I have attempted the following:

#1
$xml->Envelope->Body->Function->request->addChild('param', 'value');

#2
$xml->children('https://webservices.sveaekonomi.se/webpay')->request->addChild('param', 'value');

#3
$xml->registerXPathNamespace('swp', 'https://webservices.sveaekonomi.se/webpay');
$xml->xpath('/swp:request')->addChild('param', 'value');

Solution

  • You've made a few common mistakes here, so I'll go through each in turn.

    Let's start with the beginning of your first attempt:

    $xml->Envelope->Body->...
    

    SimpleXML doesn't have a separate object for the document, only the root element - in this case, Envelope. So you don't need to say ->Envelope, you're already there. Without namespaces being involved, you would write:

    $xml->Body->...
    

    However, the object doesn't automatically "select" the namespace of that element, so you have to either immediately call ->children(), or pass the namespace you want to pre-select into the constructor:

    $xml->children('http://schemas.xmlsoap.org/soap/envelope/')->Body->...
    // Or
    $xml = new SimpleXMLElement($xmlString, 0, false, 'http://schemas.xmlsoap.org/soap/envelope/');
    $xml->Body->...
    

    With that in mind, we get to:

    $xml->children('http://schemas.xmlsoap.org/soap/envelope/')->Body->Function->...
    

    This fails because Function isn't in the same namespace as Body. The way I like to think of it is that the ->children() method is a "switch namespace" method, so we switch to the right namespace and carry on:

    $xml->children('http://schemas.xmlsoap.org/soap/envelope/')->Body
        ->children('https://webservices.sveaekonomi.se/webpay')->Function->request
        ->addChild('param', 'value');
    

    This will work!


    Your second attempt makes a different mistake:

    $xml->children('https://webservices.sveaekonomi.se/webpay')->request->...
    

    The "children" method doesn't let you jump deep into the document - as its name suggests, it gives you the direct children of an element, not the grand-children, great grand-children, and so on. The $xml variable points to the "Envelope" node, and that doesn't have a child called "request".

    There isn't a built-in method for "any descendant of", except by using XPath...


    Although seemingly completely different, your third attempt actually fails for the same reason:

    $xml->registerXPathNamespace('swp', 'https://webservices.sveaekonomi.se/webpay');
    $xml->xpath('/swp:request')->...
    

    The / operator in XPath similarly means "children of"; there is an operator for "any descendant of", which is //, so you would have got nearly the right result with this:

    $xml->registerXPathNamespace('swp', 'https://webservices.sveaekonomi.se/webpay');
    $xml->xpath('//swp:request')->...
    

    That will fail for a slightly more subtle reason: the xpath() method of SimpleXML always returns an array, not an object, so you have to ask for the first element ([0]) of that array.

    So the working XPath code is this:

    $xml->registerXPathNamespace('swp', 'https://webservices.sveaekonomi.se/webpay');
    $xml->xpath('//swp:request')[0]->addChild('param', 'value');