17

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

Problematic
  • 17,567
  • 10
  • 73
  • 85
sleep-er
  • 440
  • 1
  • 4
  • 16

3 Answers3

35

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.

Y. E.
  • 687
  • 1
  • 10
  • 29
Kyril
  • 3,056
  • 1
  • 19
  • 13
  • So adding a prefix to the prefix fixed the issue :) – shasi kanth Dec 27 '13 at 13:48
  • Did you find any other solution than this workaround? – Hitesh Feb 15 '16 at 11:35
  • 2
    A more correct method can be found here, http://stackoverflow.com/questions/6808578/simple-xml-add-namespaced-child – Damian May 22 '16 at 03:16
  • Note that this adds the "namespaces" as attributes, not namespaces, though the result when serialized with [`SimpleXMLElement->asXML()`](http://php.net/SimpleXMLElement.asXML) is equivalent. "xmlns:" is part of the [local part](https://www.w3.org/TR/xml-names/#NT-LocalPart) of the attribute name, rather than a prefix. Note they appear in `$xmlTest->attributes()`, but not `$xmlTest->getDocNamespaces()`. You can also use any prefix, not just "xmlns". – outis Sep 14 '16 at 07:14
  • No namespaces are being "filtered from the root element" in the code in the question, the OP just didn't add any - their root element is ``. Specifying the namespace argument `'ws'` when *reading* that element won't magically cause the element to be in a namespace when written back out. – IMSoP Apr 22 '17 at 15:26
16

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.

IMSoP
  • 89,526
  • 13
  • 117
  • 169
  • Actually you don't need to repeat the same namespace to children elements, they will inherit the same namespace specified from parent element. – Gabplay Feb 19 '20 at 14:52
0
$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');
MeTaL
  • 21
  • 6
  • 1
    Please, add some explanation. – gofr1 May 14 '16 at 07:11
  • While this code may answer the question, it would be better to include some context, explaining how it works and when to use it. Code-only answers are not useful in the long run. – Bono May 14 '16 at 09:37