PHP SimpleXML xpath 在 returns 数据时不保留名称空间

PHP SimpleXML xpath does not keep the namespaces when returns data

我正在尝试注册一个命名空间,但每次我使用 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 Warning: SimpleXMLElement::xpath(): Undefined namespace prefix in...

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

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

Warning: SimpleXMLElement::xpath(): Undefined namespace prefix

Warning: SimpleXMLElement::xpath(): xmlXPathEval: evaluation failed

评论中给出的解释相差无几,但是它们没有提供有助于回答您的问题的良好解释。

首先文档不正确。从技术上讲,它不仅适用于下一个 ::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;
    }

    ...

此 class 构造函数负责确保 DOMXPath 实例可用,并根据 SimpleXMLElement[ 设置私有属性=74=] 传入ctor.

静态创建者函数方便以后访问:

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

SimpleXpath 现在在实例化时始终具有 xpath 对象和 simplexml 对象。所以它只需要包装所有方法 DOMXpath 并将返回的节点转换回 simplexml 以具有此兼容性。这是一个关于如何将 DOMNodeList 转换为原始 class 的 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:

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

在后台进行此修改后,如果您使用该子 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));

}

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

完整代码也在要点中:https://gist.github.com/hakre/1d9e555ac1ebb1fc4ea8