作为 php core developer,我从事 php 语言的内核维护和开发工作有一段时间了。一直有很多想法和经验总是忘记。遂写一篇类似与小记的博客记录一下。
DONE
以下是我对 PHP 做过或正在推进的用户可见行为变化(不包含重构)。(更新于 2026-07-01 00:00 CST)
PHP 8.6.x
RFC
-
trim/ltrim/rtrim/chop支持换页符。(PR #20788, RFC, PHP.WATCH) - 为 intl 添加
Locale::getDisplayKeyword()(别名locale_get_display_keyword()) 和Locale::getDisplayKeywordValue()(别名locale_get_display_keyword_value())。(PR #22264, RFC) - 弃用
metaphone相关函数。(PR #21889, RFC)
安全漏洞
- 等待披露
- 等待披露
Bug 修复
- ext/intl 修复
IntlIterator::current()在无效迭代器上返回UNKNOWN:0,现返回NULL。(PR #22495) - ext/intl 修复
IntlDateFormatter::parse()/datefmt_parse()和IntlDateFormatter::localtime()/datefmt_localtime()现在在偏移量参数不是 int 类型时抛出 TypeError。(PR #22533) - ext/intl 修复
NumberFormatter::parse()和NumberFormatter::parseCurrency()拒绝超出 32 位范围的偏移量值,而不是静默截断。(PR #22572) - ext/intl 在 64 位平台上让
MessageFormatter::parse()将PHP_INT_MIN作为int返回。(PR #22131) - ext/soap 拒绝超出 XML Schema integer 范围的值。(PR #22178)
- ext/intl 修复
UConverter::transcode()替换字符长度截断。(PR #22199) - ext/intl 在
IntlNumberRangeFormatter::format()中保留 64 位整数精度。(PR #22202) - ext/intl 修复
ResourceBundle禁用回退时的错误信息。(PR #22236) - ext/standard
phpcredits()修复 QA 团队标题对齐问题。(PR #22501) - ext/intl 修复
IntlListFormatter在format()失败后的对象错误状态。(从 PHP 8.5 合并上来;commit 946d687) - ext/intl 修复日历日期/时间 API 中越界的参数位置。(从 PHP 8.5 合并上来;commit 10704f0)
- ext/intl 在
IntlDateFormatter错误信息中使用PATTERN常量名。(从 PHP 8.5 合并上来;commit 2d78489) - ext/intl 修复未初始化日历参数的参数位置。(从 PHP 8.5 合并上来;commit 27d593e)
- ext/soap 修复解码 SOAP 数组索引时的整数溢出。(从 PHP 8.5 合并上来;PR #21963)
- ext/phar 在
Phar::addEmptyDir()中更正确地保留.phar魔术目录。(从 PHP 8.5 合并上来;PR #22011) - ext/intl 为无效显示类型同步
IntlTimeZone对象错误状态。(从 PHP 8.5 合并上来;commit 9e68cf8) - ext/intl 保持
Spoofchecker限制级别 API 的 ICU 53 版本保护。(从 PHP 8.5 合并上来;commit edde169) - ext/opcache 修复有类型按值返回时未解开引用包装的问题。(从 PHP 8.5 合并上来;PR #21973)
- ext/intl 修复
transliterator_transliterate()错误信息中的参数位置。(从 PHP 8.5 合并上来;commit d8e7418) - ext/phar 拒绝格式错误的 ZIP 额外字段,避免剩余长度下溢。(从 PHP 8.5 合并上来;commit 657f0d6, PR #22330)
- ext/intl 让
Locale::lookup()和locale_lookup()在语言标签无效时返回null,而不是回退到默认 locale。(从 PHP 8.5 合并上来;PR #22306) - ext/intl 让
Collator::__construct()和Spoofchecker::__construct()在重复构造已初始化对象时抛出Error,避免覆盖 ICU 句柄造成内存泄漏。(从 PHP 8.5 合并上来;PR #22386) - ext/intl 让
IntlListFormatter::__construct()在重复构造已初始化对象时抛出Error,避免覆盖 ICU 句柄造成内存泄漏。(从 PHP 8.5 合并上来;PR #22394) - ext/phar 修复以
.phar为前缀但不是魔术目录的目录处理。(从 PHP 8.5 合并上来;PR #22372) base_convert()修复较小缓冲区长度下的转换结果错误。(从 PHP 8.5 合并上来;PR #22406)- ext/zip 修复
ZipArchive::getFromIndex()忽略已删除条目的FL_UNCHANGED标志。(从 PHP 8.5 合并上来;PR #21706)
其他重大改动
linkinfo('')现在会抛出ValueError。(PR #21793)getenv()和putenv()拒绝包含 NUL 字节的参数。(PR #21817)- ext/zlib 在
deflate_init()和inflate_init()中使用更严格的整数选项处理。(PR #21841, PR #21860, PR #21909) proc_open()拒绝$cwd中的 NUL 字节。(PR #21871)parse_str()拒绝$string中的 NUL 字节。(PR #21942)dl()拒绝$extension_filename中的 NUL 字节。(PR #22358)openlog()拒绝$prefix中的 NUL 字节。(PR #22366)- ext/gmp 对
**、<<、>>中会损失精度的浮点数右操作数发出弃用警告。(PR #21945)
其他微小改动
shmop_open()拒绝超出key_t长度的值。(PR #22423)- ext/intl 在所有受支持的 ICU 版本上暴露
Spoofchecker限制级别 API。(PR #22055, commit edde169) pcntl_exec()验证$args必须是列表数组。(PR #21951)- ext/intl 对不可转为字符串的时区对象抛出
TypeError。(PR #21990) IntlBreakIterator::getLocale()对无效 locale type 值抛出ValueError。(PR #21997)。shm_attach()拒绝超出key_t长度的值。(PR #22423)number_format()拒绝过大的$decimals参数,避免 OOM。(PR #22435)sleep()和usleep()现在拒绝超出平台上限的值,修复无符号整数溢出。(PR #22619)
性能优化
- ext/intl 完成一批小优化。(PR #22069)
- ext/zip 在已知数组大小时预先初始化数组。(PR #21572)
- ext/uri 预先初始化错误数组。(PR #21560)
- ext/uri 优化 RFC3986 解析中的字符串分配。(PR #21550)
- ext/pgsql 追加换行时移除一次不必要的 NUL 字节复制。(PR #21597)
- ext/json 优化美化输出时的缩进逻辑。(PR #21474)
- ext/phar 优化 Phar 目录流中的临时字符串处理。(PR #22374)
PHP 8.5.x
Bug 修复
- ext/intl 修复
IntlListFormatter在format()失败后的对象错误状态。(commit 946d687) - ext/intl 修复日历日期/时间 API 中越界的参数位置。(从 PHP 8.4 合并上来;commit 10704f0)
- ext/intl 在
IntlDateFormatter错误信息中使用PATTERN常量名。(从 PHP 8.4 合并上来;commit 2d78489) - ext/intl 修复未初始化日历参数的参数位置。(从 PHP 8.4 合并上来;commit 27d593e)
- ext/soap 修复解码 SOAP 数组索引时的整数溢出。(从 PHP 8.4 合并上来;PR #21963)
- ext/phar 在
Phar::addEmptyDir()中更正确地保留.phar魔术目录。(从 PHP 8.4 合并上来;commit 3064540) - ext/intl 为无效显示类型同步
IntlTimeZone对象错误状态。(从 PHP 8.4 合并上来;commit 9e68cf8) - ext/intl 保持
Spoofchecker限制级别 API 的 ICU 53 版本保护。(从 PHP 8.4 合并上来;commit edde169) - ext/opcache 修复有类型按值返回时未解开引用包装的问题。(从 PHP 8.4 合并上来;PR #21973)
- ext/intl 修复
transliterator_transliterate()错误信息中的参数位置。(从 PHP 8.4 合并上来;commit d8e7418) - ext/phar 拒绝格式错误的 ZIP 额外字段,避免剩余长度下溢。(从 PHP 8.4 合并上来;commit 657f0d6, PR #22330)
- ext/intl 让
Locale::lookup()和locale_lookup()在语言标签无效时返回null,而不是回退到默认 locale。(从 PHP 8.4 合并上来;PR #22306) - ext/intl 让
Collator::__construct()和Spoofchecker::__construct()在重复构造已初始化对象时抛出Error,避免覆盖 ICU 句柄造成内存泄漏。(从 PHP 8.4 合并上来;PR #22386) - ext/intl 让
IntlListFormatter::__construct()在重复构造已初始化对象时抛出Error,避免覆盖 ICU 句柄造成内存泄漏。(PR #22394) - ext/phar 修复以
.phar为前缀但不是魔术目录的目录处理。(从 PHP 8.4 合并上来;PR #22372) base_convert()修复较小缓冲区长度下的转换结果错误。(从 PHP 8.4 合并上来;PR #22406)- ext/zip 修复
ZipArchive::getFromIndex()忽略已删除条目的FL_UNCHANGED标志。(从 PHP 8.4 合并上来;PR #21706)
PHP 8.4.x
Bug 修复
- ext/intl 修复日历日期/时间 API 中越界的参数位置。(commit 10704f0)
- ext/intl 在
IntlDateFormatter错误信息中使用PATTERN常量名。(commit 2d78489) - ext/intl 修复未初始化日历参数的参数位置。(commit 27d593e)
- ext/soap 修复解码 SOAP 数组索引时的整数溢出。(PR #21963)
- ext/phar 在
Phar::addEmptyDir()中更正确地保留.phar魔术目录。(commit 3064540) - ext/intl 为无效显示类型同步
IntlTimeZone对象错误状态。(commit 9e68cf8) - ext/intl 保持
Spoofchecker限制级别 API 的 ICU 53 版本保护。(commit edde169) - ext/opcache 修复有类型按值返回时未解开引用包装的问题。(PR #21973)
- ext/intl 修复
transliterator_transliterate()错误信息中的参数位置。(commit d8e7418) - ext/phar 拒绝格式错误的 ZIP 额外字段,避免剩余长度下溢。(commit 657f0d6, PR #22330)
- ext/intl 让
Locale::lookup()和locale_lookup()在语言标签无效时返回null,而不是回退到默认 locale。(PR #22306) - ext/intl 让
Collator::__construct()和Spoofchecker::__construct()在重复构造已初始化对象时抛出Error,避免覆盖 ICU 句柄造成内存泄漏。(PR #22386) - ext/phar 修复以
.phar为前缀但不是魔术目录的目录处理。(PR #22372) base_convert()修复较小缓冲区长度下的转换结果错误。(PR #22406)- ext/zip 修复
ZipArchive::getFromIndex()忽略已删除条目的FL_UNCHANGED标志。(PR #21706)
TODO
Zend 内核
新 API
- 添加 inline 的 helper function,分别是
- zend_bin2hex()
- zend_bin2hex_upper()
- zend_bin2hex_str()
这是因为内核里有很多扩展依赖 ext/hash,这本身没什么问题,但是这个依赖仅仅是要调用 ext/hash/php_hash 的 php_hash_bin2hex。这些功能很简单。所以我们可以把这些 bin2hex 的东西做成 Zend 里内置的 API,直接给 ext 使用。这样可以避免很多 ext 依赖于 ext/hash。
- 添加 inline 的 helper function,分别是
- zend_str_append_char_to_raw
- zend_str_concat_to_raw
- zend_str_concat3_to_raw
现在,我们有很多 zend_str_* 的 helper 可以完成 zend_string 的拼接。但是对于 C char,利用这些 helper 会多进行一次内存的 allocation,导致性能下降,因此,所有对于 C char 拼接的场景我们都是用原始的 memchr 方式。这样会导致代码的可读性降低。我们可以实现这些 helper 来帮助我们进行 C char 的拼接。
API 改进
- 重写
zend_string_concat2和zend_string_concat3。这两个 inline helper 被大量使用,却没有检查字符串长度是否超出边界。理想情况下我们应该重写这些函数,在拼接前检查一下长度之和是否超出字符串最大长度以防止溢出。 - 对以下函数
- ext/zlib deflate_init
- ext/zlib inflate_init
- …
用 zval_try_get_* 替换 zval_get_*。zval_get_*系列的函数,比如 zval_get_long,会直接强制把 zval 类型的参数转化为 long。这个强转甚至是静默的。所以在我看来任何人都不应该用这个函数(它应该被 deprecated)。我们应当审计所有该函数的应用场景并换成更严格的 zval_try_get_*,以在强制转换时抛出 TypeError/ValueError。
性能优化
- 对以下函数
- 优化 ext/zip php_zip_glob
- 优化 ext/zip php_zip_pcre
- 优化 ext/uri fill_errors
- …
php代码库中有很多地方采用 array_init 来初始化数组。这个函数会先计算参数的长度,分配内存再创建数组。很多情况下我们在初始化之前就知道数组的长度了。因此,可以使用 array_init_size 函数直接分配内存创建数组,省去计算长度的一部分以达到性能优化。
- 对以下函数
- 优化 ext/json php_json_pretty_print_indent
- 优化 ext/uri php_uri_parser_rfc3986_path_read
- 优化 ext/uri php_uri_parser_rfc3986_path_host
- …
这些函数中都多多少少涉及到利用循环的方式动态扩容字符串/数组长度的行为。我们可以使用分配(pre-allocation)的方式一次性将长度计算完全后直接分配所有内存,避免在循环体中多次动态扩容,以在复杂场景(即,假设使用优化前版本需要循环很多次的场景)下带来巨量优化。
ext
- php 的
password_verify函数里的密码会被 NUL 截断。这主要是因为 php 的 bcrypt 实现是完全用 C char 写的。我们应该整个用 zend_string 重写一遍这个实现 (ps:巨大工作量) - 对以下函数进行如下描述中的改动
- linkinfo
- putenv
- getenv
- proc_open
- parse_string
- …
检查所有 ext 的函数在参数中包含空 and/or 含 NUL 的字符串时是否能正常工作。如果不能,抛出 ValueError。
重写整个
RecursiveArrayIterator的逻辑。目前该类会存储 bucket pointer。这是一种相当危险的行为(i.e. 导致静默的内存泄露),而且已经使 UAF 在任意版本中的存在。OpenSSL 对于证书的处理用的是 libc 的
mktime,然后因为微软死了亲妈导致经典的 2038 问题,这个时间在 windows 系统上一旦超过 2038 年就会溢出。我们要重写这个实现避免使用微软的傻逼接口。declare(encoding=...)在 encoding 为 ASCII 或 UTF-8 这种不是 multibyte 的编码方式时,在zend.multibyte=0时仍会导致关于 multibyte 的 warning 出现。
RFC
- deprecate 掉 open_basedir。(我知道听起来很疯狂。我懒得解释为什么…)
- deprecate 掉
soundex函数。 - deprecate 掉 php://dechunk 的 filter。
- php 的 UUID 扩展。
研究
- __PHP_Incomplete_Class
- Reflection