找回密码
 立即注册
首页 业界区 业界 2026 年 PHP 8.4 依然重要:跳到 8.5 之前你该掌握的特 ...

2026 年 PHP 8.4 依然重要:跳到 8.5 之前你该掌握的特性

秦欣艷 1 小时前
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(): string
  • setFoo(string $foo): void
  • 加上私有的 backing 字段
  • 加上在构造函数和工厂里重复的额外不变量
心智模型

带 hook 的属性仍然是属性。你还是用正常方式读写它:
  1. $user->email = "  ADMIN@EXAMPLE.COM  ";
  2. echo $user->email;
复制代码
但在底层,引擎把读写路由到 hook。
完整形式的 hook 长这样:
  1. class Example
  2. {
  3.     private bool $modified = false;
  4.     public string $foo = 'default value' {
  5.         get {
  6.             if ($this->modified) {
  7.                 return $this->foo . ' (modified)';
  8.             }
  9.             return $this->foo;
  10.         }
  11.         set(string $value) {
  12.             $this->foo = strtolower($value);
  13.             $this->modified = true;
  14.         }
  15.     }
  16. }
复制代码
这是手册里的示例,展示了核心思想:保留属性语法,同时获得集中化的行为。
Backed property vs virtual property(容易漏掉的部分)

Property hooks 可以创建两"种"属性:

  • Backed property:在对象中有存储的内存(普通属性),hook 操作 backing value。
  • Virtual property:没有 backing 存储——是派生/计算的,像伪装成 $area 的 getArea()。
手册解释说,如果两个 hook 都没有用精确语法引用 $this->propertyName,属性就是 virtual 的,virtual 属性不占内存空间。
一个干净的 virtual property 示例:
  1. class Rectangle
  2. {
  3.     public function __construct(
  4.         public int $h,
  5.         public int $w,
  6.     ) {}
  7.     public int $area {
  8.         get => $this->h * $this->w;
  9.     }
  10. }
  11. $r = new Rectangle(4, 5);
  12. echo $r->area;     // 20
  13. $r->area = 30;     // Error: no set operation defined
复制代码
这基本上是一个计算型 getter——但读起来像属性。
最有用的实际模式

专注于真正能减少 bug 和样板代码的模式。
模式 A:在 setter 中规范化输入(trim、大小写转换等)
经典场景:邮箱、用户名、slug。你想接受杂乱输入但存储规范化的值。
  1. final class UserProfile
  2. {
  3.     public string $email {
  4.         set => strtolower(trim($value));
  5.     }
  6. }
复制代码
在简写形式中,表达式结果成为存储的 backing value。
这已经很有用了,但生产代码通常还需要验证。
模式 B:在边界处验证不变量(尽早抛出)
例如,强制"用户名至少 3 个字符"并规范化空格。
  1. final class UserProfile
  2. {
  3.     public string $username {
  4.         set {
  5.             $v = trim($value);
  6.             if ($v === '') {
  7.                 throw new InvalidArgumentException('Username cannot be empty.');
  8.             }
  9.             if (strlen($v) < 3) {
  10.                 throw new InvalidArgumentException('Username is too short.');
  11.             }
  12.             $this->username = $v;
  13.         }
  14.     }
  15. }
复制代码
这让不变量紧挨着属性,而不是散落在控制器、请求验证器和构造函数各处。
PHP 迁移指南甚至展示了类似的"验证然后赋值"模式。
模式 C:派生/virtual 的"展示"属性
常见的 DTO 需求:暴露 fullName 但不存储它。
  1. final class Person
  2. {
  3.     public function __construct(
  4.         public string $first,
  5.         public string $last,
  6.     ) {}
  7.     public string $fullName {
  8.         get => "{$this->first} {$this->last}";
  9.     }
  10. }
复制代码
Virtual property 最适合的场景:

  • 确定性的,
  • 计算成本低,
  • 你永远不想"set"它们。
对于历史上滥用魔术 __get() 的团队,这是一个干净的基线。
模式 D:"计算一次,之后缓存"(谨慎使用)
有时计算值很昂贵(解析、构建对象)。你可以在对象内部缓存它。
  1. final class RequestContext
  2. {
  3.     private ?array $cachedClaims = null;
  4.     public function __construct(
  5.         public string $jwt,
  6.     ) {}
  7.     public array $claims {
  8.         get {
  9.             if ($this->cachedClaims !== null) {
  10.                 return $this->cachedClaims;
  11.             }
  12.             // 假设 parseJwt() 做签名检查、base64 解码等
  13.             $this->cachedClaims = $this->parseJwt($this->jwt);
  14.             return $this->cachedClaims;
  15.         }
  16.     }
  17.     private function parseJwt(string $jwt): array
  18.     {
  19.         // ...
  20.         return [];
  21.     }
  22. }
复制代码
这很方便,但也是 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 拦截读写,这可能与引用冲突——特别是数组元素写入:
  1. $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 读、受限写)

非对称可见性让你可以为读和写设置不同的可见性。
示例:
  1. final class Money
  2. {
  3.     public function __construct(
  4.         public private(set) string $currency,
  5.         public private(set) int $cents,
  6.     ) {}
  7. }
复制代码
调用者可以读:
  1. echo $m->cents;
复制代码
但不能写:
  1. $m->cents = 500; // Error outside the class
复制代码
迁移指南阐明了规则:第一个可见性是 get-visibility,第二个控制 set-visibility,get visibility 不能比 set visibility 更窄。
这对 DTO 很重要:它给你一种"大部分不可变"的风格,而不必采用完整的值对象方法。
组合非对称可见性 + property hooks

这个组合经常替代经典的"私有属性 + getter + setter"。
  1. final class UserInput
  2. {
  3.     public private(set) string $email {
  4.         set => strtolower(trim($value));
  5.     }
  6.     public private(set) string $name {
  7.         set {
  8.             $v = trim($value);
  9.             if ($v === '') {
  10.                 throw new InvalidArgumentException('Name is required.');
  11.             }
  12.             $this->name = $v;
  13.         }
  14.     }
  15. }
复制代码

  • 读是 public(对模板、序列化器、调试友好)
  • 写是受控的(对不变量友好)
  • 样板代码保持低
特性:新数组辅助函数(array_find、array_find_key、array_any、array_all)

PHP 8.4 新增了数组搜索/检查函数。
array_find 和 array_find_key
  1. $users = [
  2.     ['id' => 1, 'active' => false],
  3.     ['id' => 2, 'active' => true],
  4.     ['id' => 3, 'active' => false],
  5. ];
  6. $first = array_find($users, fn ($u) => $u['active'] === true);
  7. // ['id' => 2, 'active' => true]
复制代码
如果没找到,返回 null——但如果值本身就是 null,你怎么区分"找到 null"和"没找到"?
你可以用 array_find_key() 来避免歧义(因为 key 不能是 null)。
  1. $key = array_find_key($users, fn ($u) => $u['active'] === true);
  2. if ($key === null) {
  3.     // 真的没找到
  4. }
  5. $firstActive = $users[$key];
复制代码
array_any 和 array_all 看起来简单——直到它们消除了噪音
例如:强制所有上传的文件都在大小限制内。
  1. $ok = array_all($files, fn ($f) => $f['size'] <= 5_000_000);
  2. if (!$ok) {
  3.     throw new RuntimeException('One or more files are too large.');
  4. }
复制代码
对于简单任务,这比经典的 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 样板代码
  1. #[\Deprecated(message: "Use slugify() instead", since: "2026-01")]
  2. function make_slug(string $s): string
  3. {
  4.     return strtolower(trim($s));
  5. }
  6. function slugify(string $s): string
  7. {
  8.     // 真正的实现
  9.     return strtolower(trim($s));
  10. }
  11. make_slug("Hello World");
复制代码
没什么问题。只是重复,特别是在几十个消息对象上。
8.4 之后:"public 读 + private 写 + hooks"
  1. $doc = Dom\HTMLDocument::createFromString($html, LIBXML_NOERROR);
  2. $canonical = $doc->querySelector('link[rel="canonical"]');
  3. $url = $canonical?->getAttribute('href');
复制代码
你得到:

  • 强类型
  • 集中化的规范化/验证
  • Public 可读性(在模板、日志、序列化器中方便)
  • 没有访问器样板
而且你仍然可以用测试保持严格。
一个现实的"DTO + 派生属性"模式
  1. final class CreateUserCommand
  2. {
  3.     private string $email;
  4.     private string $name;
  5.     public function __construct(string $email, string $name)
  6.     {
  7.         $this->email = strtolower(trim($email));
  8.         $this->name = trim($name);
  9.         if ($this->name === '') {
  10.             throw new InvalidArgumentException('Name is required.');
  11.         }
  12.     }
  13.     public function email(): string { return $this->email; }
  14.     public function name(): string { return $this->name; }
  15. }
复制代码
你可以保持存储字段干净,并提供一个友好的派生字段而不引入额外方法。
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 的更平滑路径。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册