This is what I am after
<!-- language: lang-xml -->
<ws:Test>
<ws:somename2>somevalue2</ws:somename2>
<ws:make>
<ws:model>foo</ws:model>
<ws:model>bar</ws:model>
</ws:make>
</ws:Test>
This is my current code
<!-- language: lang-php -->
$xmlTest = new SimpleXMLElement('<Test/>', 0, false, 'ws', true);
$xmlTest->addChild("ws:somename2", "somevalue2", 'http://microsoft.com/wsdl/types/');
$make = $xmlTest->addChild('ws:make', null, 'ws');
#$make->addAttribute('name','Ford');
$make->addChild('ws:model', 'foo', 'ws');
$make->addChild('ws:model', 'bar', 'ws');
header ("Content-Type:text/xml");
print_r($xmlTest->asXML());
but it outputs
<!-- language: lang-xml -->
<Test>
<ws:somename2>somevalue2</ws:somename2>
<ws:make>
<ws:model>foo</ws:model>
<ws:model>bar</ws:model>
</ws:make>
</Test>
As you can see the ws: is missing from Test
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.
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 model
s? 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.
$xmlTest = new \SimpleXMLElement('<ws:Test></ws:Test>', LIBXML_NOERROR, false, 'ws', true);
$xmlTest->addAttribute('xmlns:xmlns:ws', 'http://url.to.namespace');
$xmlTest->addChild("ws:somename2", "somevalue2", 'http://microsoft.com/wsdl/types/');
$make = $xmlTest->addChild('ws:make', null, 'ws');
#$make->addAttribute('name','Ford');
$make->addChild('ws:model', 'foo', 'ws');
$make->addChild('ws:model', 'bar', 'ws');