2020年も半分が過ぎ、COVID-19流行の渦中においても次期メジャーバージョンの8.0のリリースに向けた開発は引き続き順調に進んでいます。PHP 8.0のリリースを管理するリリースマネージャは、開発者の投票によりSara Golemonに決まりました。Saraは、FacebookによるHHVMプロジェクトにも貢献していた人物です。PHP 8.0のリリース予定日は2020年11月26日に決まり、PHP 8.0の最初のアルファ版が2020年6月25日にリリースされています。次の大きなマイルストーンは、2020年8月4日に予定されるフィーチャーフリーズと、それに続くベータ1版リリース(2020年8月6日を予定)となります。この段階でPHP 8に実装される機能が全て確定となります。
この連載では、これまで数回に渡りPHP 8の新機能について紹介してきました。今回は、前回の記事からの数か月間で新たに採用が決定した新機能のうちの代表的な機能である属性(attribute)について紹介します。
属性(attribute)の導入
クラス、変数(プロパティ)、関数(メソッド)などの宣言に構造化された属性を付与する機能が、言語としてサポートされます。この機能は、PHPの実行コード自体には影響を与えませんが、例えば静的解析ツールなどによるコード品質の確保やコンパイラによる判断を容易にしてくれる有用な付帯情報(メタデータ)となります。類似の機能は、JavaのAnnotations、C++などのAttributesでサポートされていますが、従来のPHPでは言語によるサポートが行われておらず、docコメントと呼ばれるコメントの中に@で始まる特殊な文字列を埋め込む手法が使われてきました。
PHPでは、PHP 7.1の開発時においても同様の機能の導入が提案されましたが、その際には反対多数で否決されています。今回導入される属性機能は、前回の提案に構文に改良を加えたものであるためattribute v2と呼ばれており、2020年5月に投票が行われ、賛成多数により導入が決定されました。
属性の指定は、@@属性名(引数)のように@@の後に属性名と引数を記述、対象とする要素(クラス、変数/プロパティ、関数/メソッドなど)の前に配置します。複数の属性を並べて指定することも可能です。なお、2020年3月に提案された当初の案の定義では、<<属性名(引数)>>のような記述となっており、一度、投票により5月4日に採用されました。しかし、6月3日に代案として本案が提案され、7月1日に賛成多数で採用されたため、約2か月の間に仕様が変更されることになりました。既に8月のフィーチャーフリーズが迫っていますが、今後も細部の仕様は変更される可能性があります。
本機能には、大きく分けて2種類のユースケースが想定されています。1番目は、PHPスクリプトのコンパイル時にZendエンジンが属性を読み込み、検証などの処理に使用するものです。例えば、関数の前に@@Jitを記述することで、JIT機能の対象と指定することなどが考えられます。今後、ZendエンジンのAPIに属性を取得する関数(例:zend_attribute_get())を追加し、PHPエクステンションで属性を取得できるようにすることも検討されています。
第2のユースケースは、静的解析ツールやフレームワークなどのアプリケーションで解析やパラメータを指定する手段として使用するものです。例えば、代表的なフレームワークであるSymfonyでは、以下のようにコメント中に記述したアノテーションによりパラメータを指定するクラスローダAnnotationClassLoaderをサポートしています。
/**
* @Route(“/Blog”)
*/
class Blog {
…
}
こうしたクラスローダにパラメータを指定する手段は、今回PHPのネーティブな機能として導入される属性機能で代替される可能性があります。
docコメントと異なり、属性は名前空間を認識するため、名前の衝突を回避することが可能です。また、変数の記述誤りなどに対する対応はコメント内に記述するよりも検出しやすいと考えられます。
属性は自動的にカスタムクラスとして定義され、それ自体@@Attributeで属性を指定することができます。例えば、関数またはメソッドとして属性Routeを定義する場合は以下となります。
@@Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD)
class Route { }
クラス、プロパティの属性の場合はそれぞれAttribute::TARGET_CLASS、 Attribute::TARGET_METHODを指定します。属性を一か所に複数指定可能にする場合、Attribute::IS_REPEATABLEを指定します。
では、実際にユーザプログラムで、どのように属性を取得することができるか、試してみましょう。リスト1に使用例を示します。このコードを実行するとリスト2の出力が得られます。
まず、リスト1の2~3行目で属性を定義するためにクラスRouteを定義し、属性Attributeとして、Attribute::TARGET_CLASSとAttribute::IS_REPEATABLEを指定しています。
リスト1の5~11行目がクラスFooの定義で、属性をクラス、プロパティ、メソッドにそれぞれ指定しています。5行目では、属性Routeに異なる引数を代入して複数回(2回)指定しています。
13行目以降が、属性を取得するコードの例になります。属性には、Reflection APIによりアクセスできます。まず、クラスFooのインスタンスを引数として ReflectionClassクラスのインスタンスを生成し、getAttributesメソッドにより属性を取得します(13~14行目)。
次に16~20行目においてクラス・プロパティ・メソッドの属性を表示します。16行目ではクラスへの属性を表示しており、対応するリスト2の出力は以下となります。
string(5) "Route"
int(1)
bool(true)
$a[0]は最初の属性を指しており、getTarget()メソッドの値はクラスを示す1となります。また、複数の属性が指定されているため、isRepeated()メソッドの値はtrueとなります。
ここで、2行目において、Attribute::TARGET_CLASSを指定しない場合は、16行目で例外を発生し、25行目で以下のエラーが出力されます。
string(57) "Attribute "Route" cannot target class (allowed targets: )"
また、同様にAttribute::IS_REPEATABLEを指定しない場合は、以下のようなエラーが出力されます。
string(38) "Attribute "Route" must not be repeated"
17~18行目は2つの属性の値を取得しており、リスト2の5~13行目が対応する出力となります。ここでは、getArguments()メソッドにより、属性の引数(“/”、”/home”)をそれぞれ取得しています。
次にプロパティxに指定した属性をgetPropertyメソッドにより取得します(20~21行目)。リスト2の14~18行目が対応する出力となり、引数“2+3*2”の値(8)が取得されています。
最後にメソッドmooの属性をgetMethodメソッドにより取得します。リスト2の19~23行目が対応する出力となります。ここで、@@Deprecatedという属性は、廃止されたメソッドを下位互換性のために残していることを指定する用途を想定しています。
リスト 1 属性の使用例
<?php
@@Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)
class Route { function __construct($path) {}}
@@Route("/") @@Route("/home")
class Foo {
@@PropertyAttribute(2+3*2)
public $x;
@@Deprecated("Use bar() instead")
public function moo() {}
}
$c = new Foo(); // インスタンス生成
$ref = new \ReflectionObject($c);
$a = $ref->getAttributes();
try {
var_dump(get_class($a[0]->newInstance()), $a[0]->getTarget(), $a[0]->isRepeated());
var_dump($a[0]->getName(), $a[0]->getArguments());
var_dump($a[1]->getName(), $a[1]->getArguments());
$a = $ref->getProperty('x')->getAttributes(); // プロパティxの属性を取得
var_dump($a[0]->getName(), $a[0]->getArguments());
$a = $ref->getMethod('moo')->getAttributes(); // メソッドmooの属性を取得
var_dump($a[0]->getName(), $a[0]->getArguments());
} catch (\Throwable $e) {
var_dump($e->getMessage());
}
--
リスト2 リスト1実行時の出力
--
string(5) "Route"
int(1)
bool(true)
string(5) "Route"
array(1) {
[0]=>
string(1) "/"
}
string(5) "Route"
array(1) {
[0]=>
string(5) "/home"
}
string(17) "PropertyAttribute"
array(1) {
[0]=>
int(8)
}
string(10) "Deprecated"
array(1) {
[0]=>
string(17) "Use bar() instead
}
今回サポートされた属性機能は、フレームワークなどの記述やPHPのエンジンの挙動に影響を与えるという意味で影響が大きい変更と考えられますが、まだまだ具体例が乏しいため、なじみにくいかもしれません。
現時点では言語としての基本的な機能が実装されたのみで、実際には今後フレームワークによるサポートを含むユースケースの整備や本機能をより使いやすくするためのPHP本体の機能整備が必要となると思われます。代表的なフレームワークであるSymfonyではすでにPHP8に対応する次期版での対応を検討しています。メジャーなフレームワークでの対応が進めば、ユースケースが蓄積され、広く普及していく可能性があります。
次回は、残るPHP 8.0における変更点について紹介します。
(2020年8月1日現在の情報に基づき掲載しています)