0

PHP 8.3 — #[Override] attribute

Intermediate5 min read·php-09-021
compare

Concept

The #[Override] attribute, introduced in PHP 8.3, marks a method as intentionally overriding a parent class or interface method. If the annotated method does not actually override anything — because the parent renamed the method, the method was removed, or there is a typo — PHP raises a fatal error at class load time. This provides the same safety guarantee that Java's @Override and C#'s override keyword have offered for years.

Without #[Override], a common and silent bug is: you rename a method in a base class, but a subclass still has the old method name. PHP does not warn you — the subclass method becomes a new method, the override silently disappears, and the old behavior is restored through the now-unoverridden parent. This class of bug is particularly nasty in frameworks and large codebases with deep inheritance hierarchies.

#[Override] works on regular instance methods, static methods, and abstract method implementations. It does not work on constructors (PHP does not treat __construct overrides the same way since constructors have different inheritance semantics). The attribute carries no arguments — it is purely a declaration of intent.

In comparison to other languages:

LanguageConstructEnforcement
PHP 8.3+#[Override] attributeFatal error at class load
Java@Override annotationCompile error
C#override keywordCompile error
TypeScriptoverride keywordCompile error
PythonNo built-in mechanismLinters (mypy, pyright)
PHP < 8.3PHPDoc @inheritdocNone — documentation only

The attribute is particularly useful in codebases that use PHPStan or Psalm: even before PHP 8.3, these analyzers recognized #[Override]-equivalent patterns. Now that it is a language feature, it is enforced at the PHP engine level, making the check independent of your static analysis tooling.

Code Example

php
<?php
declare(strict_types=1);

abstract class EventHandler
{
    abstract public function handle(array $event): void;

    public function supports(string $type): bool
    {
        return false;
    }

    public function priority(): int
    {
        return 0;
    }
}

class UserCreatedHandler extends EventHandler
{
    #[Override]
    public function handle(array $event): void
    {
        // Process user.created event
        echo "Handling user.created for user #{$event['id']}\n";
    }

    #[Override]
    public function supports(string $type): bool
    {
        return $type === 'user.created';
    }

    #[Override]
    public function priority(): int
    {
        return 10;
    }
}

// This would cause a Fatal Error at class load time:
// class BrokenHandler extends EventHandler
// {
//     #[Override]
//     public function handl(array $event): void  // typo: "handl" not "handle"
//     {
//     }
// }

// Works with interfaces too
interface Serializable
{
    public function serialize(): string;
    public function unserialize(string $data): void;
}

class JsonPayload implements Serializable
{
    public function __construct(private array $data) {}

    #[Override]
    public function serialize(): string
    {
        return json_encode($this->data, JSON_THROW_ON_ERROR);
    }

    #[Override]
    public function unserialize(string $data): void
    {
        $this->data = json_decode($data, associative: true, flags: JSON_THROW_ON_ERROR);
    }
}

// Static method override
class BaseRepository
{
    public static function tableName(): string
    {
        return 'records';
    }
}

class UserRepository extends BaseRepository
{
    #[Override]
    public static function tableName(): string
    {
        return 'users';
    }
}

$handler = new UserCreatedHandler();
var_dump($handler->supports('user.created')); // bool(true)
var_dump($handler->priority());               // int(10)
$handler->handle(['id' => 42]);
echo UserRepository::tableName();             // users

Interview Q&A

Q: What class of bugs does #[Override] prevent, and give a concrete example from a real codebase?

#[Override] prevents silent override disappearance — when a parent method is renamed or deleted and a subclass's override silently becomes a new dead method. A real example: Laravel's Illuminate\Foundation\Exceptions\Handler has a method render(Request $request, Throwable $e): Response. If you override it in App\Exceptions\Handler but accidentally write renders() with a typo, Laravel falls back to the parent's generic rendering without any warning. Marking your override with #[Override] would have caused a fatal error at class load, immediately surfacing the mistake. This is especially critical in large teams where base classes are actively maintained and APIs change.


Q: Does #[Override] perform any runtime behavior change, or is it purely a declaration?

It is purely a declaration — adding #[Override] to a method does not change how PHP dispatches calls, how vtable slots are assigned, or how the method behaves at runtime. The only runtime effect is the fatal error thrown when the class is loaded if no matching parent or interface method exists. Once the class loads successfully, #[Override] has no further effect. You can read it via ReflectionMethod::getAttributes() for tooling purposes, but calling the method, profiling it, or tracing it behaves identically with or without the attribute.


Q: How does #[Override] compare to PHPStan's or Psalm's override checking, and do you still need static analysis?

PHP's #[Override] is a runtime-enforced check: it catches "this method overrides nothing" at class load time, which happens in production if you deploy broken code. PHPStan and Psalm can detect this earlier — at CI/CD time before deployment — and they also catch subtler override issues like mismatched signatures, covariance violations, and overrides of final methods. The two are complementary: #[Override] is the last line of defense at runtime and serves as machine-readable documentation; PHPStan/Psalm provide richer analysis earlier in the pipeline. In a well-run project you want both.