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(): string
  • setFoo(string $foo): void
  • 加上私有的 backing 字段
  • 加上在构造函数和工厂里重复的额外不变量

心智模型

带 hook 的属性仍然是属性。你还是用正常方式读写它:

php 复制代码
$user->email = "  ADMIN@EXAMPLE.COM  ";
echo $user->email;

但在底层,引擎把读写路由到 hook。

完整形式的 hook 长这样:

php 复制代码
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 存储------是派生/计算的,像伪装成 $areagetArea()

手册解释说,如果两个 hook 都没有用精确语法引用 $this->propertyName,属性就是 virtual 的,virtual 属性不占内存空间。

一个干净的 virtual property 示例:

php 复制代码
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。你想接受杂乱输入但存储规范化的值。

php 复制代码
final class UserProfile
{
    public string $email {
        set => strtolower(trim($value));
    }
}

在简写形式中,表达式结果成为存储的 backing value。

这已经很有用了,但生产代码通常还需要验证。

模式 B:在边界处验证不变量(尽早抛出)

例如,强制"用户名至少 3 个字符"并规范化空格。

php 复制代码
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 但不存储它。

php 复制代码
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:"计算一次,之后缓存"(谨慎使用)

有时计算值很昂贵(解析、构建对象)。你可以在对象内部缓存它。

php 复制代码
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 拦截读写,这可能与引用冲突------特别是数组元素写入:

php 复制代码
$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 读、受限写)

非对称可见性让你可以为读和写设置不同的可见性。

示例:

php 复制代码
final class Money
{
    public function __construct(
        public private(set) string $currency,
        public private(set) int $cents,
    ) {}
}

调用者可以读:

php 复制代码
echo $m->cents;

但不能写:

php 复制代码
$m->cents = 500; // Error outside the class

迁移指南阐明了规则:第一个可见性是 get-visibility,第二个控制 set-visibility,get visibility 不能比 set visibility 更窄。

这对 DTO 很重要:它给你一种"大部分不可变"的风格,而不必采用完整的值对象方法。

组合非对称可见性 + property hooks

这个组合经常替代经典的"私有属性 + getter + setter"。

php 复制代码
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

php 复制代码
$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)。

php 复制代码
$key = array_find_key($users, fn ($u) => $u['active'] === true);
if ($key === null) {
    // 真的没找到
}
$firstActive = $users[$key];

array_any 和 array_all 看起来简单------直到它们消除了噪音

例如:强制所有上传的文件都在大小限制内。

php 复制代码
$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

示例:

php 复制代码
#[\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"链接标签。

php 复制代码
$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\MySqlPdo\PgsqlPdo\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 风格:

  1. "到处都是 public 属性"
  2. "所有东西都是私有属性 + getter/setter"
  3. "readonly 提升属性"(好用,但死板)

PHP 8.4 增加了第四种,非常实用:

  • public 读,
  • 受控写,
  • 不变量靠近数据。

8.4 之前:常见的 DTO 样板代码

php 复制代码
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"

php 复制代码
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 + 派生属性"模式

php 复制代码
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=tracingopcache.jit_buffer_size=0
  • opcache.jit=disableopcache.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 的更平滑路径。

相关推荐
程序员爱钓鱼1 小时前
Node.js 博客系统实战(一):项目需求分析
前端·后端·node.js
BingoGo1 小时前
2026 年 PHP 8.4 依然重要:跳到 8.5 之前你该掌握的特性
后端·php
都叫我大帅哥2 小时前
Docker Swarm 部署方案
后端
都叫我大帅哥3 小时前
在Swarm中部署Nacos并配置外部MySQL
后端
想摆烂的不会研究的研究生10 小时前
每日八股——Redis(1)
数据库·经验分享·redis·后端·缓存
毕设源码-郭学长10 小时前
【开题答辩全过程】以 基于SpringBoot技术的美妆销售系统为例,包含答辩的问题和答案
java·spring boot·后端
追逐时光者11 小时前
精选 10 款 .NET 开源免费、功能强大的 Windows 效率软件
后端·.net
追逐时光者11 小时前
一款开源、免费的 WPF 自定义控件集
后端·.net
S***q37711 小时前
Spring Boot管理用户数据
java·spring boot·后端