PHP 中的 #[\Override] 注解

引言

在 PHP 8.3 中新增了一个非常实用的注解 #[\Override],它允许你标注某个方法是对父类方法的重写(override)。

在这篇速读文章里,我们将了解 #[\Override] 是什么、如何使用,以及它能带来的收益与对代码维护的帮助。

什么是 #[\Override] 注解?

如果你还没有接触过注解,可以先阅读我的《PHP8.x 注解使用指南》一文,里面解释了注解是什么、如何使用以及如何创建自定义注解。

#[\Override] 是 PHP 8.3 引入的新注解,用于显式标注某个方法是在重写父类中的对应方法。

例如,假设你有如下父类:

php
class ParentClass
{
    protected function someMethod(): void
    {
        // ...
    }
}

然后有一个子类继承该父类并重写 someMethod 方法:

php
class ChildClass extends ParentClass
{
    #[\Override]
    protected function someMethod(): void
    {
        // ...
    }
}

如上所示,我们通过 #[\Override] 明确表示 ChildClass::someMethod 是对 ParentClass::someMethod 的重写。

使用 #[\Override] 的好处

在运行时检测错误

#[\Override] 只能应用在确实重写了父类方法的方法上。

如果你把该注解应用到一个并未重写父类方法的方法上,PHP 会在方法被调用时抛出致命错误。这能帮助你更早捕获错误,避免意外行为。

沿用之前的示例,假设父类 ParentClass 中的 someMethod 被移除了(也许是团队中另一位开发者删的,或者它原本来自的依赖升级后删掉了)。运行你的应用时,你会看到如下错误:

Fatal error: ChildClass::someMethod() has #[\Override] attribute, but no matching parent method exists

借助静态分析检测错误

如果子类尝试重写的方法已不再存在于父类中,你如何在不运行应用的情况下知道?例如父类来自某个第三方 Composer 包,升级时维护者移除了你原本在子类里重写的方法。你可能直到运行应用时才发现。

使用 #[\Override] 可以让静态分析工具(如 PHPStan)在不运行的情况下就帮你发现此类问题。

继续前面的例子,若我们对代码运行 PHPStan,将看到类似错误:

shell
  Line   ChildClass.php

  42     Method ChildClass::someMethod() has #[\Override] attribute but does not override any method.

[ERROR] Found 1 error

这很好,因为你可以即时获得反馈,在问题进入生产前就将其修复。

顺带一提,我在《Battle Ready Laravel》中有一章专门展示如何使用 Larastan/PHPStan 来审计并改进你的 Laravel 应用。

更好的 IDE 支持

另一个好处是,你可以向 IDE 明确表达“此方法应当重写父类方法”的意图。

在诸如 PHPStorm 的 IDE 中,通常会用图标标识某个方法重写了父类方法。但这只能说明“当前确实发生了重写”,并不能表达“意图”。继续我们的例子,如果 ParentClass 里不再有 someMethod,PHPStorm 可能只是不会再显示重写图标,并不会直接告诉你“这本应当是一次重写,但现在不再是了”,从而容易被忽略。

当你显式标注 #[\Override] 后,如果方法不再重写父类对应方法,PHPStorm 会直接提示错误,明确告诉你存在问题。

更清晰的代码

#[\Override] 还能让代码更清晰易读。

当你第一次阅读一个类时,哪些方法是重写父类的,可能并不一目了然。给这些方法加上 #[\Override],就能一眼看出哪些方法是“有意图地重写”。这在大型代码库或不熟悉的代码中尤其有用,能帮助你更快建立正确的理解。

真实场景示例

为更直观地展示如何在实际项目中使用 #[\Override],我们来看一个例子。

假设我们在 Laravel 应用中有 App\Models\User,它继承自 Illuminate\Foundation\Auth\User。父类提供了一个空的 casts 方法,子类可以重写以声明模型属性的类型转换。

我们的 App\Models\User 可能像这样:

php
declare(strict_types=1);

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;

final class User extends Authenticatable
{
    // ...

    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'password' => 'hashed',
        ];
    }
}

在上面的 casts 方法中,我们声明 email_verified_at 使用 datetime 转换(通常从数据库取出时会被转换为 Carbon\Carbon 实例),password 使用 hashed 转换(在保存时会自动哈希)。

这些都是我们依赖其“自动生效”的操作,一旦出错后果可能很严重。例如我们绝不希望把 password 明文存到数据库!当然,Laravel 的 casts 机制本身是稳健可靠的,理想情况下你也应有测试保证字段按预期进行转换。

但假设在未来的 Laravel 版本中,casts 被一个新方法 castsAttributes 取代,Illuminate\Foundation\Auth\User 不再有空的 casts。这意味着我们的 App\Models\User 里依然有一个名为 casts 的方法,但它不再重写任何父类方法。可以推测,这将导致我们的字段不再按预期进行转换。

你如何得知这一变化?现实中很可能会出现“你以为在重写,但其实没有”的情况。也许你漏看了升级指南里的这段话;如果测试没有覆盖到,你可能直到线上行为异常、问题单出现才会注意到。

我们可以像这样给 casts 方法加上 #[\Override]

php
declare(strict_types=1);

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Override;

final class User extends Authenticatable
{
    // ...

    #[Override]
    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'password' => 'hashed',
        ];
    }
}

这样一来,如果 casts 在父类中被移除,#[\Override] 将导致抛出致命错误,从而立即向我们发出信号提示存在问题。

可以想见,这非常有用,能作为一道额外的防线,降低问题进入生产的概率。

结语

希望这篇文章能帮助你理解 #[\Override] 是什么,以及如何在代码中使用它。

JaguarJack
后端开发工程师,前端入门选手,略知相关服务器知识,偏爱❤️ Laravel & Vue
本作品采用《CC 协议》,转载必须注明作者和本文链接