了解 XML 扩展的更改

XML 1.0 版本于 1998 年作为万维网联盟(W3C)规范推出。XML 与超文本标记语言(HTML)有几分相似;不过,XML 的主要目的是提供一种机器和人类都能读懂的数据格式化方法。XML 至今仍被广泛使用的原因之一就是它易于理解,而且在表示树形结构数据方面表现出色。

PHP 提供了许多扩展功能,使您可以使用和生成 XML 文档。在 PHP 8 中,这些扩展有一些变化。在大多数情况下,这些变化都不大;但是,如果你想成为一个全面、知情的 PHP 开发人员,了解这些变化是很重要的。

首先让我们看看 XMLWriter 扩展的变化。

检查 XMLWriter 扩展差异

所有 XMLWriter 扩展程序函数现在都接受并返回 XMLWriter 对象,而不是资源。但是,如果您查看一下 XMLWriter 扩展的 PHP 官方文档,就会发现其中并没有提到过程函数。原因有二:首先,PHP 语言正在慢慢地从离散的过程函数转向面向对象编程(OOP)。

第二个原因是,XMLWriter 过程函数实际上只是 XMLWriter OOP 方法的封装器!例如,xmlwriter_open_memory()XMLWriter::openMemory() 的封装器,xmlwriter_text()XMLWriter::text() 的封装器,等等。

如果你真的想使用过程式编程技术来使用 XMLWriter 扩展,xmlwriter_open_memory() 会在 PHP 8 中创建一个 XMLWriter 实例,而不是一个资源。同样,所有 XMLWriter 扩展程序函数都是与 XMLWriter 实例而不是资源一起工作的。

本章中提到的任何扩展都会产生对象实例而不是资源,因此可能会出现向后兼容中断。使用 XMLWriter 过程函数和 is_resource() 来检查资源是否已创建就是这种中断的一个例子。我们在此不举例说明,因为问题和解决方案与上一节所述相同:使用 !empty() 代替 is_resource()

最佳做法是使用 XMLWriter 扩展的 OOP 应用程序编程接口(API),而不是过程式 API。幸运的是,OOP API 自 PHP 5.1 起就可用了。下面是一个 XML 示例文件,将在下一个示例中使用:

<?xml version="1.0" encoding="UTF-8"?>
<fruit>
    <item>Apple</item>
    <item>Banana</item>
</fruit>
xml

此处显示的示例在 PHP 7 和 PHP 8 中均可运行。本例的目的是使用 XMLWriter 扩展来构建前面显示的 XML 文档。下面是实现这一目的的步骤:

  1. 我们首先创建一个 XMLWriter 实例。然后,我们打开共享内存的连接,并初始化 XML 文档类型,如下所示:

    // //repo/ch07/php8_xml_writer.php
    $xml = new XMLWriter();
    $xml->openMemory();
    $xml->startDocument('1.0', 'UTF-8');
    php
  2. 接下来,我们使用 startElement() 对水果根节点进行初始化,并添加一个值为 Apple 的子节点项,如下所示:

    $xml->startElement('fruit');
    $xml->startElement('item');
    $xml->text('Apple');
    $xml->endElement();
    php
  3. 接下来,我们添加另一个值为 Banana 的子节点项目,如下所示:

    $xml->startElement('item');
    $xml->text('Banana');
    $xml->endElement();
    php
  4. 最后,我们关闭 fruit 根节点,结束 XML 文档。以下代码片段中的最后一条命令将显示当前的 XML 文档:

    $xml->endElement();
    $xml->endDocument();
    echo $xml->outputMemory();
    php

以下是在 PHP 7 中运行的示例程序的输出结果:

root@php8_tips_php7 [ /repo/ch07 ]# php php8_xml_writer.php
<?xml version="1.0" encoding="UTF-8"?>
<fruit><item>Apple</item><item>Banana</item></fruit>
bash

如您所见,所需的 XML 文档已经生成。如果我们在 PHP 8 中运行同样的程序,结果也是一样的(未显示)。

现在我们来看看 SimpleXML 扩展的变化。

处理 SimpleXML 扩展的更改

SimpleXML 扩展是面向对象的,被广泛使用。因此,了解 PHP 8 中对该扩展所做的几处重大修改至关重要。好消息是你不需要重写任何代码!更好的消息是,这些改动大大改进了 SimpleXML 扩展的功能。

从 PHP 8 开始,SimpleXMLElement 类现在实现了标准 PHP 库(SPL)的 RecursiveIterator 接口,并包含了 SimpleXMLIterator 类的功能。在 PHP 8 中,SimpleXMLIterator 现在是 SimpleXMLElement 的空扩展。如果考虑到 XML 通常用于表示复杂的树形结构数据,那么这一看似简单的更新就具有重大意义。

作为示例,请看一下温莎王朝家谱的部分视图,如下所示:

image 2023 11 22 16 25 05 265
Figure 1. Figure 7.1 – Example of complex tree-structured data

如果我们使用 XML 进行建模,文档可能会是这样的:

<?xml version="1.0" encoding="UTF-8"?>
<!-- /repo/ch07/tree.xml -->
<family>
    <branch name="Windsor">
        <descendent gender="M">George V</descendent>
        <spouse gender="F">Mary of Treck</spouse>
        <branch name="George V">
            <descendent gender="M">George VI</descendent>
            <spouse gender="F">Elizabeth Bowes-Lyon</spouse>
            <branch name="George VI">
                <descendent gender="F">Elizabeth II</descendent>
                <spouse gender="M">Prince Philip</spouse>
                <branch name="Elizabeth II">
                    <descendent gender="M">Prince Charles</descendent>
                    <spouse gender="F">Diana Spencer</spouse>
                    <spouse gender="F">Camilla Parker Bowles</spouse>
                    <branch name="Prince Charles">
                        <descendent gender="M">William</descendent>
                        <spouse gender="F">Kate Middleton</spouse>
                    </branch>
                    <!-- not all nodes are shown -->
                </branch>
            </branch>
        </branch>
    </branch>
</family>
xml

然后,我们开发代码来解析树。不过,在 PHP 8 之前的 PHP 版本中,我们需要定义一个递归函数才能解析整个树。为此,我们将遵循以下步骤:

  1. 我们先定义一个递归函数,显示后裔的姓名和配偶(如果有),如下代码所示。该函数还能识别后裔的性别,并检查是否有子女。如果后者为真,函数将调用自身:

    function recurse($branch) {
        foreach ($branch as $node) {
            echo $node->descendent;
            echo ($node->descendent['gender'] == 'F')
                ? ', daughter of '
                : ', son of ';
            echo $node['name'];
            if (empty($node->spouse)) echo "\n";
            else echo ", married to {$node->spouse}\n";
            if (!empty($node->branch))
                recurse($node->branch);
        }
    }
    php
  2. 然后,我们从外部 XML 文件创建一个 SimpleXMLElement 实例,并调用递归函数,如下所示:

    // //repo/ch07/php7_simple_xml.php
    $fn = __DIR__ . '/includes/tree.xml';
    $xml = simplexml_load_file($fn);
    recurse($xml);
    php

    该代码块在 PHP 7 和 PHP 8 中均可运行。 以下是在 PHP 7 中运行的输出结果:

    root@php8_tips_php7 [ /repo/ch07 ]#
    php php7_simple_xml.php
    George V, son of Windsor, married to Mary of Treck
    George VI, son of George V, married to Elizabeth BowesLyon
    Elizabeth II, daughter of George VI, married to Philip
    Prince Charles, son of Elizabeth II, married to Diana
    Spencer
    William, son of Prince Charles, married to Kate Middleton
    Harry, son of Prince Charles, married to Meghan Markle
    Princess Anne, daughter of Elizabeth II, married to
    M.Phillips
    Princess Margaret, daughter of George VI, married to
    A.Jones
    Edward VIII, son of George V, married to Wallis Simpson
    Princess Mary, daughter of George V, married to
    H.Lascelles
    Prince Henry, son of George V, married to Lady Alice
    Montegu
    Prince George, son of George V, married to Princess
    Marina
    Prince John, son of George V
    bash

    不过,在 PHP 8 中,由于 SimpleXMLElement 现在实现了 RecursiveIterator,因此产生相同结果的代码更加简单。

  3. 与前面的示例一样,我们从外部文件定义了一个 SimpleXMLElement 实例。不过,我们不需要定义递归函数,只需定义一个 RecursiveIteratorIterator 实例即可,如下所示:

    // //repo/ch07/php8_simple_xml.php
    $fn = __DIR__ . '/includes/tree.xml';
    $xml = simplexml_load_file($fn);
    $iter = new RecursiveIteratorIterator($xml,
        RecursiveIteratorIterator::SELF_FIRST);
    php
  4. 之后,我们只需要一个简单的 foreach() 循环,其内部逻辑与前面的示例相同。我们不需要检查分支节点是否存在,也不需要递归-- RecursiveIteratorIterator 实例已经解决了这个问题!下面是你需要的代码:

    foreach ($iter as $branch) {
        if (!empty($branch->descendent)) {
            echo $branch->descendent;
            echo ($branch->descendent['gender'] == 'F')
                ? ', daughter of '
                : ', son of ';
            echo $branch['name'];
            if (empty($branch->spouse)) echo "\n";
            else echo ", married to {$branch->spouse}\n";
        }
    }
    php

下面是在 PHP 8 中运行该代码示例的输出结果。如您所见,输出结果完全相同:

root@php8_tips_php8 [ /repo/ch07 ]# php php8_simple_xml.php
George V, son of Windsor, married to Mary of Treck
George VI, son of George V, married to Elizabeth Bowes-Lyon
Elizabeth II, daughter of George VI, married to Philip
Prince Charles, son of Elizabeth II, married to Diana Spencer
William, son of Prince Charles, married to Kate Middleton
Harry, son of Prince Charles, married to Meghan Markle
Princess Anne, daughter of Elizabeth II, married to M.Phillips
Princess Margaret, daughter of George VI, married to A.Jones
Edward VIII, son of George V, married to Wallis Simpson
Princess Mary, daughter of George V, married to H.Lascelles
Prince Henry, son of George V, married to Lady Alice Montegu
Prince George, son of George V, married to Princess Marina
Prince John, son of George V
bash

请注意,在使用 Docker 容器运行这些示例时,此处显示的输出已稍作修改,以适应页面宽度。

现在让我们来看看 XML 扩展的其他变化。

了解其他 XML 扩展更改

PHP 8 的其他 XML 扩展也有一些变化。在大多数情况下,这些改动都很小,不会造成严重的向后兼容代码中断。但是,如果我们不讨论这些额外的更改,那将是我们的失职。我们建议您仔细阅读本小节中的其余更改,以提高您的认识。使用这些 XML 扩展将使您有能力对 PHP 8 更新后出现不一致行为的应用程序代码进行故障排除。

libxml 扩展的变更

libxml 扩展利用 Expat C 库,提供各种 PHP XML 扩展( https://libexpat.github.io/ )使用的 XML 解析功能。

对服务器上安装的 libxml 版本有新的要求。运行 PHP 8 时的最低版本必须是 2.9.0(或以上)。这一更新要求的主要好处之一是增强了对 XML 外部实体 (XXE) 处理攻击的防护。

推荐的 libxml 最低版本默认禁止依赖 libxml 扩展加载外部 XML 实体的 PHP XML 扩展。这反过来又减少了为防范 XXE 攻击而采取昂贵而费时的额外步骤的必要性。

有关 XXE 攻击的更多信息,请使用以下链接访问开放式网络应用程序安全项目 (OWASP): https://owasp.org/www-community/vulnerabilities/XML_External_Entity_(XXE)_Processing

对 XMLReader 扩展的修改

XMLReader 扩展是对 XMLWriter 扩展的补充。XMLWriter 扩展用于生成 XML 文档,而 XMLReader 扩展则用于读取。

XMLReader::open()XMLReader::xml() 这两个方法现在被定义为静态方法。您仍然可以创建 XMLReader 实例,但如果您扩展了 XMLReader 并重载了这两个方法,请务必将它们声明为静态方法。

XMLParser 扩展的变更

XMLParser 扩展是 PHP 最古老的 XML 扩展之一。因此,它几乎完全由过程函数而不是类和方法组成。不过,在 PHP 8 中,该扩展遵循了产生对象而不是资源的趋势。因此,当运行 xml_parser_create()xml_parser_create_ns() 时,创建的是 XMLParser 实例而不是资源。

正如在涉及 is_resource() 的潜在代码断点部分所提到的,你需要做的就是用 !empty() 代替任何使用 is_resource() 的检查。资源到对象迁移的另一个副作用是使 xml_parser_free() 函数变得多余。要停用解析器,只需使用 XmlParser 对象即可。

现在,您已经了解了与 XML 扩展相关的变化,这将有助于您更有效地解析和管理 XML 数据。通过利用本节中提到的新特性,您可以编写出比 PHP 8 以前更高效、性能更好的代码。现在让我们来看看 mbstring 扩展。