学习关键的高级字符串处理差异

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

这些改进在处理数字字符串时尤为明显。PHP 8 在字符串处理方面的其他变化涉及参数的细微变化。在本节中,我们将介绍 PHP 8 在处理字符串方面的主要变化。

重要的是,不仅要了解 PHP 8 中引入的处理改进,还要了解 PHP 8 之前在字符串处理方面的不足。

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

处理对 Needle 参数的更改

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

needle 和 haystack 区别

为了说明 needle 和 haystack 之间的区别,请看 strpos() 的函数签名:

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

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

PHP 8 在处理针参数方面的两个关键变化有可能会破坏迁移到 PHP 8 的应用程序。这些变化适用于针参数不是字符串或针参数为空的情况。让我们先看看非字符串针参数的处理。

处理非字符串 needle 参数

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

如果需要提供 ASCII 值,则必须使用 chr() 函数将其转换为字符串。在下面的示例中,使用的是 LF("\n") 的 ASCII 值而不是字符串。在 PHP 7 或以下版本中,strpos() 会在运行搜索之前执行内部转换。在 PHP 8 中,数字被简单地键入到字符串中,从而产生了意想不到的结果。

下面是一个搜索字符串中是否存在 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

正如您所看到的,比较 PHP 7 和 PHP 8 的输出结果,同样的代码块产生了完全不同的结果。这是一个极难发现的潜在代码错误,因为不会产生警告或错误。

最好的做法是对任何使用了 PHP str*() 函数的函数或方法的针参数应用字符串类型提示。如果我们重写前面的示例,输出结果在 PHP 7 和 PHP 8 中都是一致的。下面是使用类型提示重写的示例:

// /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 参数前添加字符串类型提示,任何误用您代码的开发人员都会收到一个明确的提示,即这种做法是不可接受的。

现在让我们看看在 PHP 8 中缺少 needle 参数时会发生什么。

处理空 needle 参数

str*() 函数的另一个重大变化是,针参数现在可以为空(例如,任何会使 empty() 函数返回 TRUE 的参数)。这很有可能导致向后兼容问题。在 PHP 7 中,如果针参数为空,strpos() 的返回值将是布尔值 FALSE,而在 PHP 8 中,空值将首先转换为字符串,从而产生完全不同的结果。

如果您计划将 PHP 版本升级到 8,那么意识到这一潜在的代码错误是非常重要的。手动审查代码时很难发现空 needle 参数。在这种情况下,需要一套可靠的单元测试来确保 PHP 移植的顺利进行。

为了说明潜在的问题,请看下面的例子。假设 needle 参数为空。在这种情况下,传统的 if() 检查 strpos() 的结果是否与 FALSE 相同,在 PHP 7 和 PHP 8 中会产生不同的结果。下面是代码示例:

  1. 首先,我们定义一个函数,使用 strpos() 报告是否在 haystack 中找到 needle 值。注意针对布尔值 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. 然后,我们将 haystack 定义为一个包含字母和数字的字符串。needle 参数以数组的形式提供,所有值都被视为空值:

    $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

在一系列警告之后,最终输出结果出现了。从输出中可以看到,在 PHP 7 中,strpos($haystack, $search) 的返回值一直是布尔值 FALSE

然而,在 PHP 8 中运行相同代码的输出却截然不同。下面是 PHP 8 的输出结果:

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

在 PHP 8 中,空 needle 参数首先会被静默转换为字符串。没有一个 needle 值返回布尔值 FALSE。这会导致函数报告已找到针。这当然不是我们想要的结果。然而,在数字 0 的情况下,它被包含在 haystack 中,结果返回值为 19。

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

使用 str_contains() 解决问题

上一节中显示的代码块的目的是确定 haystack 是否包含 needle。 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);
};

如果我们随后在 PHP 8 中运行修改后的代码,我们将得到与 PHP 7 类似的结果:

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() 系列;在 PHP 8 中,它是另一个对参数进行更严格控制的字符串函数系列。

处理 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. 然后,我们将参数定义为数组 $arr 并使用 vprintf() 得出相同的结果:

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

    下面是程序在 PHP 8 中运行的输出结果。在 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() 系列函数的参数进行随意的处理。在 PHP 8 中,参数的数据类型现在被严格执行。这只会在没有代码控制来确保数组的情况下产生问题。另一个更重要的区别是,PHP 7 允许在 v*printf() 中使用 ArrayObject,而 PHP 8 则不允许。

在本例中,PHP 7 发出了警告,而 PHP 8 则抛出了错误:

  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.

从输出中可以看到,PHP 7 中接受数组和 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

不出所料,PHP 8 的输出更加一致。在 PHP 8 中,v*printf() 函数的类型严格限定为只接受数组作为参数。不幸的是,您很可能使用了 ArrayObject。只需在 ArrayObject 实例上使用 getArrayCopy() 方法,返回一个数组,就能轻松解决这个问题。

下面是重写后的代码,在 PHP 7 和 PHP 8 中都可以使用:

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

现在您已经知道在使用 v*printf() 函数时应该从哪里查找潜在的代码错误,让我们来看看在 PHP 8 中带有空长度参数的字符串函数在工作方式上的差异。

在 PHP 8 中使用空长度参数

在 PHP 7 及更早版本中,长度参数为 NULL 会导致空字符串。在 PHP 8 中,NULL 长度参数的处理方法与省略长度参数相同。受影响的函数如下:

  • substr()

  • substr_count()

  • substr_compare()

  • iconv_substr()

在下例中,PHP 7 返回空字符串,而 PHP 8 返回字符串的剩余部分。如果使用操作结果来确认或否认子串的存在,则很有可能导致代码断开:

  1. 首先,我们定义一个 haystack 和一根 needle。然后,我们运行 strpos() 来获取 needle 在 haystack 中的位置:

    // /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:

不出所料,PHP 7 发出了通知。但是,由于 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

PHP 8 会发出警告并返回字符串的剩余部分。这与完全省略 length 参数的行为一致。如果您的代码依赖于空字符串的返回,那么在 PHP 8 更新后可能会出现代码错误。

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

检查 implode() 的更改

两个广泛使用的 PHP 函数执行数组到字符串的转换和反向转换:explode() 将字符串转换为数组,而 implode() 将数组转换为字符串。然而,implode() 函数潜藏着一个不可告人的秘密:它的两个参数可以以任何顺序表达!

请记住,当 PHP 于 1994 年首次推出时,最初的目标是使其尽可能易于使用。这种方法取得了成功,根据 w3techs 最近对服务器端编程语言的调查,如今 PHP 已成为超过 78% 的网络服务器的首选语言。( 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() 以错误的顺序接收参数是非常困难的。在迁移 PHP 8 之前,最好的办法是记录下所有使用 implode() 的 PHP 文件类。另一个建议是利用 PHP 8 的命名参数功能(见第 1 章,PHP 8 OOP 新功能介绍)。

了解 PHP 8 中常量的用法

在 PHP 第 8 版之前,它有一个非常了不起的功能,就是可以定义不区分大小写的常量。在 PHP 问世之初,许多开发人员都在编写大量的 PHP 代码,但却没有任何编码标准。当时的目标只是让代码正常工作。

为了与执行良好编码标准的大趋势保持一致,PHP 7.3 废弃了这一功能,并在 PHP 8 中删除了它。如果使用 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";

在 PHP 7 中,所有代码行都按编写的运行。下面是输出结果:

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() 的第三个参数在 PHP 7.3 中已被弃用。因此,如果在 PHP 7.3 或 7.4 中运行此代码示例,输出结果是相同的,只是增加了一个弃用通知。

但在 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 行产生了预期的结果。但最后一行却出现了致命的错误,因为在 PHP 8 中常量是区分大小写的。此外,由于第三个参数在 PHP 8 中被忽略,所以第三行 define() 语句发出了警告。

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