编程 PHP 8.6 弃用了一批名字:let、namespace、in/out,以及 finally 里的 return

2026-11-14 00:04:38

PHP 8.6 弃用了一批名字:let、namespace、in/out,以及 finally 里的 return

把 CI 切到 PHP 8.6 后,测试还能跑完,但日志里会多出几行:

Deprecated: Calling a class "let" is deprecated
Deprecated: Returning from a finally block is deprecated

这些都不是 Error,进程退出码也没有变化。触发提示的是标识符命名和两处控制流写法。PHP 8.6 定于 2026-11-19 发布(RC5 2026-11-05),这批变更来自 RFC「Deprecations for PHP 8.6」(2025-10-12,Status: Pending Implementation),另一份整理见 Nuno Maduro 的 Everything New in PHP 8.6。

弃用不等于报错,移除在 PHP 9

RFC 的原话是:A deprecation is not an error. Your code runs, and you get an E_DEPRECATED notice that tells you what to change before PHP 9.

判断一个写法是否受影响,看三点就够了:

  1. 在 error_reporting=E_ALL 下是否出现 E_DEPRECATED;
  2. 行为是否改变——这批里只有 finally 里的 return 本来就是语义陷阱,其余都只是命名限制;
  3. 移除版本是 PHP 9,8.6 只负责让提示出现在日志里。

所以处理节奏是:先让这些提示出现在测试日志和静态扫描结果里,再在 9 之前清理完;不要在 8.6 上线前做大规模改名,尤其是不要盲目跑批量替换(见下文 list())。

逐条:建议不要再使用这些名字

finally 里的 return

Deprecated: Returning from a finally block is deprecated

当 try 或 catch 因抛异常或返回而退出时,finally 里的 return 会悄悄地丢弃这个退出:异常被吞,返回值被覆盖,调用方拿到一个看起来很正常的返回值。

function getConfig(): array {
    try {
        return loadConfig(); // 文件缺失时抛异常
    } finally {
        return []; // 异常没了,调用方只拿到 []
    }
}

RFC 对最常用的约 5000 个 Composer 包做了静态扫描:finally 里出现 return 的共 12 处,分布在 9 个包中。逐个人工核对后,9 处是有意为之或无害,3 处的 try 在真实路径上会抛异常,而 finally 的 return 会把异常吞掉。投票结果:39 yes / 3 no / 4 abstain。

let 作标识符

Deprecated: Calling a {class|trait|interface|enum|constant|function} "let" is deprecated

class_alias() 的第二个参数传 "let" 同样被弃用。

动机是把这个词正式预留给未来的关键字。let 曾作为 Block Scoping RFC 的一部分提出,该 RFC 未被接受;但块作用域最自然的关键字就是 let,现在预留之后,将来的块作用域 RFC 可以直接使用它而不造成破坏性变更。静态扫描命中 3 处。

in / out / inout 作标识符

这三个词的预留方向是引用与泛型。RFC 给出的两个理由:一是它们可能构成 PHP 处理引用(类型一致性与可变性)的新语法的基础;二是它们可能用作泛型类型的协变/逆变标记——这一观察来自「Bound-Erased Generic Types」RFC 的二次投票,那轮投票讨论的正是采用哪种 variance 标记语法。

扫描命中 33 处:in 25 处(涉及 Doctrine、Laminas、Laravel、Magento),out 8 处,inout 0 处。也就是说,实际受影响的主要是 in,而且集中在几个大生态包里。

namespace 作类常量名

namespace 目前是半保留字,因此允许作为类成员名:

class Foo { const NAMESPACE = ''; }

动机是给未来的语法留位置。预留之后,可以实现一个类似 ::class 的 ::namespace 伪常量,用来表示目录命名空间,把 "Order\\Domain\\Entities" 这种字符串 API 换成 \Order\Domain\Entities::namespace,IDE 支持更好。GitHub 搜索约 120 个匹配文件,RFC 的影响分析记为 21 处。

readonly 作函数名

readonly 最初允许作函数名,是因为 WordPress 有一个同名函数,当时靠在 lexer 里加了一段 hack 实现。这个 hack 现在被弃用。

_ 作常量名与编译期别名

弃用 "_" 作为常量名,以及作为编译期别名;同时弃用 "_" 作为函数名和 gettext() 的 _() 别名。

另外三条容易漏的

  • define() 的第三个参数 $case_insensitive:define('FOO', 1, true) 这种写法被弃用。
  • is_a() / is_subclass_of() 在 $allow_string 为 false 时传字符串。
  • __debugInfo() 的返回类型写成 ?array,应写成 array。

函数别名这一批的提示长这样:

Deprecated: Function is_integer() is deprecated since 8.6, use is_int() instead
Deprecated: Function is_double() is deprecated since 8.6, use is_float() instead
Deprecated: Function doubleval() is deprecated since 8.6, use floatval() instead
Deprecated: Function spl_object_hash() is deprecated since 8.6, consider using spl_object_id() instead

同一份 RFC 里还有 __construct() / __destruct() 中带值 return、构造器或析构器写成 generator、把对象传给 array_walk() 系列函数的数组参数、ReflectionProperty::setValue() 传错类型对象、SplFileObject 的 CSV 方法、mysqli_stmt_init()、mb_ereg*、php://filter 超过 16 个 filter、session handler 缺 create_sid()/validateId() 等,这些与命名保留无关,不在本文逐条展开。

list() 这次没有被弃用

list() 与短语法完全等价,不提供任何额外能力,只是为了向后兼容保留的长写法。提案给出的迁移方式是:

ast-grep run --pattern 'list($$$ARGS)' --rewrite '[$$$ARGS]' --lang php -U

扫描结果确实很大:5545 个文件里 12275 处。

但最终投票是 23 yes / 23 no / 1 abstain,未达到 2/3 门槛,所以 PHP 8.6 实际并未弃用 list()。这也是不要盲目跑批量替换的直接原因:工具能改,不代表当前版本要求你改;大批量的机械改写会带来无谓的 diff 和 review 成本。

哪些写法不受影响

  • 名为 namespace 的方法和静态属性不受影响,例如 Laravel 的 Route::namespace()。
  • 常量持有的对象可以写属性(const CONFIG = new Config(); CONFIG->items['app.name'] = 'Acme';),但常量本身不能替换,enum case 的属性仍然不可写。
  • readonly 属性可以就地写默认值(public readonly int $perPage = 15;),重复赋值仍然报 Error: Cannot modify readonly property Settings::$perPage。

怎么查出来

先用 grep 粗筛,误报可以人工过:

# 声明位置用了保留字
grep -rnE --include='*.php' \
  '(class|interface|trait|enum|function|const)[[:space:]]+(let|is|in|out|inout)\b' .

# namespace 作类常量
grep -rnE --include='*.php' 'const[[:space:]]+namespace\b' .

# readonly 作函数名
grep -rnE --include='*.php' 'function[[:space:]]+readonly\b' .

# define 第三参数
grep -rnE --include='*.php' "define\([^)]*,[^)]*,[[:space:]]*true" .

finally 里的 return 更适合用 ast-grep 定位到块再人工判断,因为这个模式是否真的会吞异常,取决于 try 里有没有会抛的路径:

ast-grep run --pattern 'finally { $$$BODY }' --lang php .

更可靠的做法是让运行时自己报告。php -l 只做语法检查,不执行代码,不会触发 E_DEPRECATED;要真正跑起来:

php -d error_reporting=E_ALL -d display_errors=1 vendor/bin/phpunit

PHPStan 能覆盖一部分静态命名问题,但对本轮这种「名字本身被弃用」的规则覆盖有限,不能只靠它。

最后可以直接编译 8.6 分支验证:

git clone --depth 1 --branch PHP-8.6 https://github.com/php/php-src.git
cd php-src && ./buildconf --force \
  && ./configure --disable-all --enable-cli --enable-mbstring --disable-mbregex \
     --enable-tokenizer --enable-ctype --enable-filter \
  && make -j8

容易漏的两个点单独记一下:define() 的 $case_insensitive 第三参数在代码里常常只写两个参数,grep 时要按三个参数的位置找;__debugInfo() 的 ?array 返回类型则要翻实现 __debugInfo 的类,光看调用方是看不出来的。

推荐文章

程序员茄子在线接单