PHP 8.4 正式引入属性钩子,仅支持非静态、非 readonly 属性;支持箭头/块语法的 get/set 钩子;不兼容 __get/__set;虚拟属性不占内存。
PHP 属性钩子(Property Hooks)是 PHP 8.4 正式引入的语法特性,不是实验性 RFC,也不是未来版本(如 PHP 8.5)的功能——它已稳定可用,且仅存在于
。如果你在 PHP 8.3 或更低版本中尝试使用,会直接报错:
。
属性钩子只能用在非静态、非 readonly 属性上
这是最容易被忽略的硬性限制。一旦属性声明为
、
或
,就无法附加
或
钩子。
✅ 合法
❌ 解析失败:readonly 和钩子互斥
❌ 静态属性不支持钩子
✅ 私有属性可以,但需注意:外部无法直接访问,必须配合 public getter 或其他方式暴露
原因在于:钩子本质是运行时拦截逻辑,而
的语义是编译期+运行期双重不可变保障,二者设计目标冲突;静态属性则缺乏实例上下文,无法可靠绑定
。
get/set 钩子的三种写法及适用场景
PHP 8.4 支持三种钩子体语法,差异直接影响可读性和能力边界:
立即学习
“
PHP免费学习笔记(深入)
”;
:箭头语法,仅限单表达式,无参数、无多行、不能提前 return;适合纯计算型只读字段(如
拼接)
:块语法,完整作用域,可写多行、调用方法、加条件判断;推荐用于含副作用或复杂逻辑的场景(如缓存检查、日志记录)
:带显式参数签名的块语法,支持类型声明、引用传递(
)、默认值;必须用块语法,箭头不支持参数声明
注意:
钩子内不能用
语句(语法错误),但可以抛出异常;
钩子若返回引用(
),需确保返回的是可引用变量,否则运行时报
。
__get/__set 不是属性钩子的替代方案
很多开发者想“降级兼容”到旧版本,试图用魔术方法模拟钩子行为,但这会产生三类实质性偏差:
作用域失控:
/
是类级兜底,无法为单个属性独立配置逻辑;你必须手动
分发,维护成本陡增
类型系统断裂:IDE 和
无法推断
返回类型,
在静态分析中始终是
,失去类型安全
性能损耗明确:每次访问都触发函数调用+字符串匹配,而原生钩子由 Zend 引擎直接内联处理,实测开销低一个数量级
更关键的是:
对已声明的
属性完全不生效——它只捕获对未定义或不可访问属性的赋值。这意味着你必须把所有目标属性改为
,再靠魔术方法代理,彻底破坏属性的直觉语义。
虚拟属性与内存占用
当一个属性只有
钩子、没有
,且钩子体内不读写同名属性(即不访问
),该属性就是“虚拟属性”(virtual property)。它不占用对象内存空间,仅提供计算接口。
例如:
这里
不会在
中出现,
也不会增加。但如果钩子里写了
,它就变成 backed 属性,引擎会自动分配存储槽位——这种隐式切换容易引发意外内存膨胀,尤其在高频创建对象的场景下需警惕。
PHP 8.4+Parse error: syntax error, unexpected '{'staticreadonlyreadonly staticgetsetpublic string $name { get => $this->value; }public readonly string $name { get => 'fixed'; }public static string $count { get => self::$counter; }private int $id { set(int $value) { $this->id = max(1, $value); } }readonly$thisget => $this->value;$fullNameget { return $this->value; }set(string $value) { $this->value = trim($value); }set(&$value)setreturngetget &{ ... }Fatal error: Cannot return by reference__get__setswitch ($name)PHPStan__get$obj->foomixed__setpublicprivategetset$this->propNameclass User
{
public function __construct(
private string $firstName,
private string $lastName,
) {}
public string $fullName { get => "{$this->firstName} {$this->lastName}"; }
}
$fullNamevar_dump(new User('a', 'b'))sizeof($user)$this->fullName = ...