I'm trying to register a namespace , but everytime I use th returned value from xpath , I have to register the same namespace again and again.
<?php
$xml= <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response>
<extension>
<xyz:form xmlns:xyz="urn:company">
<xyz:formErrorData>
<xyz:field name="field">
<xyz:error>REQUIRED</xyz:error>
<xyz:value>username</xyz:value>
</xyz:field>
</xyz:formErrorData>
</xyz:form>
</extension>
</response>
</epp>
XML;
The parser :
$xmlObject = simplexml_load_string(trim($xml), NULL, NULL);
$xmlObject->registerXPathNamespace('ns','urn:company');
$fields = $xmlObject->xpath("//ns:field");
foreach($fields as $field){
//PHP Warning: SimpleXMLElement::xpath(): Undefined namespace prefix in
//$errors = $field->xpath("//ns:error");
// I have to register the same namespace again so it works
$field->registerXPathNamespace('ns','urn:company');
$errors = $field->xpath("//ns:error"); // no issue
var_dump((string)current($errors));
}
?>
Notice that I had to register the namespace again inside the loop, if I did not I will get the following error :
//PHP Warning: SimpleXMLElement::xpath(): Undefined namespace prefix in...
Do you have any idea how to keep the registered namespaces in the returned simplexml objects from xpath function.
Yes you're right for your example, not registering the xpath namespace again would create a warning like the following then followed by another warning leading to an empty result:
Warning: SimpleXMLElement::xpath(): Undefined namespace prefix
Warning: SimpleXMLElement::xpath(): xmlXPathEval: evaluation failed
The explanations given in the comments aren't too far off, however they do not offer a good explanation that could help to answer your question.
First of all the documentation is not correct. It's technically not only for the next ::xpath()
invocation:
$xmlObject->registerXPathNamespace('ns', 'urn:company');
$fields = $xmlObject->xpath("//ns:field");
$fields = $xmlObject->xpath("//ns:field");
$fields = $xmlObject->xpath("//ns:field");
$fields = $xmlObject->xpath("//ns:field");
This does not give the warning despite it's not only the next, but another further three calls. So the description from the comment is perhaps more fitting that this is related to the object.
One solution would be to extend from SimpleXMLElement and interfere with the namespace registration so that when the xpath query is executed, all result elements could get the namespace prefix registered as well. But that would be much work and won't work when you would access further children of a result.
Additionally you can't assign arrays or objects to store the data within a SimpleXMLElement it would always create new element nodes and then error that objects / arrays are not supported.
One way to circumvent that is to store not inside the SimpleXMLElement but inside the DOM which is accessible via dom_import_simplexml
.
So, if you create a DOMXpath you can register namespaces with it. And if you store the instance inside the owner document, you can access the xpath object from any SimpleXMLElement via:
dom_import_simplexml($xml)->ownerDocument-> /** your named field here **/
For this to work, a circular reference is needed. I outlined this in The SimpleXMLElement Magic Wonder World in PHP and an encapsulated variant with easy access could look like:
/**
* Class SimpleXpath
*
* DOMXpath wrapper for SimpleXMLElement
*
* Allows assignment of one DOMXPath instance to the document of a SimpleXMLElement so that all nodes of that
* SimpleXMLElement have access to it.
*
* @link
*/
class SimpleXpath
{
/**
* @var DOMXPath
*/
private $xpath;
/**
* @var SimpleXMLElement
*/
private $xml;
...
/**
* @param SimpleXMLElement $xml
*/
public function __construct(SimpleXMLElement $xml)
{
$doc = dom_import_simplexml($xml)->ownerDocument;
if (!isset($doc->xpath)) {
$doc->xpath = new DOMXPath($doc);
$doc->circref = $doc;
}
$this->xpath = $doc->xpath;
$this->xml = $xml;
}
...
This class constructor takes care that the DOMXPath instance is available and sets the private properties according to the SimpleXMLElement passed in the ctor.
A static creator function allows easy access later:
/**
* @param SimpleXMLElement $xml
*
* @return SimpleXpath
*/
public static function of(SimpleXMLElement $xml)
{
$self = new self($xml);
return $self;
}
The SimpleXpath now always has the xpath object and the simplexml object when instantiated. So it only needs to wrap all the methods DOMXpath has and convert returned nodes back to simplexml to have this compatible. Here is an example on how to convert a DOMNodeList to an array of SimpleXMLElements of the original class which is the behavior of any SimpleXMLElement::xpath()
call:
...
/**
* Evaluates the given XPath expression
*
* @param string $expression The XPath expression to execute.
* @param DOMNode $contextnode [optional] <The optional contextnode
*
* @return array
*/
public function query($expression, SimpleXMLElement $contextnode = null)
{
return $this->back($this->xpath->query($expression, dom_import_simplexml($contextnode)));
}
/**
* back to SimpleXML (if applicable)
*
* @param $mixed
*
* @return array
*/
public function back($mixed)
{
if (!$mixed instanceof DOMNodeList) {
return $mixed; // technically not possible with std. SimpleXMLElement
}
$result = [];
$class = get_class($this->xml);
foreach ($mixed as $node) {
$result[] = simplexml_import_dom($node, $class);
}
return $result;
}
...
It's more straight forward for the actual registering of xpath namespaces because it works 1:1:
...
/**
* Registers the namespace with the DOMXPath object
*
* @param string $prefix The prefix.
* @param string $namespaceURI The URI of the namespace.
*
* @return bool true on success or false on failure.
*/
public function registerNamespace($prefix, $namespaceURI)
{
return $this->xpath->registerNamespace($prefix, $namespaceURI);
}
...
With these implementations in the chest, all what is left is to extend from SimpleXMLElement and wire it with the newly created SimpleXpath class:
/**
* Class SimpleXpathXMLElement
*/
class SimpleXpathXMLElement extends SimpleXMLElement
{
/**
* Creates a prefix/ns context for the next XPath query
*
* @param string $prefix The namespace prefix to use in the XPath query for the namespace given in ns.
* @param string $ns The namespace to use for the XPath query. This must match a namespace in use by the XML
* document or the XPath query using prefix will not return any results.
*
* @return bool TRUE on success or FALSE on failure.
*/
public function registerXPathNamespace($prefix, $ns)
{
return SimpleXpath::of($this)->registerNamespace($prefix, $ns);
}
/**
* Runs XPath query on XML data
*
* @param string $path An XPath path
*
* @return SimpleXMLElement[] an array of SimpleXMLElement objects or FALSE in case of an error.
*/
public function xpath($path)
{
return SimpleXpath::of($this)->query($path, $this);
}
}
With this modification under the hood, it works transparently with your example if you use that sub-class:
/** @var SimpleXpathXMLElement $xmlObject */
$xmlObject = simplexml_load_string($buffer, 'SimpleXpathXMLElement');
$xmlObject->registerXPathNamespace('ns', 'urn:company');
$fields = $xmlObject->xpath("//ns:field");
foreach ($fields as $field) {
$errors = $field->xpath("//ns:error"); // no issue
var_dump((string)current($errors));
}
This example then runs error free, see here: https://eval.in/398767
The full code is in a gist, too: https://gist.github.com/hakre/1d9e555ac1ebb1fc4ea8