学习关键的高级字符串处理差异
在 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()
。所有这些函数都有两个共同的参数:needle 和 haystack。
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 中会产生不同的结果。下面是代码示例:
-
首先,我们定义一个函数,使用
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); };
-
然后,我们将 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()
函数的设计是接受数组作为参数,而不是接受无限制的一系列参数。下面是一个简单的示例,可以说明两者的区别:
-
首先,我们定义一组将插入到模式
$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";
-
然后我们使用一系列参数执行
printf()
语句:printf($patt, $ord, $day, $pos, $date->format('l, d M Y'));
-
然后,我们将参数定义为数组
$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 则抛出了错误:
-
首先,我们定义模式和源数组:
// /repo/ch06/php7_vprintf_bc_break.php $patt = "\t%s. %s. %s. %s. %s."; $arr = ['Person', 'Woman', 'Man', 'Camera', 'TV'];
-
然后,我们定义一个测试数据数组,以测试
vsprintf()
可以接受哪些参数:$args = [ 'Array' => $arr, 'Int' => 999, 'Bool' => TRUE, 'Obj' => new ArrayObject($arr) ];
-
然后,我们定义一个
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 返回字符串的剩余部分。如果使用操作结果来确认或否认子串的存在,则很有可能导致代码断开:
-
首先,我们定义一个 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);
-
接下来,我们取出子串,故意不定义长度参数:
$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 中引入的关键字符串处理差异有所了解了。接下来,我们将关注数字字符串与数字比较方式的变化。