PHP SimpleXML xpath在返回数据时不保留命名空间

时间:2015-02-14 22:24:13

标签: php xpath simplexml

我正在尝试注册一个命名空间,但每次我使用xpath返回的值时,我必须一次又一次地注册相同的命名空间。

<?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;

解析器:

         $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));

        }

?>

请注意,我必须在循环内再次注册命名空间,如果我没有,我将收到以下错误:

  

// PHP警告:SimpleXMLElement :: xpath():未定义的名称空间前缀   在...

您是否知道如何在xpath函数中保留已返回的simplexml对象中的已注册命名空间。

1 个答案:

答案 0 :(得分:4)

是的,你的示例是正确的,没有再次注册xpath命名空间会产生如下所示的警告,然后是另一个警告,导致结果为空:

  

警告:SimpleXMLElement :: xpath():未定义的名称空间前缀

     

警告:SimpleXMLElement :: xpath():xmlXPathEval:评估失败

评论中给出的解释并不太远,但是它们没有提供有助于回答你问题的好解释。

首先,文档不正确。从技术上讲,它不仅适用于下一个::xpath()调用:

$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");

这不会发出警告,尽管它不仅是下一个,而是另外三个电话。因此,评论中的描述可能更适合与对象相关。

一种解决方案是从 SimpleXMLElement 扩展并干扰命名空间注册,以便在执行xpath查询时,所有结果元素也可以获取注册的命名空间前缀。但这将是很多工作,并且当您访问结果的更多孩子时将无法工作。

此外,您无法分配数组或对象来存储 SimpleXMLElement 中的数据,它总是会创建新的元素节点,然后会错误地支持对象/数组。

避免这种情况的一种方法是不存储在 SimpleXMLElement 内,而是存储在 DOM 中,可以通过dom_import_simplexml访问。

因此,如果您创建 DOMXpath ,则可以使用它注册名称空间。如果您将实例存储在所有者文档中,则可以通过以下任何 SimpleXMLElement 访问xpath对象:

dom_import_simplexml($xml)->ownerDocument-> /** your named field here **/

为此,需要循环引用。我在 The SimpleXMLElement Magic Wonder World in PHP 中概述了这一点,并且一个易于访问的封装变体可能如下所示:

/**
 * 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;
    }

    ...

此类构造函数负责 DOMXPath 实例可用,并根据ctor中传递的 SimpleXMLElement 设置私有属性。

静态创建者功能允许以后轻松访问:

    /**
     * @param SimpleXMLElement $xml
     *
     * @return SimpleXpath
     */
    public static function of(SimpleXMLElement $xml)
    {
        $self = new self($xml);
        return $self;
    }

SimpleXpath 现在在实例化时始终具有xpath对象和simplexml对象。所以它只需要包含 DOMXpath 所有的方法并将返回的节点转换回simplexml以使其兼容。下面是一个示例,介绍如何将 DOMNodeList 转换为原始类的 SimpleXMLElements 数组,这是任何SimpleXMLElement::xpath()调用的行为:

    ...

    /**
     * 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;
    }

    ...

实际注册xpath命名空间更直接,因为它以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);
    }

    ...

将这些实现放在首位,剩下的就是从 SimpleXMLElement 扩展并使用新创建的 SimpleXpath 类连接它:

/**
 * 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);
    }
}

如果您使用该子类,则通过此修改,它可以透明地与您的示例一起使用:

/** @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));

}

此示例随后无误运行,请参见此处:https://eval.in/398767

完整的代码也是一个要点:https://gist.github.com/hakre/1d9e555ac1ebb1fc4ea8