#[\Override]属性で「オーバーライドしたつもり」を潰す
#[\Override] は PHP 8.3 で追加された属性で、そのメソッドが親クラスかインターフェースのメソッドを実際に上書きしているかを、実行前にエンジンに検証させる印です。
継承まわりのバグで一番やっかいなのは、エラーが何も出ないタイプです。メソッド名を一文字打ち間違えただけなのに、動くには動く。ただ自分の書いたメソッドは誰からも呼ばれていない。これを何度かやらかしてきたので、8.3でこの属性が入ったときは正直ほっとしました。
なぜ「上書きしたつもり」が起きるのか
PHPは、メソッドを上書きしたときにシグネチャの互換性はきっちり見てくれます。ただ「あなたはこれを上書きするつもりでしたか?」という意図までは分かりません。名前が違えば、それは単なる新しいメソッドとして受け入れられます。
final class MyTest extends TestCase
{
protected bool $ready;
// setUp と書いたつもりが setUpp。
// 親の setUp() は上書きされず、このメソッドは一度も呼ばれない。
protected function setUpp(): void
{
$this->ready = true;
}
public function testItWorks(): void
{
$this->assertTrue($this->ready); // 未初期化でコケる
}
}
テストが落ちてくれるならまだ幸運で、実務で怖いのはこれが本番コードで起きるパターンです。フレームワークのフックメソッド名を微妙に間違えていて、何も起きないまま数か月動き続ける、というやつですね。
#[\Override] はどう書く?
付けるのはメソッドの上だけです。属性クラス自体はグローバル名前空間に final class Override として定義されていて、対象は Attribute::TARGET_METHOD のみ。引数はありません。
class ParentClass
{
public function handle(): void {}
}
class Child extends ParentClass
{
#[\Override]
public function handle(): void {} // OK
#[\Override]
public function hadnle(): void {} // Fatal error
}
タイプミスした側は、実行時ではなくコンパイル時にこう止まります。
Fatal error: Child::hadnle() has #[\Override] attribute, but no matching parent method exists
インターフェースの実装でも同じように使えます。implements したインターフェースのメソッドを実装している場合、その実装メソッドに付けられます。インターフェース自身のメソッド宣言に付けることもできますが、その場合は「親インターフェースにそのメソッドがある」ことが条件になります。enumや無名クラスでもそのまま働きます。
class Bag implements IteratorAggregate
{
#[\Override]
public function getIterator(): Traversable
{
yield from [];
}
}
嬉しいのは、後日ライブラリ側がインターフェースからそのメソッドを削除したときです。黙って「ただの独自メソッド」に降格するのではなく、こちらが気づける形で落ちてくれます。
何が「親のメソッド」として認められるのか
判定の目安はRFCの言い方が分かりやすくて、「シグネチャを変えたら Declaration of X must be compatible with Y が出る関係かどうか」です。つまり互換性チェックの対象になっている親メソッドだけが、#[\Override] を満たします。
具体的には、親クラスやインターフェースの public / protected なメソッドが対象。抽象メソッドも対象になります。staticメソッドもインスタンスメソッドと同じ扱いです。一方で、次の2つは満たしません。
class P
{
private function secret(): void {}
public function __construct() {}
}
class C extends P
{
#[\Override]
public function secret(): void {} // Fatal error: privateは外部APIではない
#[\Override]
public function __construct() {} // Fatal error: コンストラクタは対象外
}
privateメソッドは子クラスから見えないので、そもそも上書きという関係が成立していない。コンストラクタは、生成済みオブジェクトのAPIの一部ではないという理由で除外されています。理屈としては一貫していますが、うっかり __construct に付けて怒られる人は多いと思います。プロパティにも付けられません(属性の対象がメソッドのみなので)。
トレイトに書いたときが一番ややこしい
ここが実務で一番誤解されるところです。「トレイトのメソッドに #[\Override] を付けると、使う側のクラスに実装を強制できる」と読みたくなりますが、そうではありません。
トレイトのメソッドは、使う側のクラスにコピペされたように振る舞います。なので #[\Override] も一緒にコピーされ、「そのクラスの親クラスかインターフェースに同名メソッドがあること」を要求します。
trait Greeting
{
#[\Override]
public function greet(): string { return 'hi'; }
}
interface Greeter
{
public function greet(): string;
}
class A implements Greeter
{
use Greeting; // OK: インターフェースに greet() がある
}
class B
{
use Greeting; // Fatal error: 一致する親メソッドが無い
}
使いどころは、インターフェースのデフォルト実装をトレイトで配るときです。トレイト側に #[\Override] を付けておくと、そのトレイトが「インターフェースを実装しているクラス」以外で使われた瞬間に気づけます。
逆向きのケースも覚えておくと事故が減ります。トレイトの通常メソッドを、使う側のクラスが同名メソッドで隠した場合、そのクラス側のメソッドは #[\Override] を満たしません。トレイトのメソッドは「上書き」ではなく単に負けて消えるだけだからです。ただし、トレイト内の抽象メソッドは #[\Override] を満たします。
trait T
{
public function run(): void {}
}
class C
{
use T;
#[\Override]
public function run(): void {} // Fatal error: トレイトの通常メソッドは親ではない
}
実務ではどこに付けるか
全部のメソッドに機械的に付ける必要はないと思っています。個人的に効果が大きいと感じているのは、この3か所です。
ひとつはテストクラスの setUp() / tearDown()。名前を間違えても何も言われない代表格です。ふたつめはフレームワークの基底クラスを継承したときのフックメソッド。バージョンアップで親のメソッドが消えたり改名されたりしたことに、その場で気づけます。みっつめはインターフェース実装クラスのメソッド全般で、これは静的解析ツールが「ここに付けられますよ」と提案してくれることも多いです。
導入コストが低いのもありがたい点で、属性の構文自体はPHP 8.0からあるため、8.0〜8.2では未知の属性として単に無視されます(リフレクションで解決しようとしない限りエラーになりません)。1行で書いていればPHP 7系でも # 以降が行コメント扱いになるので、構文エラーにはなりません。つまり、将来8.3に上げる前提のコードに先に書いておいても実害がない、ということですね。
まとめ
#[\Override] は挙動を一切変えない属性です。変えないからこそ気軽に付けられて、付けた分だけ「つもり違い」がコンパイル時に落ちてくる。private・コンストラクタ・トレイトの3つの例外だけ頭に入れておけば、あとは付けて回るだけの話だと思います。継承を多用するコードほど効きます。
よくある質問
Q. #[\Override] を付けると実行速度やメソッドの挙動は変わりますか?
A. 変わりません。コンパイル時に親メソッドの存在を確認するだけで、呼び出し側にも継承する側にも影響しません。純粋に書き手のための注釈です。
Q. PHP 8.2以下のコードに書いてしまっても大丈夫ですか?
A. 8.0〜8.2では未知の属性として無視されるだけで、エラーにはなりません。ただし当然チェックも働かないので、恩恵を受けたいなら8.3以降が必要です。
Q. プロパティやクラス定数にも付けられますか?
A. 付けられません。この属性の対象は Attribute::TARGET_METHOD のみで、プロパティはRFCの検討段階で対象外とされています。