处理标准 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()
方法实现此目的的示例:
-
我们首先定义一个获取文件名的代码块:
// /repo/ch05/php7_spl_splfileobject.php $fn = $_GET['fn'] ?? ''; if (!$fn || !file_exists($fn)) exit('Unable to locate file');
-
然后,我们创建
SplFileObject
实例,并使用fgetss()
方法逐行读取文件。最后,我们将回显安全内容:$obj = new SplFileObject($fn, 'r'); $safe = ''; while ($line = $obj->fgetss()) $safe .= $line; echo '<h1>Contents</h1><hr>' . $safe;
-
假设要读取的文件是这样的:
<h1>This File is Infected</h1> <script>alert('You Been Hacked');</script> <img src="http://very.bad.site/hacked.php" />
-
下面是使用此 URL 在 PHP 7.1 中运行的输出结果:
从下图的输出结果中可以看到,所有 HTML 标记都已删除:

要在 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
建立按姓氏排列的亿万富翁列表的代码示例:
-
首先,我们定义一个包含亿万富翁数据的文件。在本例中,我们只是简单地复制和粘贴了以下来源的数据: https://www.bloomberg.com/billionaires/ 。
-
然后,我们定义了一个
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
如您所见,数据是按降序排列的,关键字代表净资产。而在示例程序中,我们计划按姓氏升序生成数据。
-
然后,我们定义一个常量来标识亿万富翁数据源文件,并设置一个自动加载器:
// /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();
-
接下来,我们创建一个
BillionaireTracker
类的实例,并将结果赋值给$list
:use Services\BillionaireTracker; $tracker = new BillionaireTracker(); $list = $tracker->extract(SRC_FILE);
-
现在到了最令人感兴趣的部分:创建堆。为此,我们定义了一个扩展
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
的值是从第一个数组中分配的。之所以进行这样的切换,是因为我们希望以升序生成结果。 -
然后我们使用
SplHeap::insert()
将元素添加到堆中:foreach ($list as $item) $heap->insert($item);
-
最后,我们定义了一个
BillionaireTracker::view()
方法(未显示)来运行堆并显示结果:$patt = "%20s\t%32s\n"; $line = str_repeat('-', 56) . "\n"; echo $tracker->view($heap, $patt, $line);
-
下面是我们在 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
类变化的讨论到此结束。请注意,同样的更改也适用于 SplMinHeap
和 SplMaxHeap
。现在让我们来看看 SplDoublyLinkedList
类中一个潜在的重大变更。
处理 SplDoublyLinkedList 中的更改
SplDoublyLinkedList
类是一个迭代器,能以 FIFO(先进先出)或 LIFO(后进先出)顺序显示信息。不过,更常见的说法是,您可以按正向或反向顺序遍历列表。
这对任何开发人员的库来说都是一个非常强大的补充。举例来说,用 ArrayIterator
做同样的事情至少需要十几行代码!因此,PHP 开发人员喜欢在需要随意向任一方向浏览列表时使用该类。
遗憾的是,由于 push()
和 unshift()
方法的返回值不同,可能会造成代码中断。push()
方法用于在列表末尾添加一个值。另一方面,unshift()
方法将值添加到列表的开头。
在 PHP 7 及以下版本中,这些方法如果成功,则返回布尔值 TRUE
。如果方法失败,则返回布尔值 FALSE
。但在 PHP 8 中,这两个方法都不返回值。如果查看当前文档中的方法签名,会发现返回的数据类型是 void
。如果在继续执行之前检查 push()
或 unshift()
的返回值,就可能会出现代码中断。
让我们来看一个简单的示例,该示例用一个包含五个值的简单列表填充一个双链接列表,并以先进先出和后进先出的顺序显示它们:
-
首先,我们定义了一个扩展
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(); } } };
-
接下来,我们定义一个样本数据数组,并使用
push()
将值插入到链表中。请注意,if()
语句用于确定操作成功或失败。如果操作失败,将抛出异常:$item = ['Person', 'Woman', 'Man', 'Camera', 'TV']; foreach ($item as $key => $value) if (!$double->push($value)) throw new Exception('ERROR');
这是存在潜在代码错误的代码块。在 PHP 7 及以下版本中,
push()
返回TRUE
或FALSE
。在 PHP 8 中,没有返回值。 -
然后,我们使用
SplDoublyLinkedList
类常量将模式设置为 FIFO(向前),并显示列表:echo "**************** Foward ********************\n"; $forward = SplDoublyLinkedList::IT_MODE_FIFO | SplDoublyLinkedList::IT_MODE_KEEP; $double->show($forward);
-
接下来,我们使用
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.
-
如果在 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 的代码出现问题至关重要。
下一章将继续讨论潜在的代码错误。不过,下一章的重点是程序代码而不是对象代码。