在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文档时并非总是必需的。


