PHP Simplexml Namespace Problem

PHP namespace simplexml problems

Usually, people use children().

$rss = simplexml_load_string(
'<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:moshtix="http://www.moshtix.com.au">
<channel>
<link>qweqwe</link>
<moshtix:genre>asdasd</moshtix:genre>
</channel>
</rss>'
);

foreach ($rss->channel as $channel)
{
echo 'link: ', $channel->link, "\n";
echo 'genre: ', $channel->children('moshtix', true)->genre, "\n";
}

PHP SimpleXML Namespace Problem

The <value> element is not in the same namespace as <cap:parameter>:

<cap:parameter>
<valueName>VTEC</valueName>
<value>/O.CON.KMPX.FL.W.0012.000000T0000Z-110517T1800Z/</value>
</cap:parameter>

So you have to call children() again.

Code (demo)

$feed = simplexml_load_file('http://alerts.weather.gov/cap/us.php?x=1');
foreach ($feed->entry as $entry){
printf(
"ID: %s\nVTEC: %s\n<hr>",
$entry->id,
$entry->children('cap', true)->parameter->children()->value
);
}

Resolve namespaces with SimpleXML regardless of structure or namespace

You want to use SimpleXMLElement to extract data from XML and convert it into an array.

This is generally possible but comes with some caveats. Before XML Namespaces your XML comes with CDATA. For XML to array conversion with Simplexml you need to convert CDATA to text when you load the XML string. This is done with the LIBXML_NOCDATA flag. Example:

$xml = simplexml_load_string($buffer, null, LIBXML_NOCDATA);
print_r($xml); // print_r shows how SimpleXMLElement does array conversion

This gives you the following output:

SimpleXMLElement Object
(
[@attributes] => Array
(
[version] => 2.0
)

[title] => Blah
[description] => Blah
)

As you can already see, there is no nice form to present the attributes in an array, therefore Simplexml by convention puts these into the @attributes key.

The other problem you have is to handle those multiple XML namespaces. In the previous example no specific namespace was used. That is the default namespace. When you convert a SimpleXMLElement to an array, the namespace of the SimpleXMLElement is used. As none was explicitly specified, the default namespace has been taken.

But if you specify a namespace when you create the array, that namespace is taken.

Example:

$xml = simplexml_load_string($buffer, null, LIBXML_NOCDATA, "http://base.google.com/ns/1.0");
print_r($xml);

This gives you the following output:

SimpleXMLElement Object
(
[id] => Blah
[product_type] => Blah
)

As you can see, this time the namespace that has been specified when the SimpleXMLElement was created is used in the array conversion: http://base.google.com/ns/1.0.

As you write you want to take all namespaces from the document into account, you need to obtain those first - including the default one:

$xml = simplexml_load_string($buffer, null, LIBXML_NOCDATA);
$namespaces = [null] + $xml->getDocNamespaces(true);

Then you can iterate over all namespaces and recursively merge them into the same array shown below:

$array = [];
foreach ($namespaces as $namespace) {
$xml = simplexml_load_string($buffer, null, LIBXML_NOCDATA, $namespace);
$array = array_merge_recursive($array, (array) $xml);
}
print_r($array);

This then finally should create and output the array of your choice:

Array
(
[@attributes] => Array
(
[version] => 2.0
)

[title] => Blah
[description] => Blah
[id] => Blah
[product_type] => Blah
)

As you can see, this is perfectly possible with SimpleXMLElement. However it's important you understand how SimpleXMLElement converts into an array (or serializes to JSON which does follow the same rules). To simulate the SimpleXMLElement-to-array conversion, you can make use of print_r for a quick output.

Note that not all XML constructs can be equally well converted into an array. That's not specifically a limitation of Simplexml but lies in the nature of which structures XML can represent and which structures an array can represent.

Therefore it is most often better to keep the XML inside an object like SimpleXMLElement (or DOMDocument) to access and deal with the data - and not with an array.

However it's perfectly fine to convert data into an array as long as you know what you do and you don't need to write much code to access members deeper down the tree in the structure. Otherwise SimpleXMLElement is to be favored over an array because it allows dedicated access not only to many of the XML feature but also querying like a database with the SimpleXMLElement::xpath method. You would need to write many lines of own code to access data inside the XML tree that comfortable on an array.

To get the best of both worlds, you can extend SimpleXMLElement for your specific conversion needs:

$buffer = <<<BUFFER
<?xml version="1.0" encoding="utf-8" ?>
<rss version="2.0" xmlns:g="http://base.google.com/ns/1.0">
...
<g:id><![CDATA[Blah]]></g:id>
<title><![CDATA[Blah]]></title>
<description><![CDATA[Blah]]></description>
<g:product_type><![CDATA[Blah]]></g:product_type>
</rss>
BUFFER;

$feed = new Feed($buffer, LIBXML_NOCDATA);
print_r($feed->toArray());

Which does output:

Array
(
[@attributes] => stdClass Object
(
[version] => 2.0
)

[title] => Blah
[description] => Blah
[id] => Blah
[product_type] => Blah
[@text] => ...
)

For the underlying implementation:

class Feed extends SimpleXMLElement implements JsonSerializable
{
public function jsonSerialize()
{
$array = array();

// json encode attributes if any.
if ($attributes = $this->attributes()) {
$array['@attributes'] = iterator_to_array($attributes);
}

$namespaces = [null] + $this->getDocNamespaces(true);
// json encode child elements if any. group on duplicate names as an array.
foreach ($namespaces as $namespace) {
foreach ($this->children($namespace) as $name => $element) {
if (isset($array[$name])) {
if (!is_array($array[$name])) {
$array[$name] = [$array[$name]];
}
$array[$name][] = $element;
} else {
$array[$name] = $element;
}
}
}

// json encode non-whitespace element simplexml text values.
$text = trim($this);
if (strlen($text)) {
if ($array) {
$array['@text'] = $text;
} else {
$array = $text;
}
}

// return empty elements as NULL (self-closing or empty tags)
if (!$array) {
$array = NULL;
}

return $array;
}

public function toArray() {
return (array) json_decode(json_encode($this));
}
}

Which is an adoption with namespaces of the Changing JSON Encoding Rules example given in SimpleXML and JSON Encode in PHP – Part III and End.

How to use namespaces when writing XML file with SimpleXML

Here is an example of how to do this using DOM:

<?php

$nsUrl = 'http://base.google.com/ns/1.0';

$doc = new DOMDocument('1.0', 'UTF-8');

$rootNode = $doc->appendChild($doc->createElement('rss'));
$rootNode->setAttribute('version', '2.0');
$rootNode->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:g', $nsUrl);

$channelNode = $rootNode->appendChild($doc->createElement('channel'));
$channelNode->appendChild($doc->createElement('title', 'Removed'));
$channelNode->appendChild($doc->createElement('description', 'Removed'));
$channelNode->appendChild($doc->createElement('link', 'Removed'));

foreach ($products as $product) {
$itemNode = $channelNode->appendChild($doc->createElement('item'));
$itemNode->appendChild($doc->createElement('title'))->appendChild($doc->createTextNode($product['title']));
$itemNode->appendChild($doc->createElement('description'))->appendChild($doc->createTextNode($product['title']));
$itemNode->appendChild($doc->createElement('link'))->appendChild($doc->createTextNode($product['url']));
$itemNode->appendChild($doc->createElement('g:id'))->appendChild($doc->createTextNode($product['product_id']));
$itemNode->appendChild($doc->createElement('g:price'))->appendChild($doc->createTextNode($product['price_latest']));
$itemNode->appendChild($doc->createElement('g:brand'))->appendChild($doc->createTextNode($product['range']));
$itemNode->appendChild($doc->createElement('g:condition'))->appendChild($doc->createTextNode('new'));
$itemNode->appendChild($doc->createElement('g:image_link'))->appendChild($doc->createTextNode($product['image']));
}

echo $doc->saveXML();

See it working

PHP - Namespace Shift in Child Node in SimpleXML Troubleshooting

In the line

$dmd_fields = $xml_report_abbrev_bb[0]->children('dmd', true);

This will mean that $dmd_fields will be a list of nodes, even though there is only one node in the list - so use $dmd_fields[0] to reference the <dmd:IndexEntry> element. As this is the IndexEntry element, just using this and the list off attributes for that element you can do...

echo '<br>dmd:IndexEntry is...'.$dmd_fields[0]->attributes()['indexKey'].'<br>';

Warning: SimpleXMLElement::__construct(): namespace error : Namespace prefix xhtml on link is not defined in

The message is telling you that your XML file is incorrectly formatted: there is no definition of what namespace the prefix "xhtml:" refers to.

There should be an attribute somewhere like xmlns:xhtml="...", similar to the one that is there for xmlns:image="..." which defines the "image:" prefix.

Without that definition, pretty much any parser will reject the document as invalid XML. If possible, you should raise this with whoever is generating the document, and get them to fix it; if not, you'll have to write some hacky code to manipulate it.



Related Topics



Leave a reply



Submit