处理标准 PHP 库 (SPL) 更改

SPL 是一个扩展,包含实现基本数据结构和增强 OOP 功能的关键类。它在 PHP 5 中首次引入,现在默认包含在所有 PHP 安装中。涵盖整个 SPL 超出了本书的范围。在本节中,我们将讨论在运行 PHP 8 时 SPL 中发生重大变化的地方。此外,我们还将就可能导致现有应用程序停止工作的 SPL 变化提供提示和指导。

我们首先检查 SplFileObject 类的变化。

了解 SplFileObject 的更改

SplFileObject 是一个出色的类,它将大部分独立的 f*() 函数(如 fgets()fread()fwrite() 等)整合到一个类中。SplFileObject::__construct() 方法的参数与提供给 fopen() 函数的参数相同。

PHP 8 中的主要区别在于从 SplFileObject 类中删除了一个相对生僻的方法 fgetss()。在 PHP 7 及以下版本中可用的 SplFileObject::fgetss() 方法与独立的 fgetss() 函数类似,它将 fgets()strip_tags() 结合在一起。

为了便于说明,假设您创建了一个允许用户上传文本文件的网站。在显示文本文件内容之前,您希望删除任何标记标签。下面是一个使用 fgetss() 方法实现此目的的示例:

  1. 我们首先定义一个获取文件名的代码块:

    // /repo/ch05/php7_spl_splfileobject.php
    $fn = $_GET['fn'] ?? '';
    if (!$fn || !file_exists($fn))
        exit('Unable to locate file');
  2. 然后,我们创建 SplFileObject 实例,并使用 fgetss() 方法逐行读取文件。最后,我们将回显安全内容:

    $obj = new SplFileObject($fn, 'r');
    $safe = '';
    while ($line = $obj->fgetss()) $safe .= $line;
    echo '<h1>Contents</h1><hr>' . $safe;
  3. 假设要读取的文件是这样的:

    <h1>This File is Infected</h1>
    <script>alert('You Been Hacked');</script>
    <img src="http://very.bad.site/hacked.php" />
  4. 下面是使用此 URL 在 PHP 7.1 中运行的输出结果:

从下图的输出结果中可以看到,所有 HTML 标记都已删除:

image 2023 11 22 09 39 44 501
Figure 1. Figure 5.1 – Result after reading a file using SplFileObject::fgetss()

要在 PHP 8 中实现同样的功能,需要修改前面显示的代码,将 fgetss() 替换为 fgets()。我们还需要在连接到 $safe 的行中使用 strip_tags()。下面是修改后的代码:

// /repo/ch05/php8_spl_splfileobject.php
$fn = $_GET['fn'] ?? '';
if (!$fn || !file_exists($fn))
    exit('Unable to locate file');
$obj = new SplFileObject($fn, 'r');
$safe = '';
while ($line = $obj->fgets())
    $safe .= strip_tags($line);
echo '<h1>Contents</h1><hr>' . $safe;

修改后的代码输出与图 5.1 所示完全相同。现在我们将注意力转向另一个 SPL 类的修改:SplHeap

检查 SplHeap 的更改

SplHeap 是一个基础类,用于表示二叉树结构的数据。另外还有两个基于 SplHeap 的类。SplMinHeap 将最小值放在树的顶端。SplMaxHeap 则相反,将最大值放在顶部。

堆结构在数据到达时不按顺序排列的情况下特别有用。一旦插入堆中,项目就会自动按照正确的顺序排列。因此,在任何给定的时刻,您都可以安全地显示堆,而无需运行 PHP 的排序功能。

保持自动排序顺序的关键是定义一个抽象方法 compare()。由于该方法是抽象的,因此不能直接实例化 SplHeap。相反,您需要扩展该类并实现 compare()

在 PHP 8 中使用 SplHeap 时有可能出现向后兼容的代码中断,因为 compare() 的方法签名必须完全如下:SplHeap::compare($value1, $value2)

现在让我们来看一个使用 SplHeap 建立按姓氏排列的亿万富翁列表的代码示例:

  1. 首先,我们定义一个包含亿万富翁数据的文件。在本例中,我们只是简单地复制和粘贴了以下来源的数据: https://www.bloomberg.com/billionaires/

  2. 然后,我们定义了一个 BillionaireTracker 类,该类可从粘贴的文本中提取信息,并将其转换为有序对数组。该类的完整源代码(此处未显示)可在源代码库中找到:/repo/src/Services/BillionaireTracker.php 。

    下面是该类生成的数据:

    array(20) {
        [0] => array(1) {
          [177000000000] => string(10) "Bezos,Jeff"
        }
        [1] => array(1) {
          [157000000000] => string(9) "Musk,Elon"
        }
        [2] => array(1) {
          [136000000000] => string(10) "Gates,Bill"
        }
        ... remaining data not shown

    如您所见,数据是按降序排列的,关键字代表净资产。而在示例程序中,我们计划按姓氏升序生成数据。

  3. 然后,我们定义一个常量来标识亿万富翁数据源文件,并设置一个自动加载器:

    // /repo/ch05/php7_spl_splheap.php
    define('SRC_FILE', __DIR__ . '/../sample_data/billionaires.txt');
    require_once __DIR__ . '/../src/Server/Autoload/Loader.php';
    $loader = new \Server\Autoload\Loader();
  4. 接下来,我们创建一个 BillionaireTracker 类的实例,并将结果赋值给 $list

    use Services\BillionaireTracker;
    $tracker = new BillionaireTracker();
    $list = $tracker->extract(SRC_FILE);
  5. 现在到了最令人感兴趣的部分:创建堆。为此,我们定义了一个扩展 SplHeap 的匿名类。然后,我们定义一个 compare() 方法,该方法执行必要的逻辑,将插入的元素放置在适当的位置。PHP 7 允许更改方法签名。在本例中,我们以数组的形式提供参数:

    $heap = new class () extends SplHeap {
        public function compare(
            array $arr1, array $arr2) : int {
            $cmp1 = array_values($arr2)[0];
            $cmp2 = array_values($arr1)[0];
            return $cmp1 <=> $cmp2;
        }
    };

    您可能还会注意到,$cmp1 的值是从第二个数组中分配的,而 $cmp2 的值是从第一个数组中分配的。之所以进行这样的切换,是因为我们希望以升序生成结果。

  6. 然后我们使用 SplHeap::insert() 将元素添加到堆中:

    foreach ($list as $item)
        $heap->insert($item);
  7. 最后,我们定义了一个 BillionaireTracker::view() 方法(未显示)来运行堆并显示结果:

    $patt = "%20s\t%32s\n";
    $line = str_repeat('-', 56) . "\n";
    echo $tracker->view($heap, $patt, $line);
  8. 下面是我们在 PHP 7.1 中运行的小程序产生的输出结果:

    root@php8_tips_php7 [ /repo/ch05 ]#
    php php7_spl_splheap.php
    --------------------------------------------------------
    Net Worth                                     Name
    --------------------------------------------------------
    84,000,000,000                       Ambani,Mukesh
    115,000,000,000                    Arnault,Bernard
    83,600,000,000                       Ballmer,Steve
    ... some lines were omitted to save space ...
    58,200,000,000                          Walton,Rob
    100,000,000,000                    Zuckerberg,Mark
    --------------------------------------------------------
                                     1,795,100,000,000
    --------------------------------------------------------

但是,当我们尝试在 PHP 8 中运行同样的程序时,却出现了错误。下面是同一程序在 PHP 8 中运行的输出结果:

root@php8_tips_php8 [ /repo/ch05 ]# php php7_spl_splheap.php
PHP Fatal error: Declaration of SplHeap@
anonymous::compare(array $arr1, array $arr2): int must be
compatible with SplHeap::compare(mixed $value1, mixed $value2)
in /repo/ch05/php7_spl_splheap.php on line 16

因此,要使其正常工作,我们必须重新定义扩展 SplHeap 的匿名类。下面是这部分代码的修改版本:

$heap = new class () extends SplHeap {
    public function compare($arr1, $arr2) : int {
        $cmp1 = array_values($arr2)[0];
        $cmp2 = array_values($arr1)[0];
        return $cmp1 <=> $cmp2;
    }
};

唯一的变化是 compare() 方法的签名。执行时,结果(未显示)完全相同。PHP 8 的完整代码可在 /repo/ch05/php8_spl_splheap.php 中查看。

关于 SplHeap 类变化的讨论到此结束。请注意,同样的更改也适用于 SplMinHeapSplMaxHeap。现在让我们来看看 SplDoublyLinkedList 类中一个潜在的重大变更。

处理 SplDoublyLinkedList 中的更改

SplDoublyLinkedList 类是一个迭代器,能以 FIFO(先进先出)或 LIFO(后进先出)顺序显示信息。不过,更常见的说法是,您可以按正向或反向顺序遍历列表。

这对任何开发人员的库来说都是一个非常强大的补充。举例来说,用 ArrayIterator 做同样的事情至少需要十几行代码!因此,PHP 开发人员喜欢在需要随意向任一方向浏览列表时使用该类。

遗憾的是,由于 push()unshift() 方法的返回值不同,可能会造成代码中断。push() 方法用于在列表末尾添加一个值。另一方面,unshift() 方法将值添加到列表的开头。

在 PHP 7 及以下版本中,这些方法如果成功,则返回布尔值 TRUE。如果方法失败,则返回布尔值 FALSE。但在 PHP 8 中,这两个方法都不返回值。如果查看当前文档中的方法签名,会发现返回的数据类型是 void。如果在继续执行之前检查 push()unshift() 的返回值,就可能会出现代码中断。

让我们来看一个简单的示例,该示例用一个包含五个值的简单列表填充一个双链接列表,并以先进先出和后进先出的顺序显示它们:

  1. 首先,我们定义了一个扩展 SplDoublyLinkedList 的匿名类。我们还添加了一个 show() 方法,用于显示列表内容:

    // /repo/ch05/php7_spl_spldoublylinkedlist.php
    $double = new class() extends SplDoublyLinkedList {
        public function show(int $mode) {
            $this->setIteratorMode($mode);
            $this->rewind();
            while ($item = $this->current()) {
                echo $item . '. ';
                $this->next();
            }
        }
    };
  2. 接下来,我们定义一个样本数据数组,并使用 push() 将值插入到链表中。请注意,if() 语句用于确定操作成功或失败。如果操作失败,将抛出异常:

    $item = ['Person', 'Woman', 'Man', 'Camera', 'TV'];
    foreach ($item as $key => $value)
        if (!$double->push($value))
            throw new Exception('ERROR');

    这是存在潜在代码错误的代码块。在 PHP 7 及以下版本中,push() 返回 TRUEFALSE。在 PHP 8 中,没有返回值。

  3. 然后,我们使用 SplDoublyLinkedList 类常量将模式设置为 FIFO(向前),并显示列表:

    echo "**************** Foward ********************\n";
    $forward = SplDoublyLinkedList::IT_MODE_FIFO
            | SplDoublyLinkedList::IT_MODE_KEEP;
    $double->show($forward);
  4. 接下来,我们使用 SplDoublyLinkedList 类常量将模式设置为后进先出(反向),并显示列表:

    echo "\n\n************* Reverse *****************\n";
    $reverse = SplDoublyLinkedList::IT_MODE_LIFO
            | SplDoublyLinkedList::IT_MODE_KEEP;
    $double->show($reverse);

    以下是在 PHP 7.1 中运行的输出结果:

    root@php8_tips_php7 [ /repo/ch05 ]#
    php php7_spl_spldoublylinkedlist.php
    **************** Foward ********************
    Person. Woman. Man. Camera. TV.
    **************** Reverse ********************
    TV. Camera. Man. Woman. Person.
  5. 如果在 PHP 8 中运行同样的代码,结果会是这样:

    root@php8_tips_php8 [ /home/ch05 ]#
    php php7_spl_spldoublylinkedlist.php
    PHP Fatal error: Uncaught Exception: ERROR in /home/
    ch05/php7_spl_spldoublylinkedlist.php:23

如果 push() 没有返回值,则在 if() 语句中,PHP 将假定为 NULL,并将其插值为布尔值 FALSE!因此,在第一条 push() 命令之后,if() 块会导致异常抛出。由于异常没有被捕获,因此产生了一个致命错误。

要重写该代码块使其在 PHP 8 中工作,只需删除 if() 语句,并且不抛出异常即可。下面是改写后的代码块(如步骤 2 所示):

$item = ['Person', 'Woman', 'Man', 'Camera', 'TV'];
foreach ($item as $key => $value)
    $double->push($value);

现在,如果我们执行改写后的代码,结果就会出现在这里:

root@php8_tips_php7 [ /home/ch05 ]#
php php8_spl_spldoublylinkedlist.php
**************** Foward ********************
Person. Woman. Man. Camera. TV.
**************** Reverse ********************
TV. Camera. Man. Woman. Person.

现在您已经知道了如何使用 SplDoublyLinkedList,也知道了与 push()unshift() 有关的潜在代码中断。您还了解了在 PHP 8 中使用各种 SPL 类和函数时可能出现的代码中断。本章讨论到此结束。

总结

在本章中,您了解了迁移到 PHP 8 时 OOP 代码中可能出现的问题。在第一节中,您了解了在 PHP 7 和早期版本中允许使用的一些不良实践,在 PHP 8 中可能会导致代码崩溃。有了这些知识,你就能成为一名更好的开发人员,交付高质量的代码,为公司带来效益。

在下一节中,您将学到使用魔法方法时的良好习惯。潜在的代码断裂可能发生,因为 PHP 8 现在强制执行一定程度的一致性,这在 PHP 早期版本中是看不到的。这些不一致性涉及类构造函数的使用和魔法方法使用的某些方面。下一节将介绍 PHP 序列化,以及 PHP 8 中的更改如何使代码在序列化和非序列化过程中更有弹性、更不易出错或受到攻击。

在本章中,您还了解了 PHP 8 对协变返回类型和逆变参数的增强支持。掌握了变异的知识,以及 PHP 8 如何改进了对变异的支持,就能在 PHP 8 中开发类继承结构时更具创造性和灵活性。

上一节介绍了 SPL 中的一些关键类。你学到了很多关于如何在 PHP 8 中实现堆和链表等基本数据结构的知识。本节中的信息对于避免涉及 SPL 的代码出现问题至关重要。

下一章将继续讨论潜在的代码错误。不过,下一章的重点是程序代码而不是对象代码。