adding a namespace when using SimpleXMLElement

Solution 1:

SimpleXML has an unusual quirk where the namespace prefixes are filtered from the root element. I'm not sure why it does this.

However, a workaround I've used has been to basically prefix the prefix, so that the parser only removes the first ones, and leaves the second

$xmlTest = new SimpleXMLElement('<xmlns:ws:Test></xmlns:ws:Test>', LIBXML_NOERROR, false, 'ws', true);
$xmlTest->addAttribute('xmlns:xmlns:ws', 'http://url.to.namespace');
$xmlTest->addAttribute('xmlns:xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');

This seems to work for me, though I'm interested to understand why SimpleXML does this exactly.

Solution 2:

The Problem

The problem with this code is in the very first line:

$xmlTest = new SimpleXMLElement('<Test/>', 0, false, 'ws', true);

Before doing anything else, let's output this as XML:

echo $xmlTest->asXML();

<?xml version="1.0"?>
<Test/>

That makes sense, we got out what we put in.

The manual is rather vague on what the $ns argument does, but in this case it is not doing anything useful. What it does is set the context for reading the XML, so that ->foo refers to <ws:foo> and ['bar'] refers to ws:bar="...". It doesn't do anything to change the structure of the XML itself.

Setting a Root Namespace

To set a namespace on the root element, we just have to include it in our string defining the root element:

$xmlTest = new SimpleXMLElement('<ws:Test xmlns:ws="http://microsoft.com/wsdl/types/" />');
echo $xmlTest->asXML();

<?xml version="1.0"?>
<ws:Test xmlns:ws="http://microsoft.com/wsdl/types/"/>

So far so good...

Setting Namespaces on Children

Next, let's sanity check what the code in the question actually outputs (I've added some whitespace to make it more readable):

<?xml version="1.0"?> 
<Test>
   <ws:somename2 xmlns:ws="http://microsoft.com/wsdl/types/">somevalue2</ws:somename2>
   <ws:make xmlns:ws="ws">
        <ws:model>foo</ws:model>
        <ws:model>bar</ws:model>
   </ws:make>
</Test>

OK, so this document contains two namespace declarations:

  • xmlns:ws="http://microsoft.com/wsdl/types/" on the somename2 element
  • xmlns:ws="ws" on the make element, which is then inherited by the model elements

What happens if we add our corrected root element?

<?xml version="1.0"?> 
<ws:Test xmlns:ws="http://microsoft.com/wsdl/types/">
    <ws:somename2>somevalue2</ws:somename2>
    <ws:make xmlns:ws="ws">
        <ws:model>foo</ws:model>
        <ws:model>bar</ws:model>
   </ws:make>
</ws:Test>

Cool, so the somename2 element now inherits its namespace definition from the root element, and doesn't re-declare it. But what's wrong with the make and models? Let's compare:

$xmlTest->addChild("ws:somename2", "somevalue2", 'http://microsoft.com/wsdl/types/');
$make = $xmlTest->addChild('ws:make', null, 'ws');

That third argument is supposed to be the namespace URI, not just the prefix. So when we gave it as 'ws', SimpleXML assumed we wanted to declare the actual namespace URI as ws, so added an xmlns attribute to do so.

What we actually wanted was all the elements to be in the same namespace:

$xmlTest = new SimpleXMLElement('<ws:Test xmlns:ws="http://microsoft.com/wsdl/types/" />');
$xmlTest->addChild("ws:somename2", "somevalue2", 'http://microsoft.com/wsdl/types/');
$make = $xmlTest->addChild('ws:make', null, 'http://microsoft.com/wsdl/types/');
#$make->addAttribute('name','Ford', 'http://microsoft.com/wsdl/types/');
$make->addChild('ws:model', 'foo', 'http://microsoft.com/wsdl/types/');
$make->addChild('ws:model', 'bar', 'http://microsoft.com/wsdl/types/');
echo $xmlTest->asXML();

<?xml version="1.0"?> 
<ws:Test xmlns:ws="http://microsoft.com/wsdl/types/">
    <ws:somename2>somevalue2</ws:somename2>
    <ws:make>
        <ws:model>foo</ws:model>
        <ws:model>bar</ws:model>
   </ws:make>
</ws:Test>

Great, we've got our desired output!

Tidying Up

But that code looks rather ugly, why do we have to repeat the URI everywhere? Well, as far as SimpleXML is concerned, there's not much choice: the same prefix can mean different things in different parts of the document, so we have to tell it what we want.

What we can do is tidy up our code using a variable or constant for the namespace URI, rather than writing it out in full each time:

define('XMLNS_WS', 'http://microsoft.com/wsdl/types/');

$xmlTest = new SimpleXMLElement('<ws:Test xmlns:ws="' . XMLNS_WS . '" />');
$xmlTest->addChild("ws:somename2", "somevalue2", XMLNS_WS);
$make = $xmlTest->addChild('ws:make', null, XMLNS_WS);
#$make->addAttribute('name','Ford', XMLNS_WS);
$make->addChild('ws:model', 'foo', XMLNS_WS);
$make->addChild('ws:model', 'bar', XMLNS_WS);

There's nothing special about the name XMLNS_WS here, and it could equally be a variable, a class constant, or a namespace constant. The code runs exactly the same, it's just a little easier on the eye.