【PHP8.4】Deprecatedを公式に書けるようになる

普段ドキュメントに`@deprecated`って書いてるんですが、これは公式ではなく[phpDocumentor](https://docs.phpdoc.org/guide/references/phpdoc/tags/deprecated.html#deprecated)の方言に過ぎません。
しかしまあ普及率も高いし公式でも使いたいよねってことと、あと標準関数のE_DEPRECATEDと動きを合わせたいということで、PHP8.4でアトリビュートとして実装されることになりました。

以下は[PHP RFC: #[\Deprecated] Attribute](https://wiki.php.net/rfc/deprecated_attribute)の紹介です。

# #[\Deprecated] Attribute

## Introduction

PHPの内部関数やクラス定数には非推奨マークを設定することができます。
これによってそれらを使うとE_DEPRECATEDの警告が発生し、Reflectionから利用することができます。
しかし、ユーザランドからは同じことができません。

非推奨関数を呼び出すときに[trigger_error()](http://www.php.net/trigger_error)を呼び出してE_USER_DEPRECATEDエラーを発生させるか、`/** @deprecated */`コメントをReflectionで読み取ることで似たようなことはできますが、これらは内部関数と同じ処理ではないため内部関数かユーザランドかによって処理の分岐が必要です。
`ReflectionFunctionAbstract::isDeprecated()`はユーザランド関数には常にfalseを返し、逆にドキュメントコメントは内部関数に付けられません。
クラス定数については同等の機能そのものがありません。


## Proposal

新しいアトリビュート`#[\Deprecated]`を導入します。

```php
#[Attribute(Attribute::TARGET_METHOD|Attribute::TARGET_FUNCTION|Attribute::TARGET_CLASS_CONSTANT)]
final class Deprecated
{
    public readonly ?string $message;
 
    public readonly ?string $since;
 
    public function __construct(?string $message = null, ?string $since = null) { /* … */ }
}
```

ユーザランド関数、メソッド、クラス定数にこのアトリビュートを設定すると、要素に内部フラグZEND_ACC_DEPRECATEDが設定され、内部関数のE_DEPTECATEDと一致する動作になります。
すなわち、以下のようになります。

・関数またはメソッドであれば、`ReflectionFunctionAbstract::isDeprecated()`がtrueになる。
・クラス定数であれば`ReflectionClassConstant::isDeprecated()`がtrueになる。
・関数を呼び出すとE_USER_DEPRECATEDが発生する。
・クラス定数を呼び出すとE_USER_DEPRECATEDが発生する。

`$message`は、関数やクラス定数を呼び出したときのエラーメッセージに入ります。
もちろん他のパラメータと同様、静的解析ツールからも使用可能です。

`$since`もエラーメッセージに含まれ、この要素がいつから非推奨になったを表します。
この中身は、検証されていない自由形式文字列である可能性があります。
バージョン番号、日付、その他ライブラリが適切であると見做すその他の値が含まれます。

ENUMのcaseは、内部的にはクラス定数として実装されています。
caseに`#[\Deprecated]`を追加すると、期待通りに動作します。


### Examples

様々な例。

```php
<?php
 
#[\Deprecated]
function test() {
}
 
#[\Deprecated("use test() instead")]
function test2() {
}
 
#[\Deprecated("use test() instead", since: "2.4")]
function test3() {
}
 
#[\Deprecated(since: "2024-05-07")]
function test4() {
}
 
class Clazz {
    #[\Deprecated]
    public const OLD_WAY = 'foo';
 
    #[\Deprecated]
    function test() {
    }
 
    #[\Deprecated("use test() instead")]
    function test2() {
    }
}
 
enum MyEnum {
    #[\Deprecated]
    case OldCase;
}
 
test(); // Deprecated: Function test() is deprecated in test.php on line 37
test2(); // Deprecated: Function test2() is deprecated, use test() instead in test.php on line 38
test3(); // Deprecated: Function test2() is deprecated since 2.4, use test() instead in test.php on line 39
test4(); // Deprecated: Function test4() is deprecated since 2024-05-07 in test.php on line 40
call_user_func("test"); // Deprecated: Function test() is deprecated in test.php on line 41
 
$cls = new Clazz();
$cls->test(); // Deprecated: Method Clazz::test() is deprecated in test.php on line 44
$cls->test2(); // Deprecated: Method Clazz::test2() is deprecated, use test() instead in test.php on line 45
Clazz::OLD_WAY; // Deprecated: Constant Clazz::OLD_WAY is deprecated in test.php on line 46
 
MyEnum::OldCase; // Deprecated: Enum case MyEnum::OldCase is deprecated in test.php on line 48
 
call_user_func([$cls, "test"]); // Deprecated: Method Clazz::test() is deprecated in test.php on line 50
```


```php
<?php
 
#[\Deprecated]
function test() {
}
 
$r = new ReflectionFunction('test');
 
var_dump($r->isDeprecated()); // bool(true)
```

```php
<?php
 
class Clazz {
    #[\Deprecated]
    public const OLD_WAY = 'foo';
}
 
$r = new ReflectionClassConstant(Clazz::class, 'OLD_WAY');
 
var_dump($r->isDeprecated()); // bool(true)
```

```php
<?php
 
#[\Deprecated]
function test1() {
}
 
#[\Deprecated()]
function test2() {
}
 
#[\Deprecated("use test() instead")]
function test3() {
}
 
#[\Deprecated("use test() instead", since: "2.4")]
function test4() {
}
 
#[\Deprecated(since: "2024-05-07")]
function test5() {
}
 
$reflection = new ReflectionFunction('test1');
var_dump($reflection->getAttributes()[0]->newInstance());
/*
object(Deprecated)#3 (2) {
  ["message"]=>
  NULL
  ["since"]=>
  NULL
}
*/
 
$reflection = new ReflectionFunction('test2');
var_dump($reflection->getAttributes()[0]->newInstance());
/*
object(Deprecated)#2 (2) {
  ["message"]=>
  NULL
  ["since"]=>
  NULL
}
*/
 
$reflection = new ReflectionFunction('test3');
var_dump($reflection->getAttributes()[0]->newInstance());
/*
object(Deprecated)#1 (2) {
  ["message"]=>
  string(18) "use test() instead"
  ["since"]=>
  NULL
}
*/
 
$reflection = new ReflectionFunction('test4');
var_dump($reflection->getAttributes()[0]->newInstance());
/*
object(Deprecated)#3 (2) {
  ["message"]=>
  string(18) "use test() instead"
  ["since"]=>
  string(3) "2.4"
}
*/
 
$reflection = new ReflectionFunction('test5');
var_dump($reflection->getAttributes()[0]->newInstance());
/*
object(Deprecated)#2 (2) {
  ["message"]=>
  NULL
  ["since"]=>
  string(10) "2024-05-07"
}
*/
```

[プルリクエスト](https://github.com/php/php-src/pull/11293/files?file-filters%5B%5D=.phpt&show-viewed-files=true)からさらなるテストケースを確認できます。

## Backward Incompatible Changes

下位互換性のない変更。

`Deprecated`は、グローバル空間のクラス名として使用できなくなります。
GitHubを軽く検索したところでは173件の一致が見つかりましたが、その大半は名前空間内で定義されているようです。


## Proposed PHP Version(s)

PHP8.4。


## Open Issues

少しだけ[指摘されています](https://github.com/php/php-src/pull/11293)。

## Future Scope

今後の展望であり、このRFCには含まれていません。

・クラスなど、今は非推奨をサポートしていないその他のターゲットにも`#[\Deprecated]`を使えるようにする。
・メッセージ以外に、IDEが使用できるヒントなどその他のメタデータを追加する。


## Proposed Voting Choices

賛成23反対6で受理されました。


## Patches and Tests

[プルリクエスト](https://github.com/php/php-src/pull/11293)。

## Implementation

・https://github.com/php/php-src/commit/72c874691b99a88feada151fca337ff9e471c31e
・https://github.com/php/php-src/commit/29f98e748568ebd66aaae061c0dcefbba92ca058


## References

[メーリングリスト](https://externals.io/message/112554#112554)。


# 感想

`@deprecated`わりと使うので助かる。

`@deprecated`はただのマーカーであり、強制力は特にありませんでした。
といってわざわざ手動でE_USER_DEPRECATEDを発生させるのも面倒だしなあと思っていたのですが、今後はいいかんじに気軽にDeprecatedを出して行けそうですね。
アトリビュートは未対応のバージョンではただのコメントなので、`#[\Deprecated]`と書いてあってもPHP8.3以前でも動作するので、互換性という点でも優れていますね。