PHP 8.5 闭包和一等可调用对象进入常量表达式
当"配置"变成运行时胶水代码
PHP 配置一直有个矛盾:
- 你想要声明式配置:简单的数组、常量值、属性。
- 但你也需要一点逻辑:"验证这个字段"、"选择这个处理器"、"格式化这个值"、"过滤这个列表"。
以前,一旦你需要在"配置类"的地方加逻辑,就会碰壁。PHP 故意把很多结构限制在常量表达式——基本上就是不可变的值。属性参数是最明显的例子:你可以放整数、字符串、标量数组……但不能放闭包。
所以我们用各种变通方案:
- 存字符串如 "App\\Handler::handle",然后用 call_user_func 调用。
- 在属性里用"迷你语言",比如表达式字符串。
- 用可空回调,在运行时设置默认值。
- 在引导文件里建注册表,而不是直接在该放的地方表达。
PHP 8.5 改变了这个局面:静态闭包和一等可调用对象现在可以出现在常量表达式中,包括:
这听起来像编译器特性。实际上是个"生活质量"升级:让你把配置放在它配置的代码旁边,不用魔术字符串或运行时初始化 hack。
这篇文章会讲"为什么"、具体规则(有重要限制),然后深入实际模式:路由映射、处理器注册表、策略/格式化器注册表。也会讲哪些场景不适合——因为如果不小心,可调用配置确实能搞出一团乱。
原文 PHP 8.5 闭包和一等可调用对象进入常量表达式
旧痛点:常量太受限,逻辑只能塞进运行时初始化
PHP 8.5 之前,限制不是你不能创建闭包——而是你不能在某些"配置槽"里用它们。
三个常见痛点:
痛点 A:"回调默认值"参数强制运行时初始化
如果你想写一个接受可选回调的函数,并且想要一个合理的默认回调,通常这样做:- function my_filter(array $items, ?Closure $predicate = null): array
- {
- $predicate ??= static function ($v): bool { return !empty($v); };
- $out = [];
- foreach ($items as $item) {
- if ($predicate($item)) {
- $out[] = $item;
- }
- }
- return $out;
- }
复制代码 这能用……但是样板代码,而且不是"声明式"的。
PHP 8.5 的 RFC 明确提到这个用例:允许直接声明默认回调闭包,不用可空参数的变通方案。
痛点 B:属性参数不能包含真正的逻辑
属性是表达规则的自然场所:
但属性参数只能是常量表达式,所以人们用字符串或表达式对象。
PHP 8.5 发布公告展示了一个典型的"之前"模式,访问控制属性接受字符串表达式。在 PHP 8.5 中你可以直接传静态闭包。
痛点 C:注册表和路由映射变成运行时引导
任何时候你想要从"键"到"处理器"的映射,你可能在运行时构建它:- $handlers = [
- 'json' => [JsonFormatter::class, 'format'],
- 'text' => [TextFormatter::class, 'format'],
- ];
复制代码 这能用,但很脆弱:
- IDE 重命名重构不能可靠地跟踪字符串方法名。
- 静态分析更难理解什么是可调用的。
- 你需要运行时代码来组装概念上是静态配置的东西。
PHP 8.5 的常量表达式改进让你可以把这些注册表表达为常量——并且让处理器重构安全。
什么是常量表达式,为什么重要
"常量表达式"是 PHP 内部术语,指在必须不依赖运行时状态就能计算的上下文中允许的表达式——可以理解为"不可变值"。
这些上下文包括:
闭包 RFC 总结旧规则为:常量表达式被限制在实际上是"不可变值"的操作,闭包不包括在内——尽管闭包本质上是编译后的代码(操作码),在约束下可以被视为不可变。
为什么这很重要?
因为这些上下文是你想放配置的地方:
- 属性是你的元数据/配置层。
- 默认参数/属性值表达预期行为,不需要样板代码。
- 常量表达"这个映射不会变"。
换句话说:常量表达式是 PHP 引导你走向声明式代码的地方。PHP 8.5 扩展了"声明式"的含义。
常量中的闭包:安全可读的模式(和硬性规则)
PHP 8.5 允许常量表达式中的闭包——但有严格约束:
- 必须是静态的(没有 $this)。
- 不能通过 use(...) 捕获外部变量。
- 箭头函数在常量表达式中不支持,因为它们隐式捕获变量。
这些规则是编译时强制的。
这听起来有限制,但实际上这正是这个特性安全的原因:它防止意外把"运行时状态"偷渡进常量。
默认回调参数,不需要可空样板
这是之前过滤器示例的干净 PHP 8.5 版本:
[code] |