1

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.

3
  • 1
    Well, since registerXPathNamespace is a method of SimpleXMLElement, and you are operating on a new element inside your loop each time, that seems only natural. Btw., the manual explicitly describes this method with “creates a prefix/ns context for the next XPath query”, and the first and only user comment from 4 years ago also states, “Looks like you have to use registerXPathNamespace for each node when using XPath.” Commented Feb 14, 2015 at 22:30
  • Thanks @CBroe , I saw that comment, but from the negative reputations he had , I supposed that he was wrong. Commented Feb 14, 2015 at 22:36
  • @Abdullah I would advise against paying too much attention to the comment votes on php.net. They can be utter nonsense. Bad suggestions usually have a deeply negative score (rightly) but good suggestions often also have negative scores. Commented Feb 14, 2015 at 22:46

1 Answer 1

6

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

Sign up to request clarification or add additional context in comments.

1 Comment

The technique described here is also outlined in a little different manner in PHP XPath. Convert complex XML to array.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.