Shin x Blog

PHPをメインにWebシステムを開発してます。Webシステム開発チームの技術サポートも行っています。

PHP 8 Attribute シンタックスの変遷

PHP 8 の新機能の一つ、Attribute の形式が紆余曲折ありながら最終的に #[] となりました。実用上は #[] 形式だけ覚えておけば良いのですが、シンタックスの変遷は興味深いものだったので残しておこうと思います。

Attribute

PHP 8 の Attribute は、他のプログラミング言語によくあるアノテーションです。クラスやメソッド、プロパティ、関数に付与することで任意の処理を追加することができます。PHP では従来 Doc コメントでこれを指定する文化がありましたが、これを言語仕様として実装したものです。

<?php
declare(strict_types=1);

namespace Acme;

use PhpAttribute;

#[Attribute]
final class Attr1
{
    public function __construct(public string $name) {}
}  

#[Attr1("Bar")]
final class Foo {}  

PHP によるアノテーションは、hiro_y さんの発表資料が分かりやすいです。この資料は、Doc コメントベースのアノテーションですが、これが PHP 8 からは Attribute として言語機能に導入されます。

Attribute 仕様の変遷

Attribute は、PHP: rfc:attributes_v2 で提案されたものが実装されました。過去にもいくつか提案がなされていたのですが採択には至らず、PHP 8 をターゲットにしたこの RFC が採択されました。

当初の RFC が採択された後も、シンタックスの変更や改善を行う RFC が続けて提案され、最終的には現在の #[] となります。PHP 8 alpha1 から beta4 までの間にその時点での仕様が実装されているので、各バージョンではそれぞれの実装を試すことができます。

各バージョンの Docker イメージが公開されているので、幻となったシンタックスを試してみるのも一興です。

8.0.0alpha1: <<>>

RFC: https://wiki.php.net/RFC/attributes_v2

最初の RFC を実装したバージョンです。Attribute シンタックスは <<Foo>> です。RFC では @: も候補になっていました。

ユーザクラスを Attribute クラスにするには<<PhpAttribute>> を指定します。

<?php
declare(strict_types=1);

namespace Acme;

use PhpAttribute;

<<PhpAttribute>>
final class Attr1
{
    public function __construct(public string $name) {}
}

<<Attr1('Foo')>>
<<Attr2>>
final class Foo {}

$reflectionClass = new \ReflectionClass(Foo::class);
$attributes = $reflectionClass->getAttributes();

foreach ($attributes as $attr) {
    var_dump($attr->getName());
}

var_dump($attributes[0]->newInstance());
$ docker run --rm -it -v `pwd`:/app -w /app php:8.0.0alpha1-alpine php php8-attr-alpha1.php
string(10) "Acme\Attr1"
string(10) "Acme\Attr2"
object(Acme\Attr1)#4 (1) {
  ["name"]=>
  string(3) "Foo"
}

8.0.0alpha2

RFC: https://wiki.php.net/RFC/attribute_amendments

元の Attribute を改善する RFC が提案され、それが実装されたバージョンです。シンタックスに変化はありませんが、下記が変更となりました。

  • <<PhpAttribute>><<Attribute>> に変更。
    • \Attribute は言語が定義するクラス名になったので、ユーザランドでは利用できなくなる。
  • <<Attr1, Attr2>> のようなグループ化記法の追加。
    • RFC では採択されていますが、この時点では実装されず。
  • Attribute ターゲットの検証を追加。
    • Attribute を適用する対象を指定できる。(ex. クラスのみやメソッドと関数のみなど)
<?php
declare(strict_types=1);

namespace Acme;

use Attribute;

<<Attribute(Attribute::TARGET_CLASS)>>
final class Attr1
{
    public function __construct(public string $name) {}
}

<<Attr1('Foo')>>
<<Attr2>>
final class Foo {}

$reflectionClass = new \ReflectionClass(Foo::class);
$attributes = $reflectionClass->getAttributes();

foreach ($attributes as $attr) {
    var_dump($attr->getName());
}

var_dump($attributes[0]->newInstance());
$ docker run --rm -it -v `pwd`:/app -w /app php:8.0.0alpha2-alpine php php8-attr-alpha2.php
string(10) "Acme\Attr1"
string(10) "Acme\Attr2"
object(Acme\Attr1)#4 (1) {
  ["name"]=>
  string(3) "Foo"
}

ターゲットでは無い Attribute を利用すると下記のエラーがスローされます。

Fatal error: Uncaught Error: Attribute "Acme\Attr1" cannot target class (allowed targets: function) in /app/php8-attr-alpha2.php:26

8.0.0beta1: @@

RFC: https://wiki.php.net/RFC/shorter_attribute_syntax

<<>> シンタックスに異論が持ち上がり、別のシンタックスが提案されました。RFC では、冗長である、ネストが無い、ジェネリクスと混同する(今の PHP には無い)、多言語に 4 文字のシンタックスが無い(Hack は <<>> だったが @ に変更)といった理由が書かれています。

中でもシフト演算子と紛らわしいというのは面白いところです。

<<Attr1(1 >> SHIFT, 10), Attr2>>
class Foo{}

これらの理由が妥当なものかどうかは疑問に思う箇所が無いわけではないですが、投票により 3 つの候補( <<>>@@#[])から @@ 形式に変更されることになりました。

この記法により、前回採用されたグループ化は無かったことになりました。(グループ化しようがない)

<?php
declare(strict_types=1);

namespace Acme;

use Attribute;

@@Attribute(Attribute::TARGET_CLASS)
final class Attr1
{
    public function __construct(public string $name) {}
}

@@Attr1('Foo')
@@Attr2
final class Foo {}

$reflectionClass = new \ReflectionClass(Foo::class);
$attributes = $reflectionClass->getAttributes();

foreach ($attributes as $attr) {
    var_dump($attr->getName());
}

var_dump($attributes[0]->newInstance());
$ docker run --rm -it -v `pwd`:/app -w /app php:8.0.0beta1-alpine php php8-attr-atat.php
string(10) "Acme\Attr1"
string(10) "Acme\Attr2"
object(Acme\Attr1)#4 (1) {
  ["name"]=>
  string(3) "Foo"
}

@@ の問題

@@ で決定でめでたしと思いきや、ある問題が発覚します。下記のコードを見て下さい。

function func1(@@Foo \Bar $param1) {}

これは以下のどちらにも解釈できます。

  • Attribute: @@Foo , $param1 の型宣言が \Bar
  • Attribute: @@Foo\Bar$param1は型宣言無し。

実は、PHP では namespace が空白で区切られていても、それを読み飛ばして結合して解釈するようになっていました。例えば下記のような表記は PHP 7 では valid なので Foo\Bar を use します。

<?php
use Foo \ Bar;  

この問題は、他の RFC によって修正されます。これにより、PHP 8 以降では namespace は一つのトークンとして解釈する(空白が来たら namespace の一部とみなさない)ように変更されました。

https://wiki.php.net/rfc/namespaced_names_as_token

上記のコードも PHP 8 で実行すると Parse Error となります。もし、これまで空白を含めていた場合はこれも BC となるので注意が必要です。

$ docker run --rm -it -v `pwd`:/app -w /app php:8.0.0beta4-alpine php namespace.php

Parse error: syntax error, unexpected identifier "Bar", expecting "{" in /app/namespace.php on line 2

8.0.0beta4: #[]

https://wiki.php.net/RFC/shorter_attribute_syntax_change

namespace の問題により、明示的な終端文字を持たない@@ では他にもあいまいな解釈となるケースがあるのではないか、終端文字を持つシンタックスの方が良いのでは無いかという議論が出現します。

すでにシンタックスを一度変更しており、さらに Feature Freeze の期限が迫っている段階でしたので多くの意見が飛び交う中、最終的にもう一度シンタックスを問う RFC が提出されます。

@@ , #[], @[], <<>>, @:, @{} という候補の中、@@ が優れているという主張があったりもしましたが、結果的に投票で #[] となりました。

そして、めでたくグループ化シンタックス #[Attr1, Attr2] も実装されました。

<?php
namespace Acme;

use Attribute;

#[Attribute]
final class Attr1 {}

#[Attr1, Attr2]
final class Foo {}

$reflectionClass = new \ReflectionClass(Foo::class);
$attributes = $reflectionClass->getAttributes();

foreach ($attributes as $attr) {
    var_dump($attr->getName());
}

var_dump($attributes[0]->newInstance());
$ docker run --rm -it -v `pwd`:/app -w /app php:8.0.0beta4-alpine php php8-attr-rust.php
string(10) "Acme\Attr1"
string(10) "Acme\Attr2"
object(Acme\Attr1)#4 (1) {
  ["name"]=>
  string(3) "Foo"
}

#[] の注意点

採択された #[] は PHP 7 以前では普通にコメント扱いになっています。つまり、下記のようにコメントアウトしていたものが PHP 8 では Attribute として解釈されることになり、なぜかエラーが発生するという場面が起こりえます。

Attribute を使わなくても遭遇する可能性があるので、覚えておくと良いでしょう。

<?php
$a = [
    #[1, 2, 3],
    [10, 20, 30],
];
# PHP 7 なら ok
$ docker run --rm -it -v `pwd`:/app -w /app php:7-alpine php a.php
$

# PHP 8 なら error
$ docker run --rm -it -v `pwd`:/app -w /app php:8.0.0beta4-alpine php a.php

Parse error: syntax error, unexpected integer "1" in /app/a.php on line 3

さいごに

PHP 8 の Attribute シンタックスの変遷を見てきました。シンタックスに関しては慣れの要素もあり*1、明確な問題が無ければ時間が経てばという気もします。しかし、一度リリースされてしまうと後で容易に変更できるものではないので、二転三転はありましたが、良い塩梅に落ち着いたと思います。

まだ stable release では無いので、言語仕様として実装したものでも、フィードバックを受けて問題があるようならすぐに修正案が出て、再実装し、また別の案が出てという流れがオープンな場で繰り返されていたのは健全で良い動きでした。

<<>>@@は PHP 通だけが知っているトリビアになりそうですね。

*1:もう誰も namespace separator のこと言わない