My mate who sits next to me at work, Amar, was trying to extract some info from an XML document, and we stumbled over the xpath syntax when there was a namespace defined, but no prefix was given. I'm completely unused to using xpath in PHP (I've had to query something once, I think), but had done a fair bit back in my CFML days.
Here's the XML in question (well: it's not the same XML, but it's equivalent):
<Response xmlns="http://example.com/ns/">
<user>
<dateOfBirth>1947-01-08</dateOfBirth>
<firstName>Ziggy</firstName>
<lastName>Stardust</lastName>
<gender>?</gender>
</user>
</Response>
See we've got a namespace declaration but no bloody prefix defined. Grumble.
On CF9 namespaces could be kinda ignored: just not specifying the namespace at all:
raw = '<Response xmlns="http://example.com/ns/">
<user>
<dateOfBirth>1947-01-08</dateOfBirth>
<email>sailor@example.com</email>
<firstName>Ziggy</firstName>
<lastName>Stardust</lastName>
<gender>?</gender>
</user>
</Response>';
xml = xmlParse(raw);
usingEmptyNamespace = xmlSearch(xml, "/:Response/:user/:lastName");
writeDump(usingEmptyNamespace);
Via cflive.net this yields:
Cool. However at some point - it might have been CF10, but I don't know - taking this approach stopped working because ColdFusion changed its XML parsing engine and apparently empty namespaces like that aren't legal.
The solution I had discovered (via googling and Stack Overflow) was to use the
local-name()
xpath function:usingLocalName = xml.search("/*[local-name()='Response']/*[local-name()='user']/*[local-name()='firstName']");
writeDump(usingLocalName);
And this yields (I'm using ColdFusion 2016's CLI now), hence the format change:
>cf xpath.cfm
array
1) [xml element]
XmlName: firstName
XmlNsPrefix:
XmlNsURI: http://example.com/ns/
XmlText: Ziggy
XmlComment:
XmlAttributes: [struct]
XmlChildren:
>
array
1) [xml element]
XmlName: firstName
XmlNsPrefix:
XmlNsURI: http://example.com/ns/
XmlText: Ziggy
XmlComment:
XmlAttributes: [struct]
XmlChildren:
>
Now I switch to PHP, and have to make this lot work. Firstly the empty path version simply didn't work:
<?php
$raw = '<Response xmlns="http://example.com/ns/">
<user>
<dateOfBirth>1947-01-08</dateOfBirth>
<firstName>Ziggy</firstName>
<lastName>Stardust</lastName>
<gender>?</gender>
</user>
</Response>
';
$xml = new SimpleXMLElement($raw);
$usingEmptyNamespace = $xml->xpath("/:Response/:user/:gender");
var_dump($usingEmptyNamespace);
>php xpath.php
PHP Warning: SimpleXMLElement::xpath(): Invalid expression in xpath.php on line 15
Warning: SimpleXMLElement::xpath(): Invalid expression in xpath.php on line 15
bool(false)
>
PHP Warning: SimpleXMLElement::xpath(): Invalid expression in xpath.php on line 15
Warning: SimpleXMLElement::xpath(): Invalid expression in xpath.php on line 15
bool(false)
>
Using the
local-name()
approach worked fine in PHP:$usingLocalName = $xml->xpath("/*[local-name()='Response']/*[local-name()='user']/*[local-name()='firstName']");
var_dump($usingLocalName);
>php xpath.php
array(1) {
[0]=>
object(SimpleXMLElement)#2 (1) {
[0]=>
string(5) "Ziggy"
}
}
>
However I was certain PHP would do things better than that, and doing some reading, I see they have a way of resolving the lack of defined namespace, using the
registerXPathNamespace()
method:$xml->registerXPathNamespace('db', 'http://example.com/ns/');
$usingRegisteredNamespace = $xml->xpath("/db:Response/db:user/db:lastName");
var_dump($usingRegisteredNamespace);
This allows me to specify a prefix to make the xpath string legit. Nice one!
>php xpath.php
array(1) {
[0]=>
object(SimpleXMLElement)#2 (1) {
[0]=>
string(8) "Stardust"
}
}
>
array(1) {
[0]=>
object(SimpleXMLElement)#2 (1) {
[0]=>
string(8) "Stardust"
}
}
>
One last thing I tried on a whim in CFML which worked was that it seems one can specify a wildcard namespace:
usingWildcardNamespace = xml.search("/*:Response/*:user/*:firstName");
writeDump(usingWildcardNamespace);
>cf xpath.cfm
array
1) [xml element]
XmlName: firstName
XmlNsPrefix:
XmlNsURI: http://example.com/ns/
XmlText: Ziggy
XmlComment:
XmlAttributes: [struct]
XmlChildren:
>
array
1) [xml element]
XmlName: firstName
XmlNsPrefix:
XmlNsURI: http://example.com/ns/
XmlText: Ziggy
XmlComment:
XmlAttributes: [struct]
XmlChildren:
>
This did not work on PHP. I dunno enough about XML engines and parsing and searching to make a comment on the whys and wherefores of what one's expectations ought to be when it comes to this sort of stuff, but it's good to know about
registerXPathNamespace()
, and also good to know about the wildcard stuff with CFML.Not very incisive or groundbreaking stuff, but it's just what I had to work out today.
Righto.
--
Adam