PHP8 理解 PHP8 的功能差异详解

在本章中,您将了解 PHP8 命令或函数级的潜在向后兼容中断。本章介绍了将现有代码迁移到 PHP8 时可能出现的陷阱的重要信息。本章中提供的信息非常重要,这样您就可以生成可靠的 PHP 代码。在完成了本章中的概念之后,您将能够更好地编写生成精确结果并避免不一致的代码。

本章涵盖的主题包括以下内容:

要检查并运行本章中提供的代码示例,建议使用的最低硬件如下:

  • 基于 x86_64 的台式 PC 或笔记本电脑
  • 1 GB 的可用磁盘空间
  • 4 GB 内存
  • 每秒 500 千比特(Kbps)或更快的 internet 连接

此外,您还需要安装以下软件:

  • 码头工人
  • Docker Compose

请参考第 1 章中的技术要求部分,介绍新的 PHP8 OOP 功能,了解有关 Docker 和 Docker Compose 安装的更多信息,以及如何构建用于演示本书中解释的代码的 Docker 容器。在本书中,我们将您还原本书样本代码的目录称为/repo

本章的源代码位于以下位置:

https://github.com/PacktPublishing/PHP-8-Programming-Tips-Tricks-and-Best-Practices.

我们现在可以通过检查 PHP8 中引入的字符串处理的差异来开始讨论。

中的字符串函数已在 PHP8 中收紧和规范化。您会发现 PHP8 的使用受到了更严格的限制,这最终迫使您生成更好的代码。我们可以说字符串函数参数的性质和顺序在 PHP8 中更加统一,这就是为什么我们说 PHP 核心团队已经规范化了使用。

在处理数字字符串时,这些改进尤其明显。PHP8 字符串处理中的其他更改涉及参数的微小更改。在本节中,我们将向您介绍 PHP8 如何处理字符串的关键更改。

重要的是不仅要了解 PHP8 中引入的处理改进,还要了解 PHP8 之前的字符串处理缺陷。

让我们首先看看搜索嵌入字符串的函数中 PHP8 字符串处理的一个方面。

处理针参数的更改

许多 PHP 字符串函数在较大字符串中搜索是否存在子字符串。这些功能包括strpos()strrpos()stripos()strripos()strstr()strchr()strrchr()stristr()。所有这些功能在中都有这两个共同的参数:草垛

区分针和干草堆

为了说明针和干草堆之间的区别,请查看strpos()的功能签名:

strpos(string $haystack,string $needle,int $pos=0): int|false

$haystack是搜索的目标。$needle是要查找的子字符串。strpos()函数返回子字符串在搜索目标中的位置。如果未找到子字符串,则返回布尔值FALSE。其他str*()函数产生不同类型的输出,我们在这里不再详述。

PHP8 处理指针参数的两个关键更改可能会破坏迁移到 PHP8 的应用。这些更改适用于指针参数不是字符串或指针参数为空的情况。让我们先看看非字符串指针参数处理。

处理非字符串指针参数

您的 PHP 应用可能没有采取适当的预防措施来确保此处提到的str*()函数的指针参数始终是字符串。如果是这种情况,在 PHP8 中,指针参数现在将始终解释为字符串,而不是 ASCII 码点。

如果需要提供 ASCII 值,则必须使用chr()函数将其转换为字符串。在以下示例中,使用了LF"\n"的 ASCII 值而不是字符串。在 PHP7 或更低版本中,strpos()在运行搜索之前执行内部转换。在 PHP8 中,数字被简单地类型转换成字符串,从而产生意外的结果。

下面是一个代码示例,用于搜索字符串中是否存在LF。但是,请注意,不是提供字符串作为参数,而是提供了一个值为10的整数:

// /repo/ch06/php8_num_str_needle.php
function search($needle, $haystack) {
    $found = (strpos($haystack, $needle))
           ? 'contains' : 'DOES NOT contain';
    return "This string $found LF characters\n";
}
$haystack = "We're looking\nFor linefeeds\nIn this 
             string\n";
$needle = 10;         // ASCII code for LF
echo search($needle, $haystack);

以下是 PHP 7 中运行的代码示例的结果:

root@php8_tips_php7 [ /repo/ch06 ]# 
php php8_num_str_needle.php
This string contains LF characters

下面是在 PHP 8 中运行的相同代码块的结果:

root@php8_tips_php8 [ /repo/ch06 ]# 
php php8_num_str_needle.php
This string DOES NOT contain LF characters

如您所见,将 PHP7 中的输出与 PHP8 中的输出进行比较,相同的代码块会产生完全不同的结果。由于没有生成WarningsErrors,这是一个极其困难的潜在代码中断点。

最佳实践是对任何包含 PHPstr*()函数之一的函数或方法的指针参数应用string类型提示。如果我们重写前面的示例,那么 PHP7 和 PHP8 中的输出都是一致的。下面是使用类型提示重写的相同示例:

// /repo/ch06/php8_num_str_needle_type_hint.php
declare(strict_types=1);
function search(string $needle, string $haystack) {
    $found = (strpos($haystack, $needle))
           ? 'contains' : 'DOES NOT contain';
    return "This string $found LF characters\n";
}
$haystack = "We're looking\nFor linefeeds\nIn this 
             string\n";
$needle   = 10;         // ASCII code for LF
echo search($needle, $haystack);

现在,在 PHP 的版本中,这是的输出:

PHP Fatal error:  Uncaught TypeError: search(): Argument #1 ($needle) must be of type string, int given, called in /repo/ch06/php8_num_str_needle_type_hint.php on line 14 and defined in /repo/ch06/php8_num_str_needle_type_hint.php:4

通过声明strict_types=1,并在$needle参数之前添加string类型提示,任何误用代码的开发人员都会收到一个明确的指示,表明这种做法是不可接受的。

现在让我们看看当缺少针参数时 PHP8 中会发生什么。

处理空需要 le 参数

str*()函数的另一个主要变化是指针参数现在可以为空(例如,任何使empty()函数返回TRUE的参数)。这为向后兼容性中断提供了巨大的潜力。在 PHP7 中,如果指针参数为空,strpos()的返回值将是布尔值FALSE,而在 PHP8 中,空值首先转换为字符串,从而产生完全不同的结果。

如果您计划将 PHP 版本更新为 8,那么了解这种潜在的代码中断是非常重要的。手动查看代码时,很难发现空指针参数。在这种情况下,需要一组可靠的单元测试来确保顺利的 PHP 迁移。

为了说明潜在的问题,请考虑下面的例子。假设针参数为空。在这种情况下,传统的if()检查strpos()结果是否与FALSE不一致会在 PHP7 和 PHP8 之间产生不同的结果。以下是代码示例:

  1. 首先,我们定义了一个函数,该函数使用strpos()报告是否在干草堆中找到针值。注意对布尔值FALSE

    // php7_num_str_empty_needle.php
    function test($haystack, $search) {
        $pattern = '%15s | %15s | %10s' . "\n";
        $result  = (strpos($haystack, $search) !== FALSE)
                 ? 'FOUND' :  'NOT FOUND';
        return sprintf($pattern,
               var_export($search, TRUE),
               var_export(strpos($haystack, $search), 
                 TRUE),
               $result);
    };

    进行严格的类型检查

  2. We then define the haystack as a string with letters and numbers. The needle argument is provided in the form of an array of values that are all considered empty:

    $haystack = 'Something Anything 0123456789';
    $needles = ['', NULL, FALSE, 0];
    foreach ($needles as $search) 
        echo test($haystack, $search);

    PHP 7 中的输出如下所示:

root@php8_tips_php7 [ /repo/ch06 ]# 
php php7_num_str_empty_needle.php
PHP Warning:  strpos(): Empty needle in /repo/ch06/php7_num_str_empty_needle.php on line 5
// not all Warnings are shown ...
             '' |           false |  NOT FOUND
           NULL |           false |  NOT FOUND
          false |           false |  NOT FOUND
              0 |           false |  NOT FOUND

在一组Warnings之后,出现最终输出。从输出中可以看出,strpos($haystack, $search)的返回值在 PHP7 中始终是布尔值FALSE

然而,在 PHP8 中运行相同代码的输出是完全不同的。以下是 PHP8 的输出:

root@php8_tips_php8 [ /repo/ch06 ]# 
php php7_num_str_empty_needle.php
             '' |               0 |      FOUND
           NULL |               0 |      FOUND
          false |               0 |      FOUND
              0 |              19 |      FOUND

在 PHP8 中,空指针参数首先以静默方式转换为字符串。没有一个指针值返回布尔值FALSE。这会导致函数报告已找到指针。这肯定不是期望的结果。然而,在编号0的情况下,它包含在干草堆中,导致返回一个值19

让我们看看如何解决这个问题。

使用 str_contains()解决问题

上一节中显示的代码块的目的是确定草堆是否包含针。strpos()不是完成此任务的正确工具!使用str_contains()查看相同的函数:

// /repo/ch06/php8_num_str_empty_needle.php
function test($haystack, $search) {
    $pattern = '%15s | %15s | %10s' . "\n";
    $result  = (str_contains($search, $haystack) !==  
                FALSE)  
                 ? 'FOUND'  : 'NOT FOUND';
    return sprintf($pattern,
           var_export($search, TRUE),
           var_export(str_contains($search, $haystack), 
             TRUE),
           $result);
};

如果我们在 PHP8 中运行修改后的代码,我们会得到与 PHP7 类似的结果:

root@php8_tips_php8 [ /repo/ch06 ]# 
php php8_num_str_empty_needle.php
             '' |           false |  NOT FOUND
           NULL |           false |  NOT FOUND
          false |           false |  NOT FOUND
              0 |           false |  NOT FOUND

您可能会问,为什么在字符串中找不到号0?答案是str_contains()进行了更严格的搜索。整数0与字符串"0"不一样!现在我们来看看v*printf()家族;另一个字符串函数家族在 PHP8 中对其参数实施更严格的控制。

使用 v*printf()更改处理

v*printf()系列函数是printf()系列函数的子集,包括vprintf()vfprintf()vsprintf()。此子集与主族的区别在于v*printf()函数的设计目的是接受数组作为参数,而不是无限系列的参数。下面是一个简单的例子,说明了两者的区别:

  1. 首先,我们定义一组参数,这些参数将插入到模式中,$patt

    // /repo/ch06/php8_printf_vs_vprintf.php
    $ord  = 'third';
    $day  = 'Thursday';
    $pos  = 'next';
    $date = new DateTime("$ord $day of $pos month");
    $patt = "The %s %s of %s month is: %s\n";
  2. 然后,我们使用一系列参数执行一个printf()语句:

    printf($patt, $ord, $day, $pos, 
           $date->format('l, d M Y'));
  3. We then define the arguments as an array, $arr, and use vprintf() to produce the same result:

    $arr  = [$ord, $day, $pos, $date->format('l, d M 
               Y')];vprintf($patt, $arr);

    下面是在 PHP8 中运行的程序的输出。输出与 PHP 7 中运行的相同(未显示):

    root@php8_tips_php8 [ /repo/ch06 ]#
    php php8_printf_vs_vprintf.php
    The third Thursday of next month is: Thursday, 15 Apr 2021
    The third Thursday of next month is: Thursday, 15 Apr 2021

    如您所见,这两个函数的输出是相同的。唯一的用法区别是vprintf()接受数组形式的参数。

早期版本的 PHP 允许开发人员通过向v*printf()系列函数提供参数来快速和松散地玩游戏。在 PHP8 中,参数的数据类型现在被严格执行。这只会在不存在代码控件以确保显示数组时出现问题。另一个更重要的区别是 PHP7 将允许ArrayObjectv*printf(),而 PHP8 将不允许。

在这里显示的示例中,PHP7 发出一个Warning,而 PHP8 发出一个Error

  1. 首先,我们定义模式和源数组:

    // /repo/ch06/php7_vprintf_bc_break.php
    $patt = "\t%s. %s. %s. %s. %s.";
    $arr  = ['Person', 'Woman', 'Man', 'Camera', 'TV'];
  2. 然后,我们定义一个测试数据数组,以测试vsprintf()

    $args = [
        'Array' => $arr, 
        'Int'   => 999,
        'Bool'  => TRUE, 
        'Obj'   => new ArrayObject($arr)
    ];

    接受哪些参数

  3. 然后,我们定义一个foreach()循环,该循环遍历测试数据并练习vsprintf()

    foreach ($args as $key => $value) {
        try {
            echo $key . ': ' . vsprintf($patt, $value);
        } catch (Throwable $t) {
            echo $key . ': ' . get_class($t) 
                 . ':' . $t->getMessage();
        }
    }

以下是在 PHP 7 中运行的输出:

root@php8_tips_php7 [ /repo/ch06 ]# 
php php7_vprintf_bc_break.php
Array:     Person. Woman. Man. Camera. TV.
PHP Warning:  vsprintf(): Too few arguments in /repo/ch06/php8_vprintf_bc_break.php on line 14
Int: 
PHP Warning:  vsprintf(): Too few arguments in /repo/ch06/php8_vprintf_bc_break.php on line 14
Bool: 
Obj:     Person. Woman. Man. Camera. TV.

从输出中可以看到,PHP7 接受数组和ArrayObject参数。以下是在 PHP 8 中运行的相同代码示例:

root@php8_tips_php8 [ /repo/ch06 ]# 
php php7_vprintf_bc_break.php
Array:     Person. Woman. Man. Camera. TV.
Int: TypeError:vsprintf(): Argument #2 ($values) must be of type array, int given
Bool: TypeError:vsprintf(): Argument #2 ($values) must be of type array, bool given
Obj: TypeError:vsprintf(): Argument #2 ($values) must be of type array, ArrayObject given

正如所料,PHP8 的输出更加一致。在 PHP8 中,v*printf()函数严格类型化,只接受数组作为参数。不幸的是,您很可能一直在使用ArrayObject。只需在返回数组的ArrayObject实例上使用getArrayCopy()方法即可轻松解决此问题。

以下是在 PHP7 和 PHP8 中工作的重写代码:

    if ($value instanceof ArrayObject)
        $value = $value->getArrayCopy();
    echo $key . ': ' . vsprintf($patt, $value);

现在,您已经知道了在使用v*printf()函数时在何处查找潜在的代码中断,让我们将注意力转向 PHP8 中具有空长度参数的字符串函数的工作方式的差异。

在 PHP8 中使用空长度参数

在 PHP7 和更早版本中,NULL长度参数导致空字符串。在 PHP8 中,NULL长度参数现在被视为与省略长度参数相同。受影响的职能包括:

  • substr()
  • substr_count()
  • substr_compare()
  • iconv_substr()

在下一个示例中,PHP7 返回一个空字符串,而 PHP8 返回字符串的其余部分。如果操作结果用于确认或否认子字符串的存在,则极有可能出现代码中断:

  1. 首先,我们定义了干草堆和针。然后我们运行strpos()以获得针在草堆中的位置:

    // /repo/ch06/php8_null_length_arg.php
    $str = 'The quick brown fox jumped over the fence';
    $var = 'fox';
    $pos = strpos($str, $var);
  2. 接下来,我们拉出子字符串,故意保留长度参数未定义:

    $res = substr($str, $pos, $len);
    $fnd = ($res) ? '' : ' NOT';
    echo "$var is$fnd found in the string\n";

以下是在 PHP 7 中运行的输出:

root@php8_tips_php7 [ /repo/ch06 ]# 
php php8_null_length_arg.php
PHP Notice:  Undefined variable: len in /repo/ch06/php8_null_length_arg.php on line 8
Result   : fox is NOT found in the string
Remainder: 

正如所料,PHP7 发布了一个Notice。但是,由于NULL长度参数返回空字符串,因此搜索结果不正确。以下是在 PHP 8 中运行的相同代码:

root@php8_tips_php8 [ /repo/ch06 ]# 
php php8_null_length_arg.php
PHP Warning:  Undefined variable $len in /repo/ch06/php8_null_length_arg.php on line 8
Result   : fox is found in the string
Remainder: fox jumped over the fence

PHP8 发出一个Warning并返回字符串的剩余部分。这与行为一致,其中长度参数被完全省略。如果您的代码依赖于返回的空字符串,则在 PHP8 更新后可能存在代码中断。

现在让我们来看另一种情况,PHP8 使implode()函数中的字符串处理更加统一。

检查内爆的变化()

两个广泛使用的 PHP 函数执行数组到字符串的转换,反之亦然:explode()将字符串转换为数组,implode()将数组转换为字符串。然而,implode()函数隐藏着一个深刻的秘密:它的两个参数可以以任何顺序表示!

请记住,当 PHP 在 1994 年首次引入时,最初的目标是使其尽可能易于使用。根据 w3techs 最近对服务器端编程语言进行的调查,这种方法取得了成功,目前超过 78%的 web 服务器选择 PHP 语言。(https://w3techs.com/technologies/overview/programming_language)

然而,为了保持一致性,将implode()函数的参数与其镜像孪晶explode()对齐是有意义的。因此,提供给implode()的参数现在必须按以下顺序排列:

implode(<GLUE STRING>, <ARRAY>);

下面是调用implode()函数的代码示例,该函数的参数顺序如下:

// /repo/ch06/php7_implode_args.php
$arr  = ['Person', 'Woman', 'Man', 'Camera', 'TV'];
echo __LINE__ . ':' . implode(' ', $arr) . "\n";
echo __LINE__ . ':' . implode($arr, ' ') . "\n";

从下面的 PHP 7 输出中可以看到,两个 echo 语句都会产生结果:

root@php8_tips_php7 [ /repo/ch06 ]# php php7_implode_args.php
5:Person Woman Man Camera TV
6:Person Woman Man Camera TV

在 PHP 8 中,只有第一条语句成功,如下所示:

root@php8_tips_php8 [ /repo/ch06 ]# 
php php7_implode_args.php
5:Person Woman Man Camera TV
PHP Fatal error:  Uncaught TypeError: implode(): Argument #2 ($array) must be of type ?array, string given in /repo/ch06/php7_implode_args.php:6

implode()以错误顺序接收参数的位置很难发现。在 PHP8 迁移之前,最好的预警方式是记录所有使用implode()的 PHP 文件类。另一个建议是利用 PHP8命名参数功能(在第 1 章中介绍,引入了新的 PHP8 OOP 功能

学习 PHP 8 中常量的用法

在版本 8 之前,PHP 真正的惊人功能之一就是能够定义不区分大小写的常量。一开始,当 PHP 第一次被引入时,许多开发人员编写了大量 PHP 代码,但明显缺乏任何编码标准。当时的目标只是让它发挥作用

与强制执行良好编码标准的总体趋势一致,PHP7.3 中不推荐使用此功能,PHP8 中删除了此功能。如果使用define()并将第三个参数设置为TRUE,则可能会出现向后兼容的中断。

此处显示的示例适用于 PHP 7,但不完全适用于 PHP 8:

// /repo/ch06/php7_constants.php
define('THIS_WORKS', 'This works');
define('Mixed_Case', 'Mixed Case Works');
define('DOES_THIS_WORK', 'Does this work?', TRUE);
echo __LINE__ . ':' . THIS_WORKS . "\n";
echo __LINE__ . ':' . Mixed_Case . "\n";
echo __LINE__ . ':' . DOES_THIS_WORK . "\n";
echo __LINE__ . ':' . Does_This_Work . "\n";

在 PHP7 中,所有代码行都按照编写的方式工作。以下是输出:

root@php8_tips_php7 [ /repo/ch06 ]# php php7_constants.php
7:This works
8:Mixed Case Works
9:Does this work?
10:Does this work?

请注意,define()的第三个参数在 PHP7.3 中被弃用。因此,如果在 PHP7.3 或 7.4 中运行此代码示例,则输出与添加的Deprecation通知相同。

然而,在 PHP 8 中,会产生完全不同的结果,如下所示:

root@php8_tips_php8 [ /repo/ch06 ]# php php7_constants.php
PHP Warning:  define(): Argument #3 ($case_insensitive) is ignored since declaration of case-insensitive constants is no longer supported in /repo/ch06/php7_constants.php on line 6
7:This works
8:Mixed Case Works
9:Does this work?
PHP Fatal error:  Uncaught Error: Undefined constant "Does_This_Work" in /repo/ch06/php7_constants.php:10

正如您所料,第 7、8 和 9 行生成了预期的结果。然而,最后一行抛出了一个致命的Error,因为 PHP8 中的常量现在区分大小写。另外,第三条define()语句被发出Warning,因为第三条参数在 PHP 8 中被忽略。

现在您已经了解了 PHP8 中引入的键字符串处理差异。接下来,我们将关注数字字符串与数字比较方式的变化。

比较两个数值在 PHP 中从来都不是问题。两个字符串之间的比较也不是问题。字符串和数字数据(硬编码数字或包含floatint类型数据的变量)之间的非严格比较会出现问题。在这种情况下,如果执行非严格比较,PHP 将始终将字符串转换为数值。

只有当字符串仅包含数字(或加号、减号或十进制分隔符等数值)时,字符串到数字的转换才能 100%成功。在本节中,您将学习如何防止涉及字符串和数字数据的不准确的非严格比较。如果您希望生成行为一致且可预测的代码,那么掌握本章中介绍的概念至关重要。

在深入研究字符串到数字比较的细节之前,我们需要首先了解非严格比较的含义。

学习严格比较和非严格比较

类型杂耍的概念是 PHP 语言的基本部分。这种能力从语言诞生的第一天起就被植入到语言中。类型转换涉及在执行操作之前执行内部数据类型转换。这种能力对语言的成功至关重要。

PHP 最初设计用于在 web 环境中执行,需要一种方法来处理作为 HTTP 数据包一部分传输的数据。HTTP 头和正文以文本形式传输,PHP 以字符串形式接收,字符串存储在一组超全局中,包括$_SERVER$_GET$_POST,等。因此,当执行涉及数字的操作时,PHP 语言需要一种快速处理字符串值的方法。这是类型杂耍过程的工作。

严格比较是首先检查数据类型的。如果数据类型匹配,则进行比较。调用严格比较的运算符包括===!==等。某些函数具有强制严格数据类型的选项。一个例子是in_array()。如果第三个参数设置为TRUE,则会进行严格的类型搜索。以下是in_array()的方法签名:

in_array(mixed $needle, array $haystack, bool $strict = false)

非严格比较是指在比较之前不进行数据类型检查。执行非严格比较的运算符包括==!=<>等。值得注意的是,switch {}语言结构在其case语句中执行非严格比较。如果进行涉及不同数据类型的操作数的非严格比较,则执行类型转换。

现在让我们详细了解一下数字字符串。

检查数字字符串

数字字符串是仅包含数字或数字字符的字符串,如加号(+)、减号(-)和小数分隔符。

重要提示

需要注意的是,PHP8 内部使用句点字符(.作为十进制分隔符。如果需要在不使用句点作为小数分隔符的区域设置中呈现数字(例如,在法国,逗号(,用作小数分隔符),请使用number_format()函数(请参见 https://www.php.net/number_format). 有关更多信息,请参阅本章中的利用区域独立性部分。

数字字符串也可以使用工程符号(也称为科学符号)来组合。格式不正确的数字字符串是包含除数字、加号、减号或十进制分隔符以外的值的数字字符串。前导数字字符串以数字字符串开头,但后跟非数字字符。PHP 引擎认为任何既不是数字也不是前导数字的字符串都是非数字

在以前的 PHP 版本中,键入 juggling 不一致地解析包含数字的字符串。在 PHP8 中,只能将数字字符串干净地转换为数字:不能存在前导或尾随空格或其他非数字字符。

例如,在此代码示例中,看看 PHP 7 和 8 处理数字字符串的方式的差异:

// /repo/ch06/php8_num_str_handling.php
$test = [
    0 => '111',
    1 => '   111',
    2 => '111   ',
    3 => '111xyz'
];
$patt = "%d : %3d : '%-s'\n";
foreach ($test as $key => $val) {
    $num = 111 + $val;
    printf($patt, $key, $num, $val);
}

以下是在 PHP 7 中运行的输出:

root@php8_tips_php7 [ /repo/ch06 ]# 
php php8_num_str_handling.php
0 : 222 : '111'
1 : 222 : '   111'
PHP Notice:  A non well formed numeric value encountered in /repo/ch06/php8_num_str_handling.php on line 11
2 : 222 : '111   '
PHP Notice:  A non well formed numeric value encountered in /repo/ch06/php8_num_str_handling.php on line 11
3 : 222 : '111xyz'

从输出中可以看到,PHP7 认为带有尾随空格的字符串格式不正确。但是,具有前导空格的字符串被视为格式良好,并且通过时不会生成Notice。带有非空白字符的字符串仍在处理中,但应使用Notice

以下是在 PHP 8 中运行的相同代码示例:

root@php8_tips_php8 [ /repo/ch06 ]# 
php php8_num_str_handling.php
0 : 222 : '111'
1 : 222 : '   111'
2 : 222 : '111   '
PHP Warning:  A non-numeric value encountered in /repo/ch06/php8_num_str_handling.php on line 11
3 : 222 : '111xyz'

PHP8 更加一致,因为包含前导空格或尾随空格的数字字符串被同等对待,并且不会生成NoticesWarnings。然而,最后一个字符串,以前是 PHP7 中的一个Notice,现在生成一个Warning

提示

您可以在以下 PHP 文档中阅读关于数字字符串的内容:

https://www.php.net/manual/en/language.types.numeric-strings.php

有关类型杂耍的更多信息,请查看以下 URL:

https://www.php.net/manual/en/language.types.type-juggling.php

现在您已经了解了什么是格式良好和非格式良好的数字字符串,让我们来关注在 PHP8 中处理数字字符串时可能出现的向后兼容中断这一更严重的问题。

检测涉及数字字符串的向后兼容中断

您必须了解在 PHP8 升级后,代码哪里可能会中断。在本小节中,我们将向您展示一些可能产生重大后果的极其微妙的差异。

使用格式不正确的数字字符串时,可能会出现潜在的代码中断:

  • is_numeric()一起
  • 在字符串偏移量中(例如,$str['4x']
  • 使用位运算符
  • 递增或递减值为格式不正确的数字字符串的变量时

以下是修复代码的一些建议:

  • 考虑在数字字符串中使用可能包含先导或尾随空白空间的枚举 T0(例如,嵌入在张贴表单数据中的数字字符串)。
  • 如果代码依赖于以数字开头的字符串,请使用显式类型转换以确保正确插入数字。
  • 不要依赖空字符串(例如,$str = '')来干净地转换为 0。

在下面的代码示例中,带有尾随空格的格式不正确的字符串被分配给$age

// /repo/ch06/php8_num_str_is_numeric.php
$age = '77  ';
echo (is_numeric($age))
     ? "Age must be a number\n"
     : "Age is $age\n";

当我们在 PHP7 中运行此代码时,is_numeric()返回TRUE。以下是 PHP7 的输出:

root@php8_tips_php7 [ /repo/ch06 ]# 
php php8_num_str_is_numeric.php
Age is 77  

另一方面,当我们在 PHP8 中运行此代码时,is_numeric()返回FALSE,因为字符串不被视为数字。以下是 PHP 8 的输出:

root@php8_tips_php8 [ /repo/ch06 ]# 
php php8_num_str_is_numeric.php
Age must be a number

如您所见,PHP7 和 PHP8 之间的字符串处理差异可能会导致应用的行为不同,从而导致潜在的灾难性后果。现在让我们看一看涉及格式良好的字符串的不一致结果。

处理字符串与数值比较结果不一致的问题

为了完成涉及字符串和数字数据的非严格比较,PHP 引擎首先执行一个类型转换操作,在执行比较之前将字符串内部转换为数字。然而,即使是一个格式良好的数字字符串,也可能产生从人类角度看是荒谬的结果。

作为示例,请查看以下代码示例:

  1. 首先,我们对值为零的变量$zero和值为 ABC 的变量$string进行非严格比较:

    $zero   = 0;
    $string = 'ABC';
    $result = ($zero == $string) ? 'is' : 'is not';
    echo "The value $zero $result the same as $string\n"2
  2. 以下非严格比较使用in_array()$array数组中定位零值:

    $array  = [1 => 'A', 2 => 'B', 3 => 'C'];
    $result = (in_array($zero, $array)) 
            ? 'is in' : 'is not in';
    echo "The value $zero $result\n" 
         . var_export($array, TRUE)3
  3. 最后,我们在前导数字字符串42abc88和硬编码数字42

    $mixed  = '42abc88';
    $result = ($mixed == 42) ? 'is' : 'is not';
    echo "\nThe value $mixed $result the same as 42\n";

    之间执行非严格比较

在 PHP7 中运行的结果令人无法理解!以下是 PHP7 结果:

root@php8_tips_php7 [ /repo/ch06 ]# 
php php7_compare_num_str.php
The value 0 is the same as ABC
The value 0 is in
array (1 => 'A', 2 => 'B', 3 => 'C')
The value 42abc88 is the same as 42

从人类的角度来看,这些结果都毫无意义!另一方面,从计算机的角度来看,它非常有意义。字符串ABC在转换为数字时,最终的值为零。类似地,当进行数组搜索时,每个数组元素(只有一个字符串值)最终被插值为零。

前导数字字符串的情况有点棘手。在 PHP7 中,插值算法转换数字字符,直到遇到第一个非数字字符。一旦发生这种情况,插值就会停止。因此,出于比较目的,字符串42abc88变为整数42。现在让我们看看 PHP8 如何处理字符串到数字的比较。

了解 PHP 8 中的比较更改

在 PHP8 中,如果将字符串与数字进行比较,则只有数值字符串被认为是有效的比较。指数表示法中的字符串以及带有前导或尾随空格的数字字符串也被认为是比较有效的。非常重要的是要注意,PHP8 在转换字符串之前做出了这个决定。

请看上一小节中描述的相同代码示例的输出(处理不一致的字符串与数字比较结果),在 PHP 8 中运行:

root@php8_tips_php8 [ /repo/ch06 ]# 
php php7_compare_num_str.php
The value 0 is not the same as ABC
The value 0 is not in
array (1 => 'A', 2 => 'B', 3 => 'C')
The value 42abc88 is not the same as 42

因此,正如您从输出中所看到的,对于您的应用来说,在 PHP8 升级之后,有巨大的潜力来改变其行为。作为 PHP8 字符串处理的最后一点,让我们看看如何避免升级问题。

避免 PHP 8 升级过程中出现问题

您面临的主要问题是 PHP8 处理涉及不同数据类型的操作数的非严格比较的方式不同。如果一个操作数为intfloat,而另一个操作数为string,则升级后可能会出现问题。如果字符串是有效的数字字符串,则非严格比较将继续进行,不会出现任何问题。

以下操作员受到影响:<=>==!=>>=<<=。如果选项标志设置为默认值,则以下功能将受到影响:

最佳实践是通过为函数或方法提供类型提示来最小化 PHP 类型的变化。也可以在比较之前强制数据类型。最后,考虑使用严格的比较,尽管这在所有情况下可能并不适用。

现在您已经了解了如何在 PHP8 中正确处理涉及数字字符串的比较,现在让我们看看 PHP8 中涉及算术、按位和串联操作的更改。

算术、按位和串联操作是任何 PHP 应用的核心。在本节中,您将了解在 PHP8 迁移之后,在这些简单操作中可能出现的隐患。您必须了解在 PHP8 中所做的更改,以避免应用中潜在的代码中断。因为这些操作非常普通,没有这些知识,您将很难发现迁移后的错误。

让我们首先看看 PHP 如何在算术和位运算中处理非标量数据类型。

在算术和位运算中处理非标量数据类型

从历史上看,PHP 引擎一直非常宽容在算术或位运算中使用混合数据类型。我们已经了解了涉及数字前导数字非数字字符串和数字的比较操作。正如您所了解的,当使用非严格比较时,PHP 在执行比较之前调用类型转换将字符串转换为数字。当 PHP 执行涉及数字和字符串的算术运算时,也会发生类似的操作。

在 PHP 8 之前,算术运算中允许使用非标量数据类型(除stringintfloatboolean之外的数据类型)。PHP8 已经限制了这种不良做法,不再允许使用arrayresourceobject类型的操作数。当算术运算中使用非标量操作数时,PHP8 始终抛出一个TypeError。此常规更改的唯一例外是,在所有操作数均为array类型的情况下,仍然可以执行算术运算。

提示

有关算术和位运算的重要变化的更多信息,请查看以下内容:https://wiki.php.net/rfc/arithmetic_operator_type_checks.

这里是一个代码示例来说明 PHP8 中算术运算符处理差异:

  1. 首先,我们定义要在算术运算中测试的样本非标量数据:

    // /repo/ch06/php8_arith_non_scalar_ops.php
    $fn  = __DIR__ . '/../sample_data/gettysburg.txt';
    $fh  = fopen($fn, 'r');
    $obj = new class() { public $val = 99; };
    $arr = [1,2,3];
  2. 然后,我们尝试将整数99添加到资源、对象中,并对数组

    echo "Adding 99 to a resource\n";
    try { var_dump($fh + 99); }
    catch (Error $e) { echo $e . "\n"; }
    echo "\nAdding 99 to an object\n";
    try { var_dump($obj + 99); }
    catch (Error $e) { echo $e . "\n"; }
    echo "\nPerforming array % 99\n";
    try { var_dump($arr % 99); }
    catch (Error $e) { echo $e . "\n"; }

    执行模运算

  3. 最后,我们将两个数组添加到一起:

    echo "\nAdding two arrays\n";
    try { var_dump($arr + [99]); }
    catch (Error $e) { echo $e . "\n"; }

当我们运行代码示例时,请注意 PHP7 如何执行静默转换并允许操作继续:

root@php8_tips_php7 [ /repo/ch06 ]# 
php php8_arith_non_scalar_ops.php 
Adding 99 to a resource
/repo/ch06/php8_arith_non_scalar_ops.php:10:
int(104)
Adding 99 to an object
PHP Notice:  Object of class class@anonymous could not be converted to int in /repo/ch06/php8_arith_non_scalar_ops.php on line 13
/repo/ch06/php8_arith_non_scalar_ops.php:13:
int(100)
Performing array % 99
/repo/ch06/php8_arith_non_scalar_ops.php:16:
int(1)
Adding two arrays
/repo/ch06/php8_arith_non_scalar_ops.php:19:
array(3) {
  [0] =>  int(1)
  [1] =>  int(2)
  [2] =>  int(3)
}

特别令人惊讶的是我们如何对数组执行模运算!当向对象添加值时,PHP7 中会生成一个Notice。但是,PHP 类型会将对象变为一个值为1的整数,从而使算术运算的结果为100

在 PHP 8 中运行相同代码示例的输出非常不同:

root@php8_tips_php8 [ /repo/ch06 ]# 
php php8_arith_non_scalar_ops.php
Adding 99 to a resource
TypeError: Unsupported operand types: resource + int in /repo/ch06/php8_arith_non_scalar_ops.php:10
Adding 99 to an object
TypeError: Unsupported operand types: class@anonymous + int in /repo/ch06/php8_arith_non_scalar_ops.php:13
Performing array % 99
TypeError: Unsupported operand types: array % int in /repo/ch06/php8_arith_non_scalar_ops.php:16
Adding two arrays
array(3) {
  [0]=>  int(1)
  [1]=>  int(2)
  [2]=>  int(3)
}

从输出中可以看到,除了添加两个数组外,PHP8 始终抛出一个TypeError。在两个输出中,您可以观察到当添加两个数组时,第二个操作数被忽略。如果目标是组合两个数组,则必须使用array_merge()

现在,让我们将注意力转向 PHP8 字符串处理中与优先顺序相关的潜在重大变化。

检查优先顺序的变化

优先顺序,也称为操作顺序,或运算符优先顺序,是 18 世纪末 19 世纪初建立的一个数学概念。PHP 还采用了数学运算符优先规则,并添加了一个独特的附加项:串联运算符。PHP 语言的创始人假设连接运算符的优先级与算术运算符相同。在 PHP8 出现之前,这一假设从未受到质疑。

在 PHP8 中,算术运算的优先级高于串联运算。串联运算符降级现在将其置于位移位运算符(<<>>之下)。在任何不使用括号明确定义混合算术和串联运算的地方,都可能存在向后兼容中断。

这种变化本身不会抛出一个Error或生成WarningsNotices,因此可能出现隐藏代码中断。

提示

有关此更改原因的更多信息,请参阅以下链接:

https://wiki.php.net/rfc/concatenation_precedence

以下示例最清楚地显示了这种变化的影响:

echo 'The sum of 2 + 2 is: ' . 2 + 2;

以下是 PHP 7 中此简单语句的输出:

root@php8_tips_php7 [ /repo/ch06 ]# 
php -r "echo 'The sum of 2 + 2 is: ' . 2 + 2;"
PHP Warning:  A non-numeric value encountered in Command line code on line 1
2

在 PHP 7 中,由于连接运算符的优先级与加法运算符相同,因此字符串The sum of 2 + 2 is:首先与整数值2连接。然后将新字符串的类型变为整数,生成一个Warning。新字符串的值在0处求值,然后将其与整数2相加,产生2的输出。

然而,在 PHP8 中,加法首先发生,然后将结果与初始字符串连接起来。以下是在 PHP 8 中运行的结果:

root@php8_tips_php8 [ /repo/ch06 ]# 
php -r "echo 'The sum of 2 + 2 is: ' . 2 + 2;"
The sum of 2 + 2 is: 4

从输出中可以看出,结果更接近人类的期望!

再举一个例子,说明降级串联运算符可能产生的差异。请看一下这行代码:

echo '1' . '11' + 222;

以下是在 PHP 7 中运行的结果:

root@php8_tips_php7 [ /repo/ch06 ]# 
php -r "echo '1' . '11' + 222;"
333

PHP7 首先执行连接,生成一个字符串111。将该类型变戏法并添加到整数222,得到最终值整数333。以下是在 PHP 8 中运行的结果:

root@php8_tips_php8 [ /repo/ch06 ]# 
php -r "echo '1' . '11' + 222;"
1233

在 PHP8 中,第二个字符串11经过类型转换并添加到整数222,生成一个中间值233。该类型被篡改为字符串,并在前面加上1,最终字符串值为1233

现在您已经了解了 PHP8 中算术、按位和连接操作的变化,让我们来看看 PHP8 中引入的一个新趋势:语言环境独立性。

在 PHP8 之前的 PHP 版本中,几个字符串函数和操作与语言环境相关联。最终的结果是,数字在内部的存储方式因地区而异。这种做法带来了极难发现的微妙矛盾。在阅读了本章介绍的内容之后,您将能够更好地检测 PHP8 升级后潜在的应用代码更改,从而避免应用失败。

了解与区域依赖相关的问题

在早期的 PHP 版本中,语言环境依赖的不幸的副作用是,当从floatstring再返回时,结果不一致。当float值连接到string时,也会出现不一致。OpCache执行的某些优化操作导致在设置区域设置之前发生连接操作,这是另一种可能产生不一致结果的方式。

在 PHP8 中,易受攻击的操作和函数现在与语言环境无关。这意味着所有浮点值现在都使用句点作为十进制分隔符进行存储。默认情况下,默认区域设置不再从环境继承。如果需要设置默认语言环境,现在必须显式调用setlocale()

审查受区域独立性影响的功能和操作

大多数 PHP 函数不受切换到语言环境独立性的影响,原因很简单,语言环境与该函数或扩展无关。此外,大多数 PHP 函数和扩展已经与语言环境无关。示例包括PDO扩展,以及var_export()json_encode()等函数,以及printf()系列。

受区域设置独立性影响的功能和操作包括:

  • (string) $float
  • strval($float)
  • print_r($float)
  • var_dump($float)
  • debug_zval_dump($float)
  • settype($float, "string")
  • implode([$float])
  • xmlrpc_encode($float)

下面是一个代码示例,说明了如何处理由于区域设置独立性而产生的差异:

  1. 首先,我们定义一个要测试的区域设置数组。选择的区域设置使用不同的方式表示数字的小数部分:

    // /repo/ch06/php8_locale_independent.php
    $list = ['en_GB', 'fr_FR', 'de_DE'];
    $patt = "%15s | %15s \n";
  2. 然后,我们在区域设置中循环,设置区域设置,执行浮点到字符串,然后执行字符串到浮点的转换,在每个步骤中回显结果:

    foreach ($list as $locale) {
        setlocale(LC_ALL, $locale);
        echo "Locale          : $locale\n";
        $f = 123456.789;
        echo "Original        : $f\n";
        $s = (string) $f;
        echo "Float to String : $s\n";
        $r = (float) $s;
        echo "String to Float : $r\n";
    }

如果我们在 PHP7 中运行这个示例,请注意的结果:

root@php8_tips_php7 [ /repo/ch06 ]# 
php php8_locale_independent.php
Locale          : en_GB
Original        : 123456.789
Float to String : 123456.789
String to Float : 123456.789
Locale          : fr_FR
Original        : 123456,789
Float to String : 123456,789
String to Float : 123456
Locale          : de_DE
Original        : 123456,789
Float to String : 123456,789
String to Float : 123456

从输出中可以看出,数字在内部存储时使用句点作为十进制分隔符,用于en_GB,而逗号用于区域设置fr_FRde_DE。但是,当字符串转换回数字时,如果十进制分隔符不是句点,则该字符串将被视为前导数字字符串。在其中两个地区,逗号的存在会停止转换过程。净影响是小数部分被删除,精度损失。

在 PHP 8 中运行相同代码示例的结果如下所示:

root@php8_tips_php8 [ /repo/ch06 ]# 
php php8_locale_independent.php
Locale          : en_GB
Original        : 123456.789
Float to String : 123456.789
String to Float : 123456.789
Locale          : fr_FR
Original        : 123456.789
Float to String : 123456.789
String to Float : 123456.789
Locale          : de_DE
Original        : 123456.789
Float to String : 123456.789
String to Float : 123456.789

在 PHP8 中,不会丢失精度,并且无论语言环境如何,数字始终使用小数分隔符的句点表示。

请注意,您仍然可以使用number_format()函数或NumberFormatter类(来自Intl扩展名)根据区域设置表示数字。有趣的是,NumberFormatter类以独立于语言环境的方式在内部存储数字!

提示

有关更多信息,请参阅本文:https://wiki.php.net/rfc/locale_independent_float_to_string.

有关国际号码格式的详细信息,请参阅以下链接:https://www.php.net/manual/en/class.numberformatter.php

既然您已经了解了 PHP8 中存在的与语言环境无关的方面,那么我们需要看看数组处理方面的变化。

除了性能上的改进外,PHP8 数组处理的两个主要变化涉及负偏移量的处理和大括号({}的使用。由于这两种更改都可能导致 PHP8 迁移后的应用代码中断,因此在这里介绍它们很重要。意识到这里提出的问题,您将有更好的机会在短时间内使损坏的代码重新工作。

让我们先看看负数组偏移处理。

处理负偏移量

在 PHP 中为数组赋值时,如果不指定索引,PHP 将自动为您赋值。以这种方式选择的索引是一个整数,表示比当前分配的最高整数键高一个的值。如果尚未分配整数索引键,则自动索引分配算法从零开始。

然而,在 PHP7 及以下版本中,对于负整数索引,该算法的应用并不一致。如果数值数组的索引以负数开头,则无论下一个数字通常是什么,自动索引都会跳到零(0。另一方面,在 PHP8 中,无论索引是负整数还是正整数,自动索引始终以+1的值递增。

如果代码依赖于自动索引,并且任何起始索引都是负数,则可能存在向后兼容的代码中断。由于自动索引在没有任何WarningsNotices的情况下悄无声息地进行,因此很难检测到此问题。

以下代码示例说明了 PHP 7 和 PHP 8 之间的行为差异:

  1. 首先,我们定义一个只包含负整数作为索引的数组。我们使用var_dump()来展示这个数组:

    // /repo/ch06/php8_array_negative_index.php
    $a = [-3 => 'CCC', -2 => 'BBB', -1 => 'AAA'];
    var_dump($a);
  2. 然后定义第二个数组,并将第一个索引初始化为-3。然后添加额外的数组元素,但不指定索引。这会导致自动索引发生:

    $b[-3] = 'CCC';
    $b[] = 'BBB';
    $b[] = 'AAA';
    var_dump($b);
  3. 如果我们在 PHP7 中运行该程序,请注意第一个数组被正确呈现。在 PHP7 和更早版本中,完全可以使用负数组索引,只要它们是直接赋值的。以下是输出:

    root@php8_tips_php7 [ /repo/ch06 ]# 
    php php8_array_negative_index.php 
    /repo/ch06/php8_array_negative_index.php:6:
    array(3) {
      [-3] =>  string(3) "CCC"
      [-2] =>  string(3) "BBB"
      [-1] =>  string(3) "AAA"
    }
    /repo/ch06/php8_array_negative_index.php:12:
    array(3) {
      [-3] =>  string(3) "CCC"
      [0] =>  string(3) "BBB"
      [1] =>  string(3) "AAA"
    }
  4. 但是,正如您可以从第二个var_dump()输出中看到的,自动数组索引跳到零,而不考虑之前的高值。

  5. 另一方面,在 PHP8 中,可以看到输出是一致的。以下是 PHP8 输出:

    root@php8_tips_php8 [ /repo/ch06 ]# 
    php php8_array_negative_index.php 
    array(3) {
      [-3]=>  string(3) "CCC"
      [-2]=>  string(3) "BBB"
      [-1]=>  string(3) "AAA"
    }
    array(3) {
      [-3]=>  string(3) "CCC"
      [-2]=>  string(3) "BBB"
      [-1]=>  string(3) "AAA"
    }
  6. As you can see from the output, the array indexes are automatically assigned, incremented by a value of 1, making the two arrays identical.

    提示

    有关此增强功能的更多信息,请参阅本文:https://wiki.php.net/rfc/negative_array_index.

现在,您已经了解了关于涉及负值的索引的自动赋值的潜在代码中断,让我们将注意力转向另一个感兴趣的领域:大括号的使用。

处理花括号使用变化

花括号({}对于任何创建 PHP 代码的开发人员来说都是熟悉的场景。用 C 编写的 PHP 语言广泛使用 C 语法,包括大括号。众所周知,大括号用于描述控制结构(例如,if {}、循环(例如,for () {}、函数(例如,function xyz() {})和类中的代码块。

然而,在本小节中,我们将把对花括号用法的检查限制在与变量相关的范围内。PHP8 中一个潜在的重大变化是使用大括号来标识数组元素。从 PHP8 开始,现在不推荐使用大括号来指定数组偏移量。

鉴于以下情况,旧用法一直极具争议性:

  • 它的使用很容易与双引号字符串中的大括号混淆。

  • Curly braces cannot be used to make array assignments.

    因此,PHP 核心团队需要要么使用与方括号一致的大括号([ ])。。。或者干脆去掉这个大括号。最后的决定是取消对带有数组的花括号的支持。

    提示

    有关更改背后背景的更多信息,请参阅以下链接:https://wiki.php.net/rfc/deprecate_curly_braces_array_access.

下面是一个代码示例,说明了这一点:

  1. 首先,我们定义一个回调数组,用于说明删除的或非法的花括号用法:

    // /repo/ch06/php7_curly_brace_usage.php
    $func = [
        1 => function () {
            $a = ['A' => 111, 'B' => 222, 'C' => 333];
            echo 'WORKS: ' . $a{'C'} . "\n";},
        2 => function () {
            eval('$a = {"A","B","C"};');
        },
        3 => function () {
            eval('$a = ["A","B"]; $a{} = "C";');
        }
    ];
  2. 然后,我们使用try/catch块通过回调循环,以捕获抛出的错误:

    foreach ($func as $example => $callback) {
        try {
            echo "\nTesting Example $example\n";
            $callback();
        } catch (Throwable $t) {
            echo $t->getMessage() . "\n";
        }
    }

如果我们在 PHP7 中运行该示例,第一个回调就可以工作。第二个和第三个导致抛出一个ParseError

root@php8_tips_php7 [ /repo/ch06 ]# 
php php7_curly_brace_usage.php 
Testing Example 1
WORKS: 333
Testing Example 2
syntax error, unexpected '{'
Testing Example 3
syntax error, unexpected '}'

但是,当我们在 PHP8 中运行相同的示例时,所有示例都不起作用。以下是 PHP 8 的输出:

root@php8_tips_php8 [ /repo/ch06 ]# 
php php7_curly_brace_usage.php 
PHP Fatal error:  Array and string offset access syntax with curly braces is no longer supported in /repo/ch06/php7_curly_brace_usage.php on line 8

此潜在代码中断易于检测。然而,由于代码有许多大括号,您可能必须等待抛出致命的Error来捕获代码中断。

现在您已经了解了 PHP8 中数组处理的变化,让我们看看与安全相关的函数的变化。

PHP 安全特性的任何更改都值得注意。不幸的是,考虑到当今世界的现状,对任何面向 web 的代码的攻击都是必然的。因此,在本节中,我们将讨论 PHP8 中与安全相关的 PHP 函数的几个更改。受影响的功能变更包括以下内容:

  • assert()
  • password_hash()
  • crypt()

此外,PHP8 使用disable_functions指令处理php.ini文件中定义的任何函数的方式也发生了变化。让我们先看看这个指令。

了解禁用功能处理的变化

网络托管公司通常提供大幅折扣的共享托管套餐。客户注册后,托管公司的 IT 人员会在共享服务器上创建一个帐户,分配一个磁盘配额来控制磁盘空间的使用,并在 web 服务上创建一个虚拟主机定义。然而,这类托管公司面临的问题是,允许不受限制地访问 PHP 会给共享托管公司以及同一服务器上的其他用户带来安全风险。

为了解决这个问题,IT 人员通常会为php.ini指令禁用功能分配一个以逗号分隔的功能列表。这样,该列表中的任何函数都不能在该服务器上运行的 PHP 代码中使用。通常在此列表中结束的功能是那些允许操作系统访问的功能,例如system()shell_exec()

只有内部 PHP 函数才能出现在此列表中。内部函数包括在 PHP 核心中的函数以及通过扩展提供的函数。用户定义的函数不受此指令的影响。

检查禁用函数的处理差异

在 PHP7 和更早版本中,无法重新定义禁用的函数。在 PHP8 中,禁用的函数被视为从未存在过,这意味着可以重新定义。

重要提示

仅仅因为您可以在 PHP8中重新定义禁用的函数并不意味着原始功能已经恢复!

为了说明这个概念,我们首先将这一行添加到php.ini文件中:disable_functions=system.

请注意,我们需要将其添加到两个Docker 容器(PHP7 和 PHP8)中,以完成说明。更新php.ini文件的命令如下所示:

root@php8_tips_php7 [ /repo/ch06 ]# 
echo "disable_functions=system">>/etc/php.ini
root@php8_tips_php8 [ /repo/ch06 ]# 
echo "disable_functions=system">>/etc/php.ini

如果我们随后尝试使用system()函数,那么在 PHP7 和 PHP8 中尝试都会失败。这里,我们展示了 PHP 8 的输出:

root@php8_tips_php8 [ /repo/ch06 ]# 
php -r "system('ls -l');"
PHP Fatal error:  Uncaught Error: Call to undefined function system() in Command line code:1

然后我们定义一些重新定义禁用函数的程序代码:

// /repo/ch06/php8_disabled_funcs_redefine.php
function system(string $cmd, string $path = NULL) {
    $output = '';
    $path = $path ?? __DIR__;
    if ($cmd === 'ls -l') {
        $iter = new RecursiveDirectoryIterator($path);
        foreach ($iter as $fn => $obj)
            $output .= $fn . "\n";
    }
    return $output;
}
echo system('ls -l');

正如您从代码示例中看到的,我们创建了一个函数,它模仿了ls -lLinux 系统调用的行为,但只使用安全的 PHP 函数和类。但是,如果我们尝试在 PHP7 中运行这个程序,就会抛出一个致命的Error。以下是 PHP7 的输出:

root@php8_tips_php7 [ /repo/ch06 ]# 
php php8_disabled_funcs_redefine.php 
PHP Fatal error:  Cannot redeclare system() in /repo/ch06/php8_disabled_funcs_redefine.php on line 17

然而,在 PHP 8 中,我们的函数重新定义成功,如下所示:

root@php8_tips_php8 [ /repo/ch06 ]# 
php php8_disabled_funcs_redefine.php 
/repo/ch06/php8_printf_vs_vprintf.php
/repo/ch06/php8_num_str_non_wf_extracted.php
/repo/ch06/php8_vprintf_bc_break.php
/repo/ch06/php7_vprintf_bc_break.php
... not all output is shown ...
/repo/ch06/php7_curly_brace_usage.php
/repo/ch06/php7_compare_num_str_valid.php
/repo/ch06/php8_compare_num_str.php
/repo/ch06/php8_disabled_funcs_redefine.php

您现在了解了如何使用禁用的函数。接下来,让我们看看对重要的crypt()函数的更改。

了解 crypt()函数的更改

自 PHP 版本 4 以来,crypt()函数一直是 PHP 哈希生成的主要部分。其恢复力的原因之一是它有太多的选择。如果您的代码直接使用了crypt(),您会很高兴注意到,如果提供了一个不可用的salt值,那么长期以来被认为已被破坏的防御加密标准(DES)在 PHP8 中不再是的回退!盐有时也被称为初始化载体IV

另一个重要变化涉及值。就像洗一副牌:你洗的次数越多,随机化程度就越高(除非你是在对付一个拉斯维加斯的高利贷者!)。在密码学中,区块类似于卡片。在每一轮中,加密函数应用于每个块。如果加密函数简单,则可以更快地生成散列;但是,需要进行更多的轮次才能完全随机分组。

SHA-1安全哈希算法系列)使用快速但简单的算法,因此需要更多轮数。另一方面,SHA-2 系列使用更复杂的哈希函数,它占用更多资源,但轮数更少。

当在中结合CRYPT_SHA256(SHA-2 系列)使用 PHPcrypt()函数时,PHP8 将不再默默地将rounds参数解析为最近的限制。相反,crypt()将以*0返回失败,与glibc行为匹配。此外,在 PHP8 中,第二个参数(salt)现在是必需的。

以下示例说明了使用crypt()函数时 PHP 7 和 PHP 8 之间的差异:

  1. 首先,我们定义了表示无法使用的 salt 值和非法轮数的变量:

    // /repo/ch06/php8_crypt_sha256.php
    $password = 'password';
    $salt     = str_repeat('+x=', CRYPT_SALT_LENGTH + 1);
    $rounds   = 1;
  2. 然后我们使用crypt()函数创建两个散列。在第一个用法中,$default是在提供了无效的 salt 参数之后的结果。第二种用法$sha256提供了一个有效的 salt 值,但轮数无效:

    $default  = crypt($password, $salt);
    $sha256   = crypt($password, 
        '$5$rounds=' . $rounds . '$' . $salt . '$');
    echo "Default : $default\n";
    echo "SHA-256 : $sha256\n";

以下是在 PHP 7 中运行的代码示例的输出:

root@php8_tips_php7 [ /repo/ch06 ]# 
php php8_crypt_sha256.php 
PHP Deprecated:  crypt(): Supplied salt is not valid for DES. Possible bug in provided salt format. in /repo/ch06/php8_crypt_sha256.php on line 7
Default : +xj31ZMTZzkVA
SHA-256 : $5$rounds=1000$+x=+x=+x=+x=+x=+
$3Si/vFn6/xmdTdyleJl7Rb9Heg6DWgkRVKS9T0ZZy/B

请注意 PHP7 是如何以静默方式修改原始请求的。在第一种情况下,crypt()返回到DES(!)。在第二种情况下,PHP 7 无声地将rounds值从1更改为最接近的限制1000

另一方面,在 PHP 8 中运行的相同代码失败并返回*0,如下所示:

root@php8_tips_php8 [ /repo/ch06 ]# 
php php8_crypt_sha256.php 
Default : *0
SHA-256 : *0

正如我们在本书中反复强调的那样,当 PHP 为您做出假设时,最终您会得到产生不一致结果的糟糕代码。在刚刚显示的代码示例中,最佳实践是定义一个类方法或函数,该类方法或函数对其参数施加更大的控制。通过这种方式,您可以验证参数,避免依赖 PHP 假设。

接下来,我们来看看password_hash()函数的变化。

处理对密码的更改\u hash()

多年来,如此多的开发人员误用了crypt(),以至于 PHP 核心团队决定添加一个包装函数password_hash()。这被证明是一个巨大的成功,现在是应用最广泛的安全功能之一。以下是password_hash()的功能签名:

password_hash(string $password, mixed $algo, array $options=?) 

目前支持的算法包括bcryptArgon2iArgon2id。建议您为算法使用预定义的常量:PASSWORD_BCRYPTPASSWORD_ARGON2IPASSWORD_ARGON2IDPASSWORD_DEFAULT算法当前设置为bcrypt。选项因算法而异。如果您使用PASSWORD_BCRYPTPASSWORD_DEFAULT算法,则选项包括costsalt

传统智慧建议最好使用password_hash()函数创建的随机生成的salt。在 PHP7 中,salt选项被弃用,现在在 PHP8 中被忽略。这不会导致向后兼容中断,除非您出于其他原因依赖salt

在此代码示例中,使用非随机盐值:

// /repo/ch06/php8_password_hash.php
$salt = 'xxxxxxxxxxxxxxxxxxxxxx';
$password = 'password';
$hash = password_hash(
    $password, PASSWORD_DEFAULT, ['salt' => $salt]);
echo $hash . "\n";
var_dump(password_get_info($hash));

在 PHP7 输出中,发布了一个弃用Notice

root@php8_tips_php7 [ /repo/ch06 ]# php php8_password_hash.php PHP Deprecated:  password_hash(): Use of the 'salt' option to password_hash is deprecated in /repo/ch06/php8_password_hash.php on line 6
$2y$10$xxxxxxxxxxxxxxxxxxxxxuOd9YtxiLKHM/l98x//sqUV1V2XTZEZ.
/repo/ch06/php8_password_hash.php:8:
array(3) {
  'algo' =>  int(1)
  'algoName' =>  string(6) "bcrypt"
  'options' =>   array(1) { 'cost' => int(10) }
}

您还将从 PHP7 输出中注意到,非随机salt值清晰可见。另一件需要注意的事情是,当执行password_get_info()时,algo键显示一个对应于预定义算法常数之一的整数值。

PHP 8 的输出有些不同,如下所示:

root@php8_tips_php8 [ /repo/ch06 ]# php php8_password_hash.php PHP Warning:  password_hash(): The "salt" option has been ignored, since providing a custom salt is no longer supported in /repo/ch06/php8_password_hash.php on line 6
$2y$10$HQNRjL.kCkXaR1ZAOFI3TuBJd11k4YCRWmtrI1B7ZDaX1Jngh9UNW
array(3) {
  ["algo"]=>  string(2) "2y"
  ["algoName"]=>  string(6) "bcrypt"
  ["options"]=>  array(1) { ["cost"]=> int(10) }
}

您可以看到,salt值被忽略,而使用了随机的salt。PHP8 发布了一个关于使用salt选项的Warning,而不是Notice。从输出中需要注意的另一点是,当调用password_get_info()时,algorithm键返回一个字符串,而不是 PHP8 中的整数。这是因为预定义的算法常量现在是字符串值,当在crypt()函数中使用时,它们对应于它们的签名。

在下一小节中,我们将研究的最后一个函数是assert()

了解 assert()的更改

assert()功能通常与测试和诊断相关。我们将其包含在本小节中,因为它通常具有安全含义。开发人员有时在试图跟踪潜在的安全漏洞时使用此功能。

要使用assert()功能,您必须首先通过添加php.ini文件设置zend.assertions=1来启用该功能。启用后,您可以在应用代码中的任何位置进行一个或多个assert()函数调用。

了解对 assert()用法的更改

从 PHP8 开始,不再能够用要求值的字符串参数来表示assert():相反,您必须提供一个表达式。这可能导致代码中断,因为在 PHP8 中,字符串被视为表达式,因此总是解析为布尔值TRUE。此外,PHP8 中删除了assert.quiet_eval``php.ini指令和assert_options()使用的ASSERT_QUIET_EVAL预定义常量,因为它们现在没有任何效果。

为了说明潜在的问题,我们首先通过设置php.ini指令zend.assertions=1来激活断言。然后,我们定义一个示例程序,如下所示:

  1. 我们使用ini_set()导致assert()抛出异常。我们还定义了一个变量,$pi

    // /repo/ch06/php8_assert.php
    ini_set('assert.exception', 1);
    $pi = 22/7;
    echo 'Value of 22/7: ' . $pi . "\n";
    echo 'Value of M_PI: ' . M_PI . "\n";
  2. 然后我们尝试将断言作为一个表达式,$pi === M_PI

    try {
        $line    = __LINE__ + 2;
        $message = "Assertion expression failed ${line}\n";
        $result  = assert($pi === M_PI, 
            new AssertionError($message));
        echo ($result) ? "Everything's OK\n"
                       : "We have a problem\n";
    } catch (Throwable $t) {
        echo $t->getMessage() . "\n";
    }
  3. 在最后一个try/catch块中,我们尝试将断言作为字符串:

    try {
        $line    = __LINE__ + 2;
        $message = "Assertion string failed ${line}\n";
        $result  = assert('$pi === M_PI', 
            new AssertionError($message));
        echo ($result) ? "Everything's OK\n" 
                       : "We have a problem\n";
    } catch (Throwable $t) {
        echo $t->getMessage() . "\n";
    }
  4. 当我们在 PHP7 中运行程序时,一切正常:

    root@php8_tips_php7 [ /repo/ch06 ]# php php8_assert.php 
    Value of 22/7: 3.1428571428571
    Value of M_PI: 3.1415926535898
    Assertion as expression failed on line 18
    Assertion as a string failed on line 28
  5. M_PI的值来自数学扩展,比简单地将 22 除以 7 要精确得多!因此,两个断言都会引发异常。然而,在 PHP8 中,输出有很大不同:

    root@php8_tips_php8 [ /repo/ch06 ]# php php8_assert.php 
    Value of 22/7: 3.1428571428571
    Value of M_PI: 3.1415926535898
    Assertion as expression failed on line 18
    Everything's OK

作为字符串的断言被解释为一个表达式。因为字符串不是空的,所以布尔结果为TRUE,返回假阳性。如果代码以字符串形式依赖于断言的结果,那么它肯定会失败。但是,从 PHP8 输出中可以看到,作为表达式的断言在 PHP8 中的工作原理与 PHP7 中的相同。

提示

最佳实践:不要在生产代码中使用assert()。如果使用assert(),请始终提供表达式,而不是字符串。

现在,您已经了解了安全相关功能的更改,我们将结束本章。

在本章中,您了解了 PHP8 和早期版本之间字符串处理的差异,以及如何开发解决字符串处理差异的变通方法。正如您所了解的,PHP8 对字符串函数参数的数据类型施加了更大的控制,并且在参数丢失或为 null 时引入了一致性。正如您所了解的,早期版本的 PHP 的一个大问题是,为了您的利益而默默地做出了一些假设,这导致了产生意外结果的巨大可能性。

在本章中,我们还强调了涉及数字字符串和数字数据之间比较的问题。您不仅了解了数字字符串、类型转换和非严格比较,还了解了 PHP8 如何纠正早期版本中数字字符串处理中固有的缺陷。本章中涉及的另一个主题演示了与 PHP8 中几个操作符的行为不同有关的潜在问题。您学习了如何发现潜在问题,并获得了最佳实践来提高代码的弹性。

本章还讨论了许多 PHP 函数如何保持对区域设置的依赖,以及如何在 PHP8 中解决这个问题。您了解到,在 PHP8 中,浮点表示现在是统一的,不再依赖于语言环境。您还了解了 PHP8 如何处理数组元素的变化以及几个安全相关函数的变化。

本章介绍的技巧、窍门和技巧提高了人们对 PHP 早期版本中不一致行为的认识。有了这种新的认识,您就可以更好地控制 PHP 代码的使用。在 PHP8 迁移之后,您现在也可以更好地检测可能导致潜在代码中断的情况,这使您比其他开发人员更具优势,并最终使您能够编写性能可靠且一致的 PHP 代码。

下一章将向您展示如何避免涉及 PHP 扩展更改的潜在代码中断。

教程来源于Github,感谢apachecn大佬的无私奉献,致敬!

技术教程推荐

说透中台 -〔王健〕

手机摄影 -〔@随你们去〕

如何成为学习高手 -〔高冷冷〕

说透区块链 -〔自游〕

大数据经典论文解读 -〔徐文浩〕

自动化测试高手课 -〔柳胜〕

中间件核心技术与实战 -〔丁威〕

运维监控系统实战笔记 -〔秦晓辉〕

深入拆解消息队列47讲 -〔许文强〕