避免更新的 mbstring 扩展出现问题
mbstring
扩展在 PHP 4 中首次引入,此后一直是该语言的一个活跃部分。该扩展的最初目的是为各种日文字符编码系统提供支持。从那时起,又增加了对其他各种编码的支持,其中最引人注目的是对基于通用编码字符集 2 (UTC-2)、UTC-4、Unicode 转换格式 8 (UTF-8)、UTF-16、UTF-32、Shift Japanese Industrial Standards (SJIS) 和国际标准化组织 8859 (ISO-8859) 等编码的支持。
如果不确定服务器支持哪些编码,只需运行 mb_list_encodings()
命令,如下所示(显示部分输出):
root@php8_tips_php7 [ /repo/ch07 ]#
php -r "var_dump(mb_list_encodings());"
Command line code:1:
array(87) {
... only selected output is shown ...
[14] => string(7) "UCS-4BE"
[16] => string(5) "UCS-2"
[19] => string(6) "UTF-32"
[22] => string(6) "UTF-16"
[25] => string(5) "UTF-8"
[26] => string(5) "UTF-7"
[27] => string(9) "UTF7-IMAP"
[28] => string(5) "ASCII"
[29] => string(6) "EUC-JP"
[30] => string(4) "SJIS"
[31] => string(9) "eucJP-win"
[32] => string(11) "EUC-JP-2004"
[76] => string(6) "KOI8-R"
[78] => string(9) "ArmSCII-8"
[79] => string(5) "CP850"
[80] => string(6) "JIS-ms"
[81] => string(16) "ISO-2022-JP-2004"
[86] => string(7) "CP50222"
}
从前面的输出中可以看到,在本书使用的 PHP 7.1 Docker 容器中,支持 87 种编码。在 PHP 8.0 Docker 容器中(输出未显示),支持 80 种编码。现在让我们看看 PHP 8 中引入的变化,首先是 mb_str*()
函数。
发现 mb_str*() 函数中的 need-argument 差异
在 第 6 章 了解 PHP 8 的功能差异 中,您了解了 PHP 8 如何在核心 str*pos()
、str*str()
和 str*chr()
函数中引入了对针状参数处理的更改。针参数的两个主要区别是可以接受空针参数和严格的类型检查,以确保针参数仅为字符串。为了保持一致性,PHP 8 在相应的 mb_str*()
函数中引入了相同的变化。
让我们先看看空 needle-argument 的处理。
mb_str*() 函数空 needle-argument 处理
为了使 mbstring
扩展与核心字符串函数的变化保持一致,以下 mbstring
扩展函数现在允许使用空针参数。需要注意的是,这并不意味着该参数可以省略或可有可无!这一变化的意思是,作为针形参数提供的任何值现在也可以包括被认为是空的值。了解 PHP 认为什么是空的一个快速的好方法可以在 empty()
函数的文档中找到( https://www.php.net/empty )。以下是允许空针参数值的 mbstring
函数列表:
-
mb_strpos()
-
mb_strrpos()
-
mb_stripos()
-
mb_strripos()
-
mb_strstr()
-
mb_stristr()
-
mb_strrchr()
-
mb_strrichr()
这里提到的 8 个 |
下面的简短代码示例说明了上述八个函数中的空针处理。下面是实现这一功能的步骤:
-
首先,我们初始化一个多字节文本字符串。在下面的示例中,这是泰语翻译的 The quick brown fox jumped over the fence。将针参数设置为
NULL
,并初始化要测试的函数数组:// /repo/ch07/php8_mb_string_empty_needle.php $text = 'สุนัขจิ้งจอกสีน้ำ�ต�ลกระโดดข้�มรั้วอย่�งรวดเร็ว'; $needle = NULL; $funcs = ['mb_strpos', 'mb_strrpos', 'mb_stripos', 'mb_strripos', 'mb_strstr', 'mb_stristr', 'mb_strrchr', 'mb_strrichr'];
-
然后,我们定义一个
printf()
模式,并循环使用要测试的函数。每次调用函数时,我们都会提供文本,后面跟一个空 needle 参数,如下所示:$patt = "Testing: %12s : %s\n"; foreach ($funcs as $str) printf($patt, $str, $str($text, $needle));
PHP 7 的输出如下所示:
root@php8_tips_php7 [ /repo/ch07 ]#
php php8_mb_string_empty_needle.php
PHP Warning: mb_strpos(): Empty delimiter in /repo/ch07/php8_
mb_string_empty_needle.php on line 12
Testing: mb_strpos :
Testing: mb_strrpos :
PHP Warning: mb_stripos(): Empty delimiter in /repo/ch07/php8_
mb_string_empty_needle.php on line 12
Testing: mb_stripos :
Testing: mb_strripos :
PHP Warning: mb_strstr(): Empty delimiter in /repo/ch07/php8_
mb_string_empty_needle.php on line 12
Testing: mb_strstr :
PHP Warning: mb_stristr(): Empty delimiter in /repo/ch07/php8_
mb_string_empty_needle.php on line 12
Testing: mb_stristr :
Testing: mb_strrchr :
Testing: mb_strrichr :
可以看到,输出是空白的,在某些情况下还会发出警告信息。在 PHP 8 中运行的输出结果与预期的完全不同,我们可以从这里看到:
root@php8_tips_php8 [ /repo/ch07 ]#
php php8_mb_string_empty_needle.php
Testing: mb_strpos : 0
Testing: mb_strrpos : 46
Testing: mb_stripos : 0
Testing: mb_strripos : 46
Testing: mb_strstr : สุนัขจิ้งจอกสีน้ำ�ต�ลกระโดดข้�มรั้วอย่�งรวดเร็ว
Testing: mb_stristr : สุนัขจิ้งจอกสีน้ำ�ต�ลกระโดดข้�มรั้วอย่�งรวดเร็ว
Testing: mb_strrchr :
Testing: mb_strrichr :
值得注意的是,当这段代码在 PHP 8 中运行时,空针参数在 mb_strpos()
和 mb_stripos()
中的返回值是整数 0
,而在 mb_strrpos()
和 mb_strripos()
中的返回值是整数 46。在 PHP 8 中,空针参数在这种情况下被解释为字符串的开始或结束。mb_strstr()
和 mb_stristr()
的结果都是整个字符串。
mb_str*() 函数数据类型检查
为了与核心 str*()
函数保持一致,在相应的 mb_str*()
函数中,针参数必须是字符串类型。如果提供的是美国信息交换标准码(ASCII)值而不是字符串,受影响的函数将产生 ArgumentTypeError
错误。本小节没有示例,因为 第 6 章 了解 PHP 8 的功能差异 已经提供了核心 str*()
函数中这种差异的示例。
mb_strrpos() 中的差异
在早期的 PHP 版本中,允许将字符编码作为第三个参数传递给 mb_strrpos()
,而不是偏移量。PHP 8 不再支持这种糟糕的做法,取而代之的是提供 0 作为第三个参数,或者考虑使用 PHP 8 命名参数(在第 1 章 "PHP 8 OOP 新特性介绍" 中的 "理解命名参数" 一节中讨论),以避免提供一个值作为可选参数。
现在我们来看一个代码示例,它演示了 PHP 7 和 PHP 8 在处理参数方面的不同。步骤如下:
-
我们首先定义一个常量来表示我们希望使用的字符编码。我们指定了一个文本字符串,代表 The quick brown fox jumped over the fence 的泰语翻译。然后,我们使用
mb_convert_encoding()
来确保使用正确的编码。代码示例如下:// /repo/ch07/php7_mb_string_strpos.php define('ENCODING', 'UTF-8'); $text = 'สุนัขจิ้งจอกสีน้ำ�ต�ลกระโดดข้�มรั้วอย่�งรวดเร็ว'; $encoded = mb_convert_encoding($text, ENCODING);
-
然后,我们将 fence 的泰语翻译赋值给
$needle
,并回显字符串的长度和$needle
在文本中的位置。然后,我们调用mb_strrpos()
查找$needle
最后出现的位置。请注意,在下面的代码片段中,我们故意使用编码作为第三个参数,而不是偏移量:$needle = 'รั้ว'; echo 'String Length: ' . mb_strlen($encoded, ENCODING) . "\n"; echo 'Substring Pos: ' . mb_strrpos($encoded, $needle, ENCODING) . "\n";
正如我们在这里所看到的,该代码示例的输出在 PHP 7 中完美运行:
root@php8_tips_php7 [ /repo/ch07 ]#
php php7_mb_string_strpos.php
String Length: 46
Substring Pos: 30
从前面的输出可以看出,多字节字符串的长度是 46,针的位置是 30。而在 PHP 8 中,我们会得到一条致命的 "未捕获类型错误"(Uncaught TypeError)信息,如图所示:
root@php8_tips_php8 [ /repo/ch07 ]#
php php7_mb_string_strpos.php
String Length: 46
PHP Fatal error: Uncaught TypeError: mb_strrpos(): Argument
#3 ($offset) must be of type int, string given in /repo/ch07/
php7_mb_string_strpos.php:14
从 PHP 8 的输出中可以看到,mb_strrpos()
的第三个参数必须是一个整数形式的偏移值。重写此示例的一个简单方法是利用 PHP 8 命名参数 的优势。下面是重写后的代码行:
echo 'Substring Pos: '
. mb_strrpos($encoded, $needle, encoding:ENCODING) . "\n";
输出结果与 PHP 7 示例相同,此处不再显示。现在我们来看看 mbstring
扩展的正则表达式(regex)处理差异。
检查 mb_ereg*() 函数的更改
mb_ereg*()
系列函数允许对使用多字节字符集编码的字符串进行 regex 处理。相比之下,PHP 核心语言提供了 Perl 兼容正则表达式(PCRE)系列函数,具有更现代、更新的功能。
在使用 PCRE 函数时,如果在 regex 模式中添加 u(小写字母 U)修饰符,则可以接受任何 UTF-8 编码的多字节字符串。不过,UTF-8 是唯一可接受的多字节字符编码。如果要处理其他字符编码并执行 regex 功能,则需要转换为 UTF-8 或使用 mb_ereg*()
系列函数。现在让我们来看看 mb_ereg*()
系列函数的一些变化。
PHP 8 需要 Oniguruma 库
这一系列函数的一个变化在于 PHP 安装的编译方式。在 PHP 8 中,操作系统必须提供 libonig
库。该库提供了 Oniguruma 功能。(更多信息请参见 https://github.com/kkos/oniguruma 。)旧的 --with-onig
PHP source-compile-configure 选项已被删除,转而使用 pkg-config
来检测 libonig
。
对 mb_ereg_replace() 的修改
以前,你可以提供一个整数作为参数给 mb_ereg_replace()
。该参数被解释为 ASCII 码。在 PHP 8 中,这种参数现在被类型转换为字符串。如果需要 ASCII 码,则需要使用 mb_chr()
。由于将参数类型转换为字符串是静默进行的,因此可能会出现向后兼容的代码中断,即看不到任何 "注意"(Notice)或 "警告"(Warning)信息。
下面的程序代码示例说明了 PHP 7 和 PHP 8 之间的差异。接下来我们将按照这些步骤进行:
-
首先,我们定义要使用的编码,并将 Two quick brown foxes jumped over the fence 的泰语翻译作为多字节字符串赋值给
$text
。接下来,我们使用mb_convert_encoding()
来确保使用正确的编码。然后,我们使用mb_regex_encoding()
将mb_ereg*
设置为选定的编码。下面的代码片段对代码进行了说明:// /repo/ch07/php7_mb_string_strpos.php define('ENCODING', 'UTF-8'); $text = 'สุนัขจิ้งจอกสีน้ำ�ต�ล 2 ตัวกระโดดข้�มรั้ว'; $str = mb_convert_encoding($text, ENCODING); mb_regex_encoding(ENCODING);
-
然后,我们调用
mb_ereg_replace()
,提供一个整数值 50 作为第一个参数,并将其替换为字符串 "3"。原始字符串和修改后的字符串都会回传。您可以在此处查看代码:$mod1 = mb_ereg_replace(50, '3', $str); echo "Original: $str\n"; echo "Modified: $mod1\n";
请注意,mb_ereg_replace()
的第一个参数应该是字符串,但我们提供了一个整数。在 PHP 8 之前的 mbstring
扩展版本中,如果第一个参数是一个整数,它将被视为 ASCII 码。
如果我们在 PHP 7 中运行此代码示例,数字 50 将被解释为 "2" 的 ASCII 码点值,正如我们所期望的那样:
root@php8_tips_php7 [ /repo/ch07 ]#
php php7_mb_string_ereg_replace.php
Original: สุนัขจิ้งจอกสีน้ำ�ต�ล 2 ตัวกระโดดข้�มรั้ว
Modified: สุนัขจิ้งจอกสีน้ำ�ต�ล 3 ตัวกระโดดข้�มรั้ว
从前面的输出中可以看到,数字 2 被数字 3 所替换。但在 PHP 8 中,数字 50 被键入到一个字符串中。由于这个源字符串不包含数字 50,因此不会进行任何替换,我们可以在这里看到这一点:
root@php8_tips_php8 [ /repo/ch07 ]#
php php7_mb_string_ereg_replace.php
Original: สุนัขจิ้งจอกสีน้ำ�ต�ล 2 ตัวกระโดดข้�มรั้ว
Modified: สุนัขจิ้งจอกสีน้ำ�ต�ล 2 ตัวกระโดดข้�มรั้ว
这里的危险在于,如果你的代码依赖于这种无声的解释过程,你的应用程序可能会失败或表现出不一致的行为。您还会注意到缺少 "通知" 或 "警告" 信息。PHP 8 依赖于开发人员提供正确的参数!
如果确实需要使用 ASCII 码,最好的做法是使用 mb_chr()
来生成所需的搜索字符串。修改后的代码示例如下:
$mod1 = mb_ereg_replace(mb_chr(50), '3', $str);
现在,你已经知道 mbstring
扩展中发生了哪些变化。如果没有这些信息,你很容易就会写出错误的代码。不了解这些信息的开发人员最终可能会在 PHP 8 中犯错误,比如认为 mbstring
别名仍然有效。在 PHP 8 迁移之后,这种错误的理解很容易导致花费大量时间来追踪程序代码中的错误。
现在我们来看看另一个有重大变化的扩展:GD 扩展。