如果你的团队计划"今年上 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"和"安全的领域对象"更像是语言原生支持的东西:
如果你在 8.4 上内化了这些,升级到 8.5 往往感觉像"添加一些好东西",而不是"把整个 PHP 写法现代化"。
所以这篇文章可以当作基线知识:如果你还没上 8.4,这是你为 8.5 做准备应该知道的——但不会变成完整的升级清单。
Property hooks 是 PHP 8.4 引入的。
它让你可以直接在属性上附加 get 和/或 set 逻辑。可以理解为"访问器方法",但不需要写:
getFoo(): stringsetFoo(string $foo): void带 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;
}
}
}这是手册里的示例,展示了核心思想:保留属性语法,同时获得集中化的行为。
Property hooks 可以创建两"种"属性:
$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 最适合的场景:
对于历史上滥用魔术 __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 后面,可能会让调用者意外。只在人体工学真正超过成本时使用这个模式。
PHP 允许在提升的属性上使用 hook,但有一个重要规则:传给构造函数的值必须匹配属性声明的类型——不管你的 set hook 可能接受什么。
也就是说你可以写:
DateTimeInterfacestring|DateTimeInterface…但如果你用提升,构造函数参数类型仍然是 DateTimeInterface。
如果你真的想"构造函数里也允许 string",你可能需要工厂或非提升的构造函数参数。
这对喜欢不可变对象的团队很重要。
手册明确说明:property hooks 与 readonly 属性不兼容。
所以如果你的风格是"到处都是不可变值对象",hooks 不能替代那个。Hooks 更适合的场景是:
(下一节会讲用非对称可见性实现"半不可变 DTO"。)
Hooks 拦截读写,这可能与引用冲突——特别是数组元素写入:
$obj->arr['k'] = 'v';文档警告说,获取引用或间接修改可能绕过 set hook,并概述了约束(如 &get 行为)和允许的情况。
实用指南:
$obj->tags = [...$obj->tags, $newTag];),这表现得像普通 set。Hooks 很棒……直到它们不是。在以下情况避免:
一个有用的规则:property hooks 最适合实现局部不变量和局部转换——真正属于属性本身的逻辑。
PHP 8.4 不只有 hooks。专注于以下类型的特性:
非对称可见性让你可以为读和写设置不同的可见性。
示例:
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 很重要:它给你一种"大部分不可变"的风格,而不必采用完整的值对象方法。
这个组合经常替代经典的"私有属性 + 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;
}
}
}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 + 标志变量。
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");这对团队来说被低估了:它给你一个标准化的方式来:
如果你在 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 意大利面"脚本。
PHP 8.4 引入了驱动特定的 PDO 子类,如 Pdo\MySql、Pdo\Pgsql、Pdo\Sqlite 等,并在发布公告中展示了新的连接风格。
在 PHP 8.4 示例中:
PDO::connect(...) 返回 Pdo\Sqlite这改善了正确性和 IDE 支持,特别是在测试和生产混用不同驱动的代码库中。
PHP 8.4 还引入了 lazy objects 概念:初始化被延迟到访问时才进行的对象。迁移指南明确指出框架可以利用它们来延迟获取依赖或数据。
它甚至展示了使用 ReflectionClass::newLazyGhost(...) 的核心机制。
这不是你在日常应用代码中会天天用的东西,但如果你做:
…值得知道它的存在,因为你会在生态系统内部看到它。
如果你写 PHP 很多年,你可能经历过至少三种 DTO 风格:
PHP 8.4 增加了第四种,非常实用:
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; }
}没什么问题。只是重复,特别是在几十个消息对象上。
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;
}
}你得到:
而且你仍然可以用测试保持严格。
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);
}
}你可以保持存储字段干净,并提供一个友好的派生字段而不引入额外方法。
因为 hooks 不能和 readonly 一起用,不可变值对象仍然依赖:
withX() 方法(在 PHP 8.5 里更好用了,但那是另一篇文章的事)所以很多团队的实际分工是:
这一节故意简短,但是能省时间的那种简短。
PHP 8.4 移除了 E_STRICT 错误级别,E_STRICT 常量已弃用。
如果你有遗留代码或配置引用了 E_STRICT,可能会看到 CI 行为变化。
PHP 8.4 中 JIT 配置的默认值变了:
opcache.jit=tracing 和 opcache.jit_buffer_size=0opcache.jit=disable 和 opcache.jit_buffer_size=64M这不会改变"JIT 默认关闭",但可能影响之前只切换其中一个值的环境。
8.4 不兼容列表中的一些例子:
round() 无效模式、str_getcsv() 无效分隔符长度)。这些是那种除非你尽早在 8.4 下运行测试套件,否则会表现为"随机测试失败"的变更。
如果你还没上 8.4——或者上了但没用这些特性——这是一个通常有效的安全顺序。
确保你能在 8.4 上运行测试套件而不出意外。把警告和弃用当作信号。
不要重构整个代码库。只是不要再写新的"foreach-with-break"循环,除非它们真的更清晰。
这是低风险的:你主要是改变属性声明并消除一类意外修改。
从以下开始:
一开始避免在 hooks 里放重逻辑。
用清晰的消息标记旧方法和辅助函数。在 CI 日志中跟踪使用情况。让弃用可操作。
如果你的应用做 HTML 解析,新 API 可能是很大的改进——但一开始保持采用范围受限。
知道这个特性存在。不要强行塞进应用代码,除非你有非常具体的性能或架构原因。
一旦你的团队熟悉了 PHP 8.4 的"现代基线":
#[Deprecated] 变得更系统化这个基线减少了迁移到 PHP 8.5 的摩擦,因为你已经现代化了代码库中对象和日常工具的写法。PHP 8.5 就不再是"追赶",而是选择性地采用改进。
PHP 8.4 不是一个"跳过它,直接上 8.5"的版本。在 2026 年,它仍然是一个明智的基线,因为它受支持、广泛相关,而且它改变了日常 PHP 的人体工学——尤其是在 OOP 密集的代码库中。
如果你从这篇回顾中只带走一件事,那就是 property hooks——但要带着意图使用:
这个组合让你今天就有更干净的 8.4 代码库——以及准备好时通往 8.5 的更平滑路径。