2026 年 PHP 8.4 依然重要:跳到 8.5 之前你该掌握的特性
2026 年 PHP 8.4 依然重要:跳到 8.5 之前你该掌握的特性
为什么 PHP 8.4 在 2026 年仍然相关
如果你的团队计划"今年上 PHP 8.5",很可能会先聊到 PHP 8.4——不管你愿不愿意。
无聊但重要的原因是:支持窗口。
根据官方 PHP 支持时间表,PHP 8.4(2024 年 11 月 21 日发布)仍处于活跃支持期,直到 2026 年 12 月 31 日,安全修复持续到 2028 年 12 月 31 日。
这让 8.4 在 2026 年初成为一个合理的基线,特别是对于从 8.2/8.3 升级、想避免"跳太远、坏太多"的团队。
但有意思的原因是技术层面的:PHP 8.4 悄悄重塑了日常 OOP 风格。它引入的特性减少了样板代码,让"干净的 DTO"和"安全的领域对象"更像是语言原生支持的东西:
- Property hooks(头条功能)
- 非对称属性可见性(读起来是 public,写起来是 private)
- 一些生活质量改进(数组、弃用工具、DOM 等)
如果你在 8.4 上内化了这些,升级到 8.5 往往感觉像"添加一些好东西",而不是"把整个 PHP 写法现代化"。
所以这篇文章可以当作基线知识:如果你还没上 8.4,这是你为 8.5 做准备应该知道的——但不会变成完整的升级清单。
原文 2026 年 PHP 8.4 依然重要:跳到 8.5 之前你该掌握的特性
Property hooks:是什么、为什么重要、什么时候值得用
Property hooks 是 PHP 8.4 引入的。
它让你可以直接在属性上附加 get 和/或 set 逻辑。可以理解为"访问器方法",但不需要写:
getFoo(): stringsetFoo(string $foo): void- 加上私有的 backing 字段
- 加上在构造函数和工厂里重复的额外不变量
心智模型
带 hook 的属性仍然是属性。你还是用正常方式读写它:
$user->email = " ADMIN@EXAMPLE.COM ";
echo $user->email;
但在底层,引擎把读写路由到 hook。
完整形式的 hook 长这样:
class Example
{
private bool $modified = false;
public string $foo = 'default value' {
get {
if ($this->modified) {
return $this->foo . ' (modified)';
}
return $this->foo;
}
set(string $value) {
$this->foo = strtolower($value);
$this->modified = true;
}
}
}
这是手册里的示例,展示了核心思想:保留属性语法,同时获得集中化的行为。
Backed property vs virtual property(容易漏掉的部分)
Property hooks 可以创建两"种"属性:
- Backed property:在对象中有存储的内存(普通属性),hook 操作 backing value。
- Virtual property:没有 backing 存储——是派生/计算的,像伪装成
$area的getArea()。
手册解释说,如果两个 hook 都没有用精确语法引用 $this->propertyName,属性就是 virtual 的,virtual 属性不占内存空间。
一个干净的 virtual property 示例:
class Rectangle
{
public function __construct(
public int $h,
public int $w,
) {}
public int $area {
get => $this->h * $this->w;
}
}
$r = new Rectangle(4, 5);
echo $r->area; // 20
$r->area = 30; // Error: no set operation defined
这基本上是一个计算型 getter——但读起来像属性。
最有用的实际模式
专注于真正能减少 bug 和样板代码的模式。
模式 A:在 setter 中规范化输入(trim、大小写转换等)
经典场景:邮箱、用户名、slug。你想接受杂乱输入但存储规范化的值。
final class UserProfile
{
public string $email {
set => strtolower(trim($value));
}
}
在简写形式中,表达式结果成为存储的 backing value。
这已经很有用了,但生产代码通常还需要验证。
模式 B:在边界处验证不变量(尽早抛出)
例如,强制"用户名至少 3 个字符"并规范化空格。
final class UserProfile
{
public string $username {
set {
$v = trim($value);
if ($v === '') {
throw new InvalidArgumentException('Username cannot be empty.');
}
if (strlen($v) < 3) {
throw new InvalidArgumentException('Username is too short.');
}
$this->username = $v;
}
}
}
这让不变量紧挨着属性,而不是散落在控制器、请求验证器和构造函数各处。
PHP 迁移指南甚至展示了类似的"验证然后赋值"模式。
模式 C:派生/virtual 的"展示"属性
常见的 DTO 需求:暴露 fullName 但不存储它。
final class Person
{
public function __construct(
public string $first,
public string $last,
) {}
public string $fullName {
get => "{$this->first} {$this->last}";
}
}
Virtual property 最适合的场景:
- 确定性的,
- 计算成本低,
- 你永远不想"set"它们。
对于历史上滥用魔术 __get() 的团队,这是一个干净的基线。
模式 D:"计算一次,之后缓存"(谨慎使用)
有时计算值很昂贵(解析、构建对象)。你可以在对象内部缓存它。
final class RequestContext
{
private ?array $cachedClaims = null;
public function __construct(
public string $jwt,
) {}
public array $claims {
get {
if ($this->cachedClaims !== null) {
return $this->cachedClaims;
}
// 假设 parseJwt() 做签名检查、base64 解码等
$this->cachedClaims = $this->parseJwt($this->jwt);
return $this->cachedClaims;
}
}
private function parseJwt(string $jwt): array
{
// ...
return [];
}
}
这很方便,但也是 hook 可能变得"太魔法"的地方。如果你把重活藏在 $obj->claims 后面,可能会让调用者意外。只在人体工学真正超过成本时使用这个模式。
Hooks + 构造函数提升:一个微妙的坑
PHP 允许在提升的属性上使用 hook,但有一个重要规则:传给构造函数的值必须匹配属性声明的类型——不管你的 set hook 可能接受什么。
也就是说你可以写:
- 属性类型:
DateTimeInterface - set hook 接受:
string|DateTimeInterface
…但如果你用提升,构造函数参数类型仍然是 DateTimeInterface。
如果你真的想"构造函数里也允许 string",你可能需要工厂或非提升的构造函数参数。
重要限制:property hooks 不能和 readonly 一起用
这对喜欢不可变对象的团队很重要。
手册明确说明:property hooks 与 readonly 属性不兼容。
所以如果你的风格是"到处都是不可变值对象",hooks 不能替代那个。Hooks 更适合的场景是:
- DTO 和 request/response 对象
- 配置对象
- 内部可变但需要护栏的领域对象
(下一节会讲用非对称可见性实现"半不可变 DTO"。)
另一个限制:引用和间接修改可能坑你
Hooks 拦截读写,这可能与引用冲突——特别是数组元素写入:
$obj->arr['k'] = 'v';
文档警告说,获取引用或间接修改可能绕过 set hook,并概述了约束(如 &get 行为)和允许的情况。
实用指南:
- 如果调用者经常修改元素,避免在数组属性上用 hook。
- 优先用"替换整个数组"模式(
$obj->tags = [...$obj->tags, $newTag];),这表现得像普通 set。
什么时候不应该用 property hooks
Hooks 很棒……直到它们不是。在以下情况避免:
- "hook body"开始做真正的编排(IO、网络调用、日志)。
- 调试变得不清晰("为什么读这个属性会访问数据库?")。
- 你的团队需要关键行为有显式的调用点。
一个有用的规则:property hooks 最适合实现局部不变量和局部转换——真正属于属性本身的逻辑。
其他影响日常工作的 PHP 8.4 特性(挑你真正会用的)
PHP 8.4 不只有 hooks。专注于以下类型的特性:
- 减少样板代码,
- 减少 bug,
- 或让代码更容易理解。
特性:非对称属性可见性(public 读、受限写)
非对称可见性让你可以为读和写设置不同的可见性。
示例:
final class Money
{
public function __construct(
public private(set) string $currency,
public private(set) int $cents,
) {}
}
调用者可以读:
echo $m->cents;
但不能写:
$m->cents = 500; // Error outside the class
迁移指南阐明了规则:第一个可见性是 get-visibility,第二个控制 set-visibility,get visibility 不能比 set visibility 更窄。
这对 DTO 很重要:它给你一种"大部分不可变"的风格,而不必采用完整的值对象方法。
组合非对称可见性 + property hooks
这个组合经常替代经典的"私有属性 + getter + setter"。
final class UserInput
{
public private(set) string $email {
set => strtolower(trim($value));
}
public private(set) string $name {
set {
$v = trim($value);
if ($v === '') {
throw new InvalidArgumentException('Name is required.');
}
$this->name = $v;
}
}
}
- 读是 public(对模板、序列化器、调试友好)
- 写是受控的(对不变量友好)
- 样板代码保持低
特性:新数组辅助函数(array_find、array_find_key、array_any、array_all)
PHP 8.4 新增了数组搜索/检查函数。
array_find 和 array_find_key
$users = [
['id' => 1, 'active' => false],
['id' => 2, 'active' => true],
['id' => 3, 'active' => false],
];
$first = array_find($users, fn ($u) => $u['active'] === true);
// ['id' => 2, 'active' => true]
如果没找到,返回 null——但如果值本身就是 null,你怎么区分"找到 null"和"没找到"?
你可以用 array_find_key() 来避免歧义(因为 key 不能是 null)。
$key = array_find_key($users, fn ($u) => $u['active'] === true);
if ($key === null) {
// 真的没找到
}
$firstActive = $users[$key];
array_any 和 array_all 看起来简单——直到它们消除了噪音
例如:强制所有上传的文件都在大小限制内。
$ok = array_all($files, fn ($f) => $f['size'] <= 5_000_000);
if (!$ok) {
throw new RuntimeException('One or more files are too large.');
}
这替代了每个人都写得略有不同的 foreach + 标志变量。
特性:#[Deprecated] attribute 用于用户态弃用
PHP 一直有内部弃用机制,但 PHP 8.4 通过 #[Deprecated] attribute 暴露了一个干净的用户态版本。
手册说:使用已弃用的功能会触发 E_USER_DEPRECATED。
示例:
#[\Deprecated(message: "Use slugify() instead", since: "2026-01")]
function make_slug(string $s): string
{
return strtolower(trim($s));
}
function slugify(string $s): string
{
// 真正的实现
return strtolower(trim($s));
}
make_slug("Hello World");
这对团队来说被低估了:它给你一个标准化的方式来:
- 标记旧的辅助函数,
- 指导内部迁移,
- 在 CI 日志中暴露弃用使用情况。
特性:支持 HTML5 的新 DOM API(在 Dom 命名空间中)
如果你在 PHP 中解析过 HTML(爬取、清理、迁移脚本),PHP 8.4 是一次有意义的升级。
8.4 发布公告介绍了带有标准兼容 HTML5 解析的新 DOM API、Dom 命名空间中的新类,以及方便的查询辅助函数。
公告中的示例展示了:
Dom\HTMLDocument::createFromString(...)querySelector(...)classList->contains(...)
一个实际用例:安全地检测"canonical"链接标签。
$doc = Dom\HTMLDocument::createFromString($html, LIBXML_NOERROR);
$canonical = $doc->querySelector('link[rel="canonical"]');
$url = $canonical?->getAttribute('href');
对于简单任务,这比经典的 DOMDocument + DOMXPath 组合好用得多,它减少了没人想维护的"XPath 意大利面"脚本。
特性:PDO 驱动特定子类(更精确的 API)
PHP 8.4 引入了驱动特定的 PDO 子类,如 Pdo\MySql、Pdo\Pgsql、Pdo\Sqlite 等,并在发布公告中展示了新的连接风格。
在 PHP 8.4 示例中:
PDO::connect(...)返回Pdo\Sqlite- 驱动特定方法只存在于相关的地方
这改善了正确性和 IDE 支持,特别是在测试和生产混用不同驱动的代码库中。
附加:Lazy objects(主要用于框架和基础设施代码)
PHP 8.4 还引入了 lazy objects 概念:初始化被延迟到访问时才进行的对象。迁移指南明确指出框架可以利用它们来延迟获取依赖或数据。
它甚至展示了使用 ReflectionClass::newLazyGhost(...) 的核心机制。
这不是你在日常应用代码中会天天用的东西,但如果你做:
- DI 容器,
- ORM,
- 代理层,
- 或对性能敏感的 bootstrap,
…值得知道它的存在,因为你会在生态系统内部看到它。
PHP 8.4 如何改变 OOP 风格(尤其是 DTO)
如果你写 PHP 很多年,你可能经历过至少三种 DTO 风格:
- "到处都是 public 属性"
- "所有东西都是私有属性 + getter/setter"
- "readonly 提升属性"(好用,但死板)
PHP 8.4 增加了第四种,非常实用:
- public 读,
- 受控写,
- 不变量靠近数据。
8.4 之前:常见的 DTO 样板代码
final class CreateUserCommand
{
private string $email;
private string $name;
public function __construct(string $email, string $name)
{
$this->email = strtolower(trim($email));
$this->name = trim($name);
if ($this->name === '') {
throw new InvalidArgumentException('Name is required.');
}
}
public function email(): string { return $this->email; }
public function name(): string { return $this->name; }
}
没什么问题。只是重复,特别是在几十个消息对象上。
8.4 之后:"public 读 + private 写 + hooks"
final class CreateUserCommand
{
public private(set) string $email {
set => strtolower(trim($value));
}
public private(set) string $name {
set {
$v = trim($value);
if ($v === '') {
throw new InvalidArgumentException('Name is required.');
}
$this->name = $v;
}
}
public function __construct(string $email, string $name)
{
$this->email = $email;
$this->name = $name;
}
}
你得到:
- 强类型
- 集中化的规范化/验证
- Public 可读性(在模板、日志、序列化器中方便)
- 没有访问器样板
而且你仍然可以用测试保持严格。
一个现实的"DTO + 派生属性"模式
final class Address
{
public function __construct(
public private(set) string $line1,
public private(set) ?string $line2,
public private(set) string $city,
) {}
public string $singleLine {
get => trim($this->line1 . ' ' . ($this->line2 ?? '') . ', ' . $this->city);
}
}
你可以保持存储字段干净,并提供一个友好的派生字段而不引入额外方法。
readonly 仍然胜出的场景
因为 hooks 不能和 readonly 一起用,不可变值对象仍然依赖:
- readonly 提升属性
- 工厂
- 显式的
withX()方法(在 PHP 8.5 里更好用了,但那是另一篇文章的事)
所以很多团队的实际分工是:
- 值对象:readonly + 显式行为
- DTO / 命令 / 请求:非对称可见性 + hooks
经常影响测试/CI 的简短兼容性说明
这一节故意简短,但是能省时间的那种简短。
错误报告级别:E_STRICT 没了
PHP 8.4 移除了 E_STRICT 错误级别,E_STRICT 常量已弃用。
如果你有遗留代码或配置引用了 E_STRICT,可能会看到 CI 行为变化。
JIT 配置默认值变了(OPcache)
PHP 8.4 中 JIT 配置的默认值变了:
- 从
opcache.jit=tracing和opcache.jit_buffer_size=0 - 到
opcache.jit=disable和opcache.jit_buffer_size=64M
这不会改变"JIT 默认关闭",但可能影响之前只切换其中一个值的环境。
一些扩展变得更严格(类型化常量、ValueError、行为变更)
8.4 不兼容列表中的一些例子:
- 几个扩展类常量现在有类型(Date、Intl、PDO、Reflection、SPL、Sqlite、XMLReader)。
- 一些函数现在抛 ValueError 而不是静默接受无效输入(如
round()无效模式、str_getcsv()无效分隔符长度)。 - SimpleXML 迭代行为变了以避免意外的 rewind(之前可能导致无限循环)。
- 根据驱动,一些 PDO 属性现在表现为布尔而非整数。
这些是那种除非你尽早在 8.4 下运行测试套件,否则会表现为"随机测试失败"的变更。
小型迁移:采用 PHP 8.4 的小步骤,无需大重构
如果你还没上 8.4——或者上了但没用这些特性——这是一个通常有效的安全顺序。
步骤 1:先把 8.4 加到 CI,即使生产还没准备好
确保你能在 8.4 上运行测试套件而不出意外。把警告和弃用当作信号。
步骤 2:只在新代码中采用 array_find / array_any / array_all
不要重构整个代码库。只是不要再写新的"foreach-with-break"循环,除非它们真的更清晰。
步骤 3:对新的 DTO 和请求对象使用非对称可见性
这是低风险的:你主要是改变属性声明并消除一类意外修改。
步骤 4:在明显替代样板代码的地方添加 property hooks
从以下开始:
- trim 和大小写规范化,
- 简单验证,
- 派生属性。
一开始避免在 hooks 里放重逻辑。
步骤 5:用 #[Deprecated] 进行内部 API 清理
用清晰的消息标记旧方法和辅助函数。在 CI 日志中跟踪使用情况。让弃用可操作。
步骤 6:只在脚本或隔离模块中采用新 DOM API
如果你的应用做 HTML 解析,新 API 可能是很大的改进——但一开始保持采用范围受限。
步骤 7:把 lazy objects 留给框架/基础设施层
知道这个特性存在。不要强行塞进应用代码,除非你有非常具体的性能或架构原因。
通往 PHP 8.5 的桥梁(为什么 8.4 让下一步更容易)
一旦你的团队熟悉了 PHP 8.4 的"现代基线":
- DTO 因为非对称可见性 + hooks 变得更干净
- 弃用策略因为
#[Deprecated]变得更系统化 - 数组重型代码可以用原生辅助函数表达得更清晰
- HTML 解析和工具脚本变得不那么痛苦
这个基线减少了迁移到 PHP 8.5 的摩擦,因为你已经现代化了代码库中对象和日常工具的写法。PHP 8.5 就不再是"追赶",而是选择性地采用改进。
结论
PHP 8.4 不是一个"跳过它,直接上 8.5"的版本。在 2026 年,它仍然是一个明智的基线,因为它受支持、广泛相关,而且它改变了日常 PHP 的人体工学——尤其是在 OOP 密集的代码库中。
如果你从这篇回顾中只带走一件事,那就是 property hooks——但要带着意图使用:
- 用它们做不变量、规范化和干净的派生值,
- 把它们和非对称可见性配对做实用的 DTO,
- 让 hooks 保持无聊(往好的方向)。
这个组合让你今天就有更干净的 8.4 代码库——以及准备好时通往 8.5 的更平滑路径。

浙公网安备 33010602011771号