首页 > 解决方案 > 对 XPath 查询使用 XSD 模式验证

问题描述

我正在使用以下代码创建一个 DOMDocument 并针对外部 xsd 文件对其进行验证。

<?php

$xmlPath = "/xml/some/file.xml";
$xsdPath = "/xsd/some/schema.xsd";
    
$doc = new \DOMDocument();
$doc->loadXML(file_get_contents($xmlPath), LIBXML_NOBLANKS);

if (!$doc>schemaValidate($xsdPath)) {
    throw new InvalidXmlFileException();
}

更新 2(重写的问题)

这很好用,这意味着如果 XML 与 XSD 的定义不匹配,它将抛出一个有意义的异常。

现在,我想使用 Xpath 从 DOMDocument 中检索信息。它也可以正常工作,但是,从这一点开始,DOMDocument 与 XSD 完全分离!例如,如果我有一个DOMNode ,我不知道它是simpleType类型还是complexType类型。我可以检查节点是否有子(hasChild())节点,但这不一样。此外,XSD 中还有大量信息(例如,最小和最大出现次数等)。

真正的问题是,我是否必须自己查询 XSD,或者是否有编程方式来询问这类问题。即这个DOMNode是复杂的还是简单的类型?

另一篇文章中,有人建议“使用真正的模式处理器处理模式,然后使用它的 API 来询问有关模式内容的问题”。XPath 是否具有用于检索 XSD 信息的 API,或者是否有与 DOMDocument 不同的便捷方式?

为了记录,原来的问题

现在,我想继续使用 XPath 解析来自 DOMDocument 的信息。为了提高我存储到数据库的数据的完整性并向客户端提供有意义的错误消息,我希望不断使用模式信息来验证查询。即,我想针对 xsd 中定义的允许子节点验证获取的子节点。我想通过在 xsd 文档上使用 XPath 来实现。

但是,我偶然发现了这篇文章。它基本上说这是你自己的一种古怪方式,你应该使用真正的模式处理器并使用它的 API 来进行查询。如果我理解正确,我使用的是真正的模式处理器schemaValidate,但是使用它的 API 意味着什么?

我有点猜到我没有以正确的方式使用模式,但我不知道如何研究正确的用法。

问题

如果我schemaValidate在 DOMDocument 上使用是一次性验证(真或假)还是与 DOMDocument 绑定的时间更长?准确地说,我是否可以使用验证也以某种方式添加节点,或者我可以使用它来选择我感兴趣的节点,如引用的 SO 帖子所建议的那样?

更新

这个问题被评为不清楚,所以我想再试一次。假设我想添加一个节点或编辑一个节点值。我可以使用 xsd 中提供的模式来验证用户输入吗?最初,为了做到这一点,我想使用另一个 XPath 实例手动查询 xsd 以获取某个节点的规范。但正如链接文章中所建议的,这不是最佳实践。所以问题是,DOM 库是否提供任何 API 来进行此类验证?

也许我想多了。也许我只是添加节点并再次运行验证,看看它在哪里/为什么会中断?在这种情况下,自定义错误处理的答案将是正确的。你确定吗?

标签: phpxmlxsddomdocumentxml-validation

解决方案


您的问题不是很清楚,但听起来您想获得有关任何架构验证失败的详细报告。虽然DomDocument::validateSchema() 只返回一个 boolean,但您可以使用内部libxml函数来获取一些更详细的信息。

我们可以从您的原始代码开始,只更改顶部的一件事:

<?php
// without this, errors are echoed directly to screen and/or log
libxml_use_internal_errors(true);
$xmlPath = "file.xml";
$xsdPath = "schema.xsd";

$doc = new \DOMDocument();
$doc->loadXML(file_get_contents($xmlPath), LIBXML_NOBLANKS);

if (!$doc->schemaValidate($xsdPath)) {
    throw new InvalidXmlFileException();
}

然后我们可以在异常中发生有趣的事情,这可能(根据您提供的代码)在代码中更高的地方被捕获。

<?php

class InvalidXmlFileException extends \Exception
{
    private $errors = [];

    public function __construct()
    {
        foreach (libxml_get_errors() as $err) {
            $this->errors[] = self::formatXmlError($err);
        }
        libxml_clear_errors();
    }

    /**
     * Return an array of error messages
     *
     * @return array
     */
    public function getXmlErrors(): array
    {
        return $this->errors;
    }

    /**
     * Return a human-readable error message from a libxml error object
     *
     * @return string
     */
    private static function formatXmlError(\LibXMLError $error): string
    {
        $return = "";
        switch ($error->level) {
        case \LIBXML_ERR_WARNING:
            $return .= "Warning $error->code: ";
            break;
         case \LIBXML_ERR_ERROR:
            $return .= "Error $error->code: ";
            break;
        case \LIBXML_ERR_FATAL:
            $return .= "Fatal Error $error->code: ";
            break;
        }

        $return .= trim($error->message) .
               "\n  Line: $error->line" .
               "\n  Column: $error->column";

        if ($error->file) {
            $return .= "\n  File: $error->file";
        }

        return $return;
    }
}

所以现在当你捕捉到你的异常时,你可以迭代$e->getXmlErrors()

try {
    // do stuff
} catch (InvalidXmlFileException $e) {
    foreach ($e->getXmlErrors() as $err) {
        echo "$err\n";
    }
}

对于该函数,我刚刚从PHP 文档formatXmlError中复制了一个示例,该示例将错误解析为人类可读的内容,但没有理由您不能返回一些结构化数据或您喜欢的任何内容。


推荐阅读