使用XML模式定义和错误处理进行PHP XML文件验证

在PHP中使用XML模式定义验证XML文件

XML是一种实用的格式,广泛用于配置、数据存储以及不同系统间的数据传输。它既可以被人类轻松读取,又能被机器快速解析,因此迅速成为众多系统中数据存储的首选方式。

许多系统在用户毫无察觉的情况下默默地使用着XML。比如API系统SOAP,就是基于XML数据构建的,通常我们可以要求API端点以JSON或XML格式进行响应。由于XML文件易于编辑和按需解析,所以常被用于系统配置。

XML模式定义(XSD)由万维网联盟(W3C)于2001年首次发布,它是众多XML模式格式中的一种。借助XSD,我们可以快速判断一个XML文件是否有效,还能深入了解其无效的原因,从而避免在系统中使用硬编码逻辑来验证XML文档。

在本文中,我公司将学习如何使用PHP和XSD文档来验证XML文档,以及如何正确呈现错误输出。在实际的Drupal开发Drupal模块开发甚至Drupal升级到Drupal11等场景中,这种验证XML文件的方法也可能会发挥重要的作用。

一、XML和XSD文件

下面来看一个非常简单的XSD文档,它可能会用于存储公司地址信息的系统。

<?xml version="1.0" encoding="utf-8"?>
<xsd:schema attributeFormDefault="unqualified" elementFormDefault="qualified" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <xsd:element name="company">
    <xsd:complexType>
      <xsd:sequence>
        <xsd:element type="xsd:string" name="company_email" minOccurs="1" />
        <xsd:element type="xsd:string" name="company_name" minOccurs="1" />
        <xsd:element type="xsd:string" name="company_address1" minOccurs="1" />
        <xsd:element type="xsd:string" name="company_postcode" minOccurs="1" />
        <xsd:element type="xsd:string" name="company_tel" minOccurs="1" />
      </xsd:sequence>
    </xsd:complexType>
  </xsd:element>
</xsd:schema>

我们可能会接收到如下的XML文件。

<?xml version="1.0" encoding="utf-8"?>
<company>
  <company_email>firstnane@example.com</company_email>
  <company_name>example</company_name>
  <company_address1>1 Testing Street</company_address1>
  <company_postcode>T35 7ER</company_postcode>
  <company_tel>0123456789</company_tel>
</company>

二、验证

要验证这个XML文档,我们首先需要将它加载到内存中。可以使用PHP内置的DOMDocument对象来完成这个操作。load()方法可以从指定文件名的文件中加载XML文件。

$xml = new DOMDocument();
$xmlDoc = 'company.xml';
$xml->load($xmlDoc, LIBXML_NOBLANKS);

我们也可以使用loadXML()方法从字符串中加载XML。如果通过API调用或其他方式接收XML,这种方法可能会更加实用。

$xml = new DOMDocument();
$xmlDoc = 'company.xml';
$xmlString = file_get_contents($xmlDoc);
$xml->loadXML($xmlString, LIBXML_NOBLANKS);

完成加载后,我们就可以使用DOMDocument对象的schemaValidate()方法验证XML了。该方法以模式文件名作为参数,如果提供的模式与我们加载的XML文档匹配,就会返回true。

$xmlSchema = 'company.xsd';
if (!$xml->schemaValidate($xmlSchema)) {
  print 'XML file is invalid';
}

或者,我们可以使用schemaValidateSource()方法从字符串中加载模式定义。

$xmlSchema = 'company.xsd';
$xmlString = file_get_contents($xmlDoc);
if (!$xml->schemaValidateSource($xmlSchema)) {
  print 'XML file is invalid';
}

这些方法能够告诉我们模式是否有效,但我们还可以进一步了解模式无效的原因。

三、打印模式验证消息

如果模式无效,我们可以使用libxml_get_errors()函数找出原因。该函数返回一个LibXMLError对象数组,我们可以用自定义函数打印出验证错误的相关信息。打印完错误信息后,可以使用libxml_clear_errors()函数清除现有的错误缓存。

$errors = libxml_get_errors();
foreach ($errors as $error) {
  print libxml_render_error($error, $xml) . PHP_EOL;
}
libxml_clear_errors();

libxml_render_error()是一个自定义函数,它接受一个LibXMLError对象和一个DOMDocument对象,然后尽可能多地打印出问题的相关信息。下面是该函数的完整代码。

function libxml_render_error(LibXMLError $error, DOMDocument $domDocument) {
  $domDocument->formatOutput = true;
  $lines = explode("\n", $domDocument->saveXML());

  $return = '';

  if ($error->line >= 1 && isset($lines[$error->line])) {
    $return .= (isset($lines[$error->line - 2]) ? $error->line -2  . ':' . $lines[$error->line - 2] . PHP_EOL : '');
    $return .= (isset($lines[$error->line - 1]) ? $error->line -1  . ':' . $lines[$error->line - 1] . PHP_EOL : '');
    $return .= $error->line . ':' . $lines[$error->line] . PHP_EOL;
    if ($error->column === 0) {
      $return .= str_pad('', strlen(trim($lines[$error->line - 1])) - 1, '-') . '^' . PHP_EOL;
    } else {
      $return .= str_pad('', $error->column, '-') . '^' . PHP_EOL;
    }
  }

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

  if ($error->file) {
    $return .= ' in ' . basename($error->file);
  }

  $return .= ' Line: ' . $error->line . ' Column: ' . $error->column;

  return $return . PHP_EOL;
}

为了说明这个功能,我们将原始XML文件中的company_address1改为company_address,这种情况在使用XML格式时可能会出现。

  <company_address>1 Testing Street</company_address>

现在,当我们尝试验证XML文档时,会得到如下输出。

3:  <company_name>example</company_name>
4:  <company_address>1 Testing Street</company_address>
5:  <company_postcode>T35 7ER</company_postcode>
--------------------------------------------------^
Error 1871: Element 'company_address': This element is not expected. Expected is ( company_address1 ). in xml_validation Line: 5 Column: 0

如果XML模式验证存在多个错误,它们将按顺序打印出来。

四、LIBXML_SCHEMA_CREATE标志

schemaValidate()和schemaValidateSource()方法的第二个参数是一个名为LIBXML_SCHEMA_CREATE的标志。目前这是该方法唯一接受的标志,可用于在验证步骤中将默认值注入DOM对象。

为了实现这一点,我们需要修改原始的XSD文档,添加一个默认属性。这里我们将company_tel元素的默认属性设置为"0123"。

<xsd:element type="xsd:string" name="company_tel" minOccurs="1" default="0123" />

现在我们稍微修改一下XML文档,让company_tel元素存在但值为空。

  <company_tel />

然后我们验证文档(验证会通过),如果XML有效,就打印出格式化后的XML文档。

$xml = new DOMDocument();

$xmlDoc = 'company.xml';
$xmlSchema = 'company.xsd';

$xml->load($xmlDoc, LIBXML_NOBLANKS);
if ($xml->schemaValidate($xmlSchema, LIBXML_SCHEMA_CREATE)) {
  $xml->formatOutput = true;
  print $xml->saveXML() . PHP_EOL;
}

验证完成后,我们会发现company_tel元素的默认值"0123"已被添加到文档中。

<?xml version="1.0" encoding="utf-8"?>
<company>
  <company_email>firstnane@example.com</company_email>
  <company_name>example</company_name>
  <company_address1>1 Testing Street</company_address1>
  <company_postcode>T35 7ER</company_postcode>
  <company_tel>0123</company_tel>
</company>

这个值在DOMDocument对象中也可以获取到。

print $xml->getElementsByTagName('company_tel')->item(0)->nodeValue; 

这种技术可用于在XML文档中强制设置默认值,而无需在代码库中添加自定义逻辑。在Drupal开发Drupal模块开发中,这种设置默认值的方式也能提高开发效率。

五、结论

如果要接收XML文件,在使用之前对其进行验证是个很好的做法,这样可以确保文件的有效性。还可以使用LIBXML_SCHEMA_CREATE标志向XML中注入默认值,而无需编写自定义逻辑。

尽管近年来XML的受欢迎程度不如JSON和YAML等格式,但仍有许多系统在使用这种格式,甚至允许以这种格式返回数据。拥有一个验证处理程序来确保XML完全有效,有助于保护系统免受错误影响。在Drupal开发Drupal模块开发以及Drupal升级到Drupal11的过程中,保证XML文件的有效性同样重要。

目前,PHP库仅支持XSD 1.0版本。XSD规范的1.1版本引入了一些结构和更复杂的数据类型,虽然这些很有用,但在验证XML文档时并非总是必需的。