PHPドキュメントでよく使われるphpDocumentor、およびドキュメントの推奨フォーマットであるPSR-5・PSR-19には、何れも何故か@Override
がありません。
なんでや。
ということでPHP本体にアトリビュート#[\Override]
を追加するRFCが提案されました。
既に受理されており、PHP8.3から使えるようになります。
以下は該当のRFC、Marking overridden methodsの紹介です。
PHP RFC: Marking overridden methods (#[\Override])
Introduction
インターフェイスを実装したり、他クラスを継承したりする際に、PHPでは実装メソッドが親のメソッドと互換性があることを確認するために様々なチェックを行っています。
しかし、ひとつだけチェックできないことがあります。
それは、意図です。
PHPは、メソッドのシグネチャが親クラス・インターフェイスのメソッドと互換するかを検証します。
しかし、メソッドが親クラス・インターフェイスのメソッドをオーバーライドすることを意図しているかどうかを確認することはできません。
人間も同様です。
コミット履歴などを細かく調べていけばオーバーライドの意図を判断することができるかもしれませんが、最新の情報だけで明示的に表示することができたほうがよりシンプルでしょう。
以下の例では、あるメソッドが他のメソッドをオーバーライドするつもりかを表現することで、リファクタリングやクリーンアップに役立つ例を紹介します。
またライブラリが提供する親クラスが変更されたときに、変更点を見落としたり変更履歴を詳しく調べたりすることなく、おかしなところを検出することもできます。
なお、以下の例では実際のフレームワークを使っていますが、例なのでフレームワークのベストプラクティスに沿っていない場合があります。
Examples
インターフェイスのデフォルト実装にトレイトを使ってOverrideする例。
interface Formatter {
public function format(string $input): string;
public function isSupported(string $input): bool;
}
trait DefaultFormatter {
public function format(string $input): string
{
return $input;
}
public function isSupported(string $Input): bool
{
return true;
}
}
final class LengthRestrictedFormatter {
use DefaultFormatter;
public function __construct(private int $maxLength) {}
/*
正しくはisSupported()なのにisValid()とやってしまったがエラーは出ない。
isSupported()は常にtrueになってしまう。
*/
public function isValid(string $input): bool
{
return strlen($input) < $this->maxLength;
}
}
意図的にメソッドをオーバーライドする例。
namespace MyApp\Tests;
use PHPUnit\Framework\TestCase;
final class MyTest extends TestCase
{
protected bool $myProp;
/*
綴りを間違えたうえにfinalなので、このテストは一生動かない。
*/
protected function setUpp(): void
{
$this->myProp = true;
}
public function testItWorks(): void
{
$this->assertTrue($this->myProp);
}
}
オーバーライドしていたらいつのまにか親から削除された。
interface StringValidator {
public function validate(string $input): bool;
}
final class NonEmptyValidator implements StringValidator {
public function validate(string $input): bool
{
return $input !== '';
}
/*
かつては使われていたが今は使われていない?
それともNonEmptyValidatorがどこからか使ってる?
名前からしてインターフェイスの一部だったかもしれないけど確実ではない。
もはや何もわからない。
*/
public function getIdentifierForErrorMessage(): string
{
return 'string_must_not_be_empty';
}
}
いつのまにか親に追加された。
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Http;
class RssFeed extends Model {
/*
Laravel5.4でEloquentにrefresh()が追加されたが、
たまたまシグネチャが同じで意図せずオーバーライドしてしまっていた。
*/
public function refresh()
{
$this->message = Http::get($this->url);
$this->save();
}
}
Proposal
これらの意図をコードで表現できるように、アトリビュート#[\Override]
を追加します。
アトリビュートを記載した場合、親クラスもしくはインターフェイスに同名のメソッドが存在するかをチェックします。
メソッドが存在しなければコンパイルエラーになります。
上記例のうちLengthFormatter
とMyTest
においては、コンパイルエラーが発生するためミスを発見可能です。
NonEmptyValidator
では、インターフェイスを変更しようとした時点でコンパイルエラーが発生します。
RssFeed
の例においては、アトリビュートが直接エラーを発することはありません。
しかしIDEや静的解析ツールは、アトリビュート#[\Override]
が設定されていないけど実際はオーバーライドされているという診断結果を報告することができるでしょう。
診断結果から、refresh()
がメソッドのオーバーライドを意図していたのかを検出できる可能性が高まります。
Semantics
メソッドのシグネチャが異なる場合は、Fatal error: Declaration of X must be compatible with Y
のエラーとなり、#[\Override]
のエラーは出ません。
親クラスのpublic
およびprotected
メソッドが、#[\Override]
の対象となります。
abstract
メソッドも対象です。
static
メソッドはインスタンスメソッドのように振舞います。
private
は子クラスから見えないので、#[\Override]
の対象外です。
ENUMや匿名クラスは対象です。
インターフェイスは対象です。
トレイトでは基本的に#[\Override]
は無視されます。
Why an attribute and not a keyword?
このRFCがキーワードの追加でなくアトリビュートである理由は、これが可視性など他の修飾子と異なり、この機能によって作られたメソッドを使用するユーザに対する影響を与えないからです。
この機能は、純粋に実装者を支援するためのものです。
またキーワードを使わないことで後方互換性が高まります。
#[\Override]
を理解しないバージョンのPHPでもアトリビュートは問題なく動作し、解析ツールやIDEだけはアトリビュートの恩恵を受けることができます。
Precedent in other programming languages
他言語の類似機能。
Java:@Override annotation
TypeScript:override keyword
C++:override specifier
Csharp:override modifier
Kotlin:override modifier
Swift:override modifier
Static Analysis Tools and IDEs
静的解析ツールやIDEは、開発者にアトリビュートを追加するよう促す診断を出す必要があります。
これによって、与り知らないところでメソッドが追加されることによる想定しないオーバーライドを検出できるようになります。
Properties
プロパティのオーバーライドについては、本RFCの範囲ではありません。
現在のところインターフェイスにはプロパティがないため、親クラスのプロパティのみオーバーライドすることができます。
またプロパティの型は不変であり、さらにプロパティには振舞いが付随することもありません。
これらの理由により、プロパティへの#[\Override]
はメリットがほとんどありません。
用途が異なるのに名前と型が一致するプロパティが親クラスに導入された場合に、#[\Override]
ではそれを防ぐことができません。
Examples
様々な正しい例。
class P {
protected function p(): void {}
}
class C extends P {
#[\Override]
public function p(): void {}
}
class Foo implements IteratorAggregate
{
#[\Override]
public function getIterator(): Traversable
{
yield from [];
}
}
trait T {
#[\Override]
public function t(): void {}
}
trait T {
#[\Override]
public function i(): void {}
}
interface I {
public function i(): void;
}
class Foo implements I {
use T;
}
interface I {
public function i();
}
interface II extends I {
#[\Override]
public function i();
}
class P {
public function p1() {}
public function p2() {}
public function p3() {}
public function p4() {}
}
class PP extends P {
#[\Override]
public function p1() {}
public function p2() {}
#[\Override]
public function p3() {}
}
class C extends PP implements I {
#[\Override]
public function i() {}
#[\Override]
public function p1() {}
#[\Override]
public function p2() {}
public function p3() {}
#[\Override]
public function p4() {}
public function c() {}
}
以下はエラーが発生する例。
class C
{
#[\Override]
public function c(): void {}
}
// Fatal error: C::c() has #[\Override] attribute, but no matching parent method exists
interface I {
public function i(): void;
}
class P {
#[\Override]
public function i(): void {}
}
class C extends P implements I {}
// Fatal error: P::i() has #[\Override] attribute, but no matching parent method exists
trait T {
#[\Override]
public function t(): void {}
}
class Foo {
use T;
}
// Fatal error: Foo::t() has #[\Override] attribute, but no matching parent method exists
class P {
private function p(): void {}
}
class C extends P {
#[\Override]
public function p(): void {}
}
// Fatal error: C::p() has #[\Override] attribute, but no matching parent method exists
trait T {
public function t(): void {}
}
class C {
use T;
#[\Override]
public function t(): void {}
}
// Fatal error: C::t() has #[\Override] attribute, but no matching parent method exists
interface I {
#[\Override]
public function i(): void;
}
// Fatal error: I::i() has #[\Override] attribute, but no matching parent method exists
Backward Incompatible Changes
グローバル空間でクラス名Override
が使用できなくなります。
GitHubでクラス名Override
を調査したところ、94件がマッチしました。
大半は名前空間内でのマッチでしたが、一部グローバル空間のものもあります。
Proposed PHP Version(s)
PHP8.3。
Unaffected PHP Functionality
オブジェクトやクラスに関係しない機能は、本RFCの影響を受けません。
インターフェイスや継承を使用しないクラスは、本RFCの影響を受けません。
機能の全てがオプトインであるため、既存のコードは影響を受けません。
Future Scope
この項目は将来の展望であり、本RFCの範囲ではありません。
https://externals.io/message/120233#120522
オーバーライドするときは#[\Override]
アトリビュートを必須にするアトリビュート。
要するにnoImplicitOverride。
https://externals.io/message/120233#120391
親が複数あるときに、どの親をオーバーライドしているのかを明示するオプション。
Proposed Voting Choices
投票は2023/06/14から2023/06/28まで、投票数の2/3の賛成で受理されます。
本RFCは賛成22反対1の賛成多数で可決されました。
Patches and Tests
https://github.com/php/php-src/pull/9836
Implementation
https://github.com/php/php-src/commit/49ef6e209d8fbcb4694ecd59b9078498f0dffb73
References
Java:https://docs.oracle.com/javase/8/docs/api/java/lang/Override.html
TypeScript:https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-3.html#override-and-the---noimplicitoverride-flag
C++:https://en.cppreference.com/w/cpp/language/override
C#:https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/override
Kotlin:https://kotlinlang.org/docs/inheritance.html#overriding-methods
Swift:https://docs.swift.org/swift-book/documentation/the-swift-programming-language/inheritance/#Overriding
感想
個人的には前から@Override
を使っていたので、ようやく仕様になったかというかんじですね。
実は今回の#[\Override]
だけでは、知らぬ間に親クラスに同名メソッドが追加されてしまった事故を完全に防ぐことはできません。
しかし、静的解析ツールやIDEはそのあたりをきっと対応してくれると思うので、対応してくれれば開発時に気付けるようになります。
PHPがどんどん便利になりますね。