使用SimpleXML编写XML文件时如何使用命名空间
I’m writing a Google products RSS feed with SimpleXML in PHP. I’ve got my products coming from the database and creating the RSS file fine, but having problems when it comes to namespaces.
I’ve Googled and search Stack Overflow and come across dozens of posts of how to parse XML feeds containing namespaces, but my issue is actually authoring an XML file with a namespace.
Here is what the file should look like:
<?xml version="1.0" encoding="UTF-8" ?>
<rss version ="2.0" xmlns:g="http://base.google.com/ns/1.0">
<!-- content -->
</rss>
And here is my code:
<?php
$xml = new SimpleXMLElement('<rss></rss>');
$xml->addAttribute('version', '2.0');
$xml->addChild('channel');
$xml->channel->addChild('title', 'Removed');
$xml->channel->addChild('description', 'Removed');
$xml->channel->addChild('link', 'Removed');
foreach ($products as $product) {
$item = $xml->channel->addChild('item');
$item->addChild('title', htmlspecialchars($product['title']));
$item->addChild('description', htmlspecialchars($product['title']));
$item->addChild('link', $product['url']);
$item->addChild('id', $product['product_id']);
$item->addChild('price', $product['price_latest']);
$item->addChild('brand', $product['range']);
$item->addChild('condition', 'new');
$item->addChild('image_link', $product['image']);
}
How do I introduce the g
namespace, both the xmlns
declaration in the root rss
element, and then as a prefix for id
, price
, brand
, condition
and image_link
in each item
element?
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();
This can been done using the SimpleXMLElement interface. IMHO this is a hack of the dodgiest order, but it works for now.
The key point being that it works as follows, for now, but may not continue to work. As such I'm in no way recommending this over the accepted answer by @DaveRandom. Rather, I'm including this answer here so that others in future can read this and save themselves some time searching for a SimpleXMLElement way of doing it, and just go with DaveRandom's DOM based approach :-)
You can "trick" the parser to stop it from dropping your g:
namespace prefix from your element names, by putting a "rubbish" prefix prior to your real one, eg "blah:g:condition"
.
I've seen variations of this answer for use in attribute prefixes, but not for element prefixes. And those all seem to suggest using "xmlns:yourPrefix:yourAttribute"
and passing in the fully qualified names space as the third parameter, where in reality (at least, from my own personal testing) the xmlns:
part can be pretty much anything (including whitespace!) prior to a colon :
, but there has to be something prior to that first colon, ie ":g:condition"
won't work. And unless you actually create a node that declares the namespace and prefix, the rendered XML would be invalid (ie the namespace prefix you hack in against your elements will have no declaration).
So, based on your original code, you'd do the following. Note also the explicit addition of the namespace in the root node declaration (although this can probably be done via the API - but why bother?).
$xml = new SimpleXMLElement('<rss xmlns:g="http://base.google.com/ns/1.0" />'); // May as well chuck the google ns in the root element declaration here, while we're at it, rather than adding it via a separate attribute.
$xml->addAttribute('version', '2.0');
// $xml->addAttribute('hack:xmlns:g','http://base.google.com/ns/1.0'); //Or could do this instead...
$xml->addChild('channel');
$xml->channel->addChild('title', 'Removed');
$xml->channel->addChild('description', 'Removed');
$xml->channel->addChild('link', 'Removed');
foreach ($products as $product) {
$item = $xml->channel->addChild('item');
$item->addChild('title', htmlspecialchars($product['title']));
$item->addChild('description', htmlspecialchars($product['title']));
$item->addChild('link', $product['url']);
$item->addChild('hack:g:id', $product['product_id']);
$item->addChild('hack:g:price', $product['price_latest']);
$item->addChild('hack:g:brand', $product['range']);
$item->addChild('hack:g:condition', 'new');
$item->addChild('hack:g:image_link', $product['image']);
}
This can be done by adding the namespace declaration into the root element creation along with adding the namespace to the addChild
function.
The modification to the original code:
$products[] = array(
"title" => "Foobar",
"description" => "Foo",
"url" => "https://f.oo.bar",
"product_id" => "00001",
"price_latest" => "$3.50",
"range" => "Foo",
"image" => "https://f.oo.bar/image.tiff",
);
$xml = new SimpleXMLElement('<rss xmlns:g="http://base.google.com/ns/1.0"/>');
$xml->addAttribute('version', '2.0');
$xml->addChild('channel');
$xml->channel->addChild('title', 'Removed');
$xml->channel->addChild('description', 'Removed');
$xml->channel->addChild('link', 'Removed');
foreach ($products as $product) {
$item = $xml->channel->addChild('item');
$item->addChild('title', htmlspecialchars($product['title']));
$item->addChild('description', htmlspecialchars($product['title']));
$item->addChild('link', $product['url']);
$item->addChild('id', $product['product_id'], "http://base.google.com/ns/1.0");
$item->addChild('price', $product['price_latest'], "http://base.google.com/ns/1.0");
$item->addChild('brand', $product['range'], "http://base.google.com/ns/1.0");
$item->addChild('condition', 'new', "http://base.google.com/ns/1.0");
$item->addChild('image_link', $product['image'], "http://base.google.com/ns/1.0");
}
print_r($xml->asXML());
Would yield the response:
<?xml version="1.0"?>
<rss xmlns:g="http://base.google.com/ns/1.0" version="2.0">
<channel>
<title>Removed</title>
<description>Removed</description>
<link>Removed</link>
<item>
<title>Foobar</title>
<description>Foobar</description><link/>
<g:id>00001</g:id>
<g:price>$3.50</g:price>
<g:brand>Foo</g:brand>
<g:condition>new</g:condition>
<g:image_link>https://f.oo.bar/image.tiff</g:image_link>
</item>
</channel>
</rss>