PHP8ではアトリビュートというものが追加されました。一言で言えばアノテーション。プログラムにメタ情報を追加するものです。
一般的には、テスト用やプログラムのドキュメント作成に使うものなんですが、PHPの公式ドキュメントを見ると、プログラミングでも使えるっぽい言い方をしています。
アノテーションとは何か?
アノテーション(annotation)は注釈・注記という意味で、あるデータに対して付加情報(メタデータ)を追加するものです。
xmlなどでタグつけすることが多いんですが、プログラミングではコメントを使って付けます。
プログラミングにはコメントの規約があり、クラスやメソッド、プロパティ、関数の説明を書くコメントはとくにルール化されています。
そこで使われるのが、"@param" , "@return" などですが、これがアノテーション。
このルールに従えば、アノテーションからphpDocumentorなどがプログラムのドキュメントを作成するので便利です。
知らない内にコメントの一部としてアノテーションを使ってる人もいるでしょう。
アトリビュートはPHP8から導入された新しいアノテーションの仕様です。
ただのコメントじゃなくなった書式
あまり知られてませんが、'#'を書いた後ろの文字列はPHPでも部分コメントになります。'//'と同じ使い方。
PHP7までは'#[Attribute]'はただのコメントとして扱いましたが、PHP8ではそうじゃなくなります。
変わったからと言って消す必要はありません。一応、コメントではあるので、プログラムに影響はありません。
PHP8からは属性として使われます。(属性構文とも言う。)
PHPは多言語からの移植を考えて、コメントや属性を多言語のまま使える仕様になっています。'#[]' はRustの属性。
PHP7とPHP8の違いはこのサンプルが一番分かりやすい。
// PHP7ではパラメータは$param2だけ。
// PHP8ではパラメータは$param1, $param2。
function foo(
#[Attribute] $param1,
$param2
) { }
消さなくてもいいと言いましたがこのパターンはまずいですね?
普通はこのコメント化はしないと思うんですが、多言語からの移植では起きるかもしれません。注意が必要です。
アトリビュートの書き方
さっそく、アトリビュートの書き方をひとつずつやっていきましょう。結果からいうとアトリビュートはアノテーションの領域を超えています。
アトリビュートの中に、クラスのインスタンスを作成するメソッドまで用意されています。
属性を読むだけじゃなくて、それを使ってプログラミングで利用するところまで想定している。
文章だけだと『どういうこと?』なのでサンプルコードを交えながら見てみましょう。
function
まずは関数を用意します。
<?php
namespace MY_TEST;
#[ReadOnly]
#[Property(type: 'function', name: 'add')]
function add(#[Parameter] $value1, #[Parameter] $value2)
{
$result = $value1 + $value2;
echo 'add() result: ' . $value1 . ' + ' . $value2 . ' = ' . $result . PHP_EOL;
return $result;
}
属性はfuctionと同じように()で括ってパラメータの指定もできます。
今度は、アトリビュートの内容をプログラムで取得してみましょう。リフレクションAPIを使います。
<?php
require_once 'attribute-func.php';
// 関数のリフレクション取得
$refFunction = new ReflectionFunction('MY_TEST\add');
// 関数のアトリビュート取得
$attributes = $refFunction->getAttributes();
// 関数のアトリビュートの表示
foreach ($attributes as $attribute) {
// 属性名
echo 'attr : ' . $attribute->getName() . PHP_EOL;
// 属性のパラメータ
echo 'attr param: ' . PHP_EOL;
print_r($attribute->getArguments());
echo PHP_EOL;
}
// 関数のパラメータ取得
$parameters = $refFunction->getParameters();
// 関数のパラメータ名と属性を表示
foreach ($parameters as $parameter) {
// パラメータ名
echo 'func param : ' . $parameter->getName() . PHP_EOL;
// 属性名
echo 'func param attr: ' . PHP_EOL;
foreach ($parameter->getAttributes() as $paramAttr) {
echo ' ' . $paramAttr->getName() . PHP_EOL;
}
echo PHP_EOL;
}
php80 attribute-func-info.php
attr : MY_TEST\ReadOnly
attr param:
Array
(
)
attr : MY_TEST\Property
attr param:
Array
(
[type] => function
[name] => add
)
func param : value1
func param attr:
MY_TEST\Parameter
func param : value2
func param attr:
MY_TEST\Parameter
リフレクションAPIを使う処理は別ファイルで外出ししました。
今度は、属性から関数を実行してみます。
アトリビュートを使って関数を実行
アトリビュートを使ってプログラムを実行するファイルを用意します。
<?php
require_once 'attribute-func.php';
#[Function_Test1(name: 'MY_TEST\add', arg: [3, 5])]
#[Function_Test1(name: 'MY_TEST\add', arg: [8, 23])]
#[Function_Test2(name: 'MY_TEST\add', arg: [4, 89])]
function func_test1()
{
};
$refFunction = new ReflectionFunction('func_test1');
$attributes = $refFunction->getAttributes('Function_Test1');
$exec_func = '';
$exec_func_arg1 = '';
$exec_func_arg2 = '';
foreach ($attributes as $attribute) {
$arguments = $attribute->getArguments();
$exec_func = $arguments['name'];
$exec_func_arg1 = $arguments['arg'][0];
$exec_func_arg2 = $arguments['arg'][1];
$exec_func($exec_func_arg1, $exec_func_arg2);
}
func_test1()は、属性を付けるためだけのものなので名前はどうでもいいし処理は空でいいです。
php80 attribute-func-test.php
add() result: 3 + 5 = 8
add() result: 8 + 23 = 31
これ単体テスト(ユニットテスト)そのものです。アトリビュートはphpUnitを使わなくてもテストができます。
リフレクションAPIって何?
リフレクションAPIは、プログラムの内容を取得するクラスです。『リフレクション = 反省、熟考』の意味でも分かりますね?
関数名やパラメータ、クラス名や持っているメソッド、プロパティなどの情報が取れます。関数やクラスに付けるタイトルのコメントも。
お察しの通り、phpDocumentorみたいな、プログラムからドキュメントを作成することもできます。
そしてリフレクションAPIは、オブジェクトならインスタンスを生成したり、関数を実行することもできる。
テストツールのphpUnitとかでも使ってそう。
PHP8では、このリフレクションAPIにアトリビュートのクラスと取得のメソッド(getAttributes())が追加されました。
PHP公式ドキュメント
クラスオブジェクト
クラスオブジェクトはいろんなところにアトリビュートを付けれます。クラス、メソッド、メソッドのパラメータ、プロパティ。定数。
サンプルで見てみましょう。まずはクラスを用意します。
<?php
namespace MY_TEST;
use Attribute;
#[Attribute]
class MyAttribute
{
#[Define]
#[true]
const TEST = 'test';
#[Property]
public $value = '';
#[Constract]
public function __construct(#[Set_Property] $value)
{
echo '__construct() set value:' . $value . PHP_EOL;
$this->value = $value;
}
#[Get]
public function getValue()
{
echo 'getValue() value:' . $this->value . PHP_EOL;
return $this->value;
}
#[Set]
public function setValue(#[Set_Property] $value)
{
echo 'setValue() value:' . $value . PHP_EOL;
$this->value = $value;
}
}
今度はリフレクションAPIを使って、アトリビュートを取得してみましょう。
<?php
require_once 'attribute-class.php';
// クラスのリフレクションからアトリビュート取得
$reflection = new ReflectionClass(MY_TEST\MyAttribute::class);
$attributes = $reflection->getAttributes();
foreach ($attributes as $attribute) {
echo 'attribute class: ' . $attribute->getName() . PHP_EOL;
}
// クラス内の定数のリフレクションからアトリビュート取得
$refConstants = $reflection->getReflectionConstants();
foreach ($refConstants as $refConstant) {
$attributes = $refConstant->getAttributes();
foreach ($attributes as $attribute) {
echo ' - attribute const : ' . $attribute->getName() . PHP_EOL;
}
}
// クラス内のプロパティのリフレクションからアトリビュート取得
$refProperties = $reflection->getProperties();
foreach ($refProperties as $refProperty) {
$attributes = $refProperty->getAttributes();
foreach ($attributes as $attribute) {
echo ' - attribute property: ' . $attribute->getName() . PHP_EOL;
}
}
// クラス内のメソッドのリフレクションからアトリビュート取得
$refMethods = $reflection->getMethods();
foreach ($refMethods as $refMethod) {
$attributes = $refMethod->getAttributes();
foreach ($attributes as $attribute) {
echo ' - attribute method : ' . $attribute->getName() . PHP_EOL;
}
// メソッドのパラメータのアトリビュート
$parameters = $refMethod->getParameters();
foreach ($parameters as $parameter) {
$attributes = $parameter->getAttributes();
foreach ($attributes as $attribute) {
echo ' - attribute parameter: ' . $attribute->getName() . PHP_EOL;
}
}
}
PHP公式ドキュメント
ポイントとしては、定数とプロパティは別個に考えられていることと、メソッドは関数とほぼ同じです。
(クラスの中にある関数がメソッドなので当たり前だけど。)
php80 attribute-class-info.php
attribute class: MY_TEST\Attribute
- attribute const : MY_TEST\Define
- attribute property: MY_TEST\Property
- attribute method : MY_TEST\Constract
- attribute parameter: MY_TEST\Set_Property
- attribute method : MY_TEST\Get
- attribute method : MY_TEST\Set
- attribute parameter: MY_TEST\Set_Property
アトリビュートを使ってクラスを実行
functionのときと同じ用にアトリビュートを使ってユニットテストちっくな実行をしましょう。
<?php
require_once 'attribute-class.php';
use MY_TEST\MyAttribute;
#[MyAttribute(1234)]
class Test
{
}
$reflection = new ReflectionClass(Test::class);
$attributes = $reflection->getAttributes();
foreach ($attributes as $attribute) {
$instance = $attribute->newInstance();
$instance->getValue();
$instance->setValue('from attribute');
}
php80 attribute-class-test.php
__construct() set value:1234
アトリビュート名はクラス
リフレクションAPIのgetAttributes()の結果のオブジェクト(ReflectionAttribute)にはnewInstans()というメソッドがあります。
それを使って、Testクラスのアトリビュートを既存クラス(MyAttribute)のインスタンス生成の書式で書き、インスタンスを生成しました。
(そのインスタンスを使ってメソッドも実行している。)
これまでのサンプルでは、適当にアトリビュート名を付けていますが、newInstance()を使うときは必ず既存クラスがアトリビュート名じゃないといけません。
もちろん、パラメータもクラスのコンストラクターに合わせないといけません。
Attributeだってクラス
MyAttributeクラスのアトリビュート名にAttributeを使ってますが、これも既存クラスです。"use Attribute;" を書いているので気づいた人がいるかもしれませんが。
ちなみにこのuseがないとnewInstance()は失敗します。
attribute-class-test.php
PHP Fatal error: Uncaught Error: Attempting to use non-attribute class "MY_TEST\MyAttribute" as attribute in /home/.../attribute-class-test.php:15
Stack trace:
#0 /home/.../attribute-class-test.php(15): ReflectionAttribute->newInstance()
#1 {main}
thrown in /home/.../attribute-class-test.php on line 15
『アトリビュートのクラスは属性じゃないものを使おうとしている。』
Attributeがクラスじゃないと認識されてるんですね? 既存クラスじゃないものは属性じゃないと表現しているところからも、アトリビュート名 = 既存クラスを想定しているようです。
Attributeのコンストラクタのパラメータはアトリビュートの設定
クラスの実行サンプルをちょっと変更してみます。
<?php
require_once 'attribute-class.php';
use MY_TEST\MyAttribute;
#[MyAttribute(1234)]
#[MyAttribute('test')]
#[MyAttribute('test2')]
class Test
{
}
$reflection = new ReflectionClass(Test::class);
$attributes = $reflection->getAttributes();
foreach ($attributes as $attribute) {
$instance = $attribute->newInstance();
$instance->getValue();
$instance->setValue('from attribute');
}
アトリビュートは複数もてるんだから、連続して実行しようというサンプル。でもこのままだとエラーになります。
PHP Fatal error: Uncaught Error: Attribute "MY_TEST\MyAttribute" must not be repeated in /home/.../attribute-class-test.php:17
Stack trace:
#0 /home/.../attribute-class-test.php(17): ReflectionAttribute->newInstance()
#1 {main}
thrown in /home/.../attribute-class-test.php on line 17
Attributeクラスの初期設定では1回しか実行できず、繰り返し処理ができないもよう。アトリビュートを変更します。
#[Attribute(Attribute::IS_REPEATABLE)]
class MyAttribute
{
// 省略
}
php80 attribute-class-test.php
PHP Fatal error: Uncaught Error: Attribute "MY_TEST\MyAttribute" cannot target class (allowed targets: ) in /home/.../attribute-class-test.php:17
Stack trace:
#0 /home/.../attribute-class-test.php(17): ReflectionAttribute->newInstance()
#1 {main}
thrown in /home/.../attribute-class-test.php on line 17
アトリビュートを付けるターゲット(メソッドとかパラメータとか)の指定忘れ。初期値と同じALLを指定します。
#[Attribute(Attribute::TARGET_ALL | Attribute::IS_REPEATABLE)]
class MyAttribute
{
// 省略
}
php80 attribute-class-test.php
__construct() set value:1234
getValue() value:1234
setValue() value:from attribute
__construct() set value:test
getValue() value:test
setValue() value:from attribute
__construct() set value:test2
getValue() value:test2
setValue() value:from attribute
オブジェクト指向でもかんたんなユニットテストができそうですね?
これらのサンプルは公式ドキュメントを参考にしました。
ただ、Attributeのコンストラクタで指定するパラメータの全部が書いてなかったので、VS Codeエディタの入力補完から列挙します。
IS_REPEATABLE |
TARGET_ALL |
TARGET_CLASS |
TARGET_CLASS_CONSTANT |
TARGET_FUNCTION |
TARGET_METHOD |
TARGET_PARAMETER |
TARGET_PROPERTY |
アトリビュートを複数個書くもうひとつの方法
アトリビュートを複数個書く方法はもうひとつあります。
#[MyAttribute(1234), MyAttribute('test'), MyAttribute('test2')]
class Test
{
}
#[]の中でカンマ区切りでもできる。
アトリビュートで使えない文字列
ひとつ言い忘れてました。アトリビュートに使える文字列には制限があります。予約語のキーワードは使えません。シンタックスエラーが出ます。
PHP Parse error: syntax error, unexpected token "catch" in /home/.../attribute-class.php on line 9
PHP Parse error: syntax error, unexpected token "const" in /home/.../attribute-class.php on line 9
PHP Parse error: syntax error, unexpected token "function" in /home/.../attribute-class.php on line 9
PHPの公式ドキュメントでは予約語が使えないというより、こういう文言で書かれている。
アトリビュートの名前は、 名前空間の基礎 で説明している 非修飾名、修飾名、完全修飾名が指定できます。
PHP公式ドキュメント - アトリビュートの文法
字面を見ると何を言ってるのか分からないところがありますが、シンタックスエラーが出たら変えましょう。
グローバルの定数、変数
定数や変数はクラスの中だけでしかアトリビュートを付けれません。変数はクラスの中ではプロパティになります。(定数はクラス定数)
関数の中の変数や定数も取得するリフレクションAPIが無いので無理。
ReflectionNamedType, ReflectionParameter, ReflectionPropertyでもしかして? と思ったので試しましたが、やっぱりダメでした。
アトリビュートは既存クラスを想定しているので、そうなっているのでしょう。
アトリビュートでインターフェイスっぽい処理をする。
PHPの公式ドキュメントには、アトリビュートでインターフェイスみたいな処理を実装するサンプルもあります。
インターフェイスはクラスの共通ルールを決めるもの。これを守らないとエラーになる仕組みです。これをアトリビュートを使って実現します。
今回は、MyDataというデータを保持するクラスを用意し、id, nameが入っていないとデータとして異常にする処理を行います。
<?php
interface ActionHandler
{
public function get();
}
#[Attribute]
class Required
{
}
class MyData implements ActionHandler
{
public int $id = -1;
public string $name = '';
#[Required]
public function hasID()
{
if ($this->id === -1) {
throw new RuntimeException("ID not set");
}
}
#[Required]
public function hasName()
{
if (empty($this->name)) {
throw new RuntimeException("Name not set");
}
}
public function get()
{
return [$this->id, $this->name]
}
}
function executeAction(ActionHandler $actionHandler)
{
$reflection = new ReflectionObject($actionHandler);
foreach ($reflection->getMethods() as $method) {
$attributes = $method->getAttributes(Required::class);
if (count($attributes) > 0) {
$methodName = $method->getName();
$actionHandler->$methodName();
}
}
$mydata = $actionHandler->get();
print_r($mydata);
}
$action = new MyData();
//$action->id = 1;
//$action->name = "test name";
executeAction($action);
executeAction()は、データを取得するかんたんな関数ですが、取得する前にアトリビュートRequiredでデータチェックメソッドを走査してデータの整合性を担保します。
またリフレクションには、インスタンスから取得できるReflectionObjectが用意されています。これならReflectionClassからわざわざnewInstance()する必要もないし、既存のインスタンスも使えます。
php80 attribute-class-like-interface.php
PHP Fatal error: Uncaught RuntimeException: ID not set in /home/.../attribute-class-like-interface.php:22
Stack trace:
#0 /home/.../attribute-class-like-interface.php(49): MyData->hasID()
#1 /home/.../attribute-class-like-interface.php(61): executeAction()
#2 {main}
データをセットしていないのでエラーになりました。今度はidだけ設定してみます。
$action = new MyData();
$action->id = 1;
//$action->name = "test name";
executeAction($action);
php80 attribute-class-like-interface.php
PHP Fatal error: Uncaught RuntimeException: Name not set in /home/.../attribute-class-like-interface.php:30
Stack trace:
#0 /home/.../attribute-class-like-interface.php(49): MyData->hasName()
#1 /home/.../attribute-class-like-interface.php(61): executeAction()
#2 {main}
thrown in /home/.../attribute-class-like-interface.php on line 30
idチェックは通過して、nameチェックで引っかかりました。今度はデータを埋めてみましょう。
$action = new MyData();
$action->id = 1;
$action->name = "test name";
executeAction($action);
php80 attribute-class-like-interface.php
Array
(
[0] => 1
[1] => test name
)
アトリビュートの使い方がなんとなく分かってきました。
PHP公式ドキュメント
アトリビュートの使いみち
アトリビュートはPHP8の新機能の中でも全面に出されているもののひとつなんですが、今いち使いどころが分かっていませんでした。
アノテーションの進化版みたいなもんで、そこまで使うことはないと思っていたほど。
でも、それなりに使いみちはありそうです。
まず、かんたんなユニットテストで使えそう。
そして、クラスオブジェクトの処理やデータに対して、何らかの制限や条件を加えることもできます。インターフェイスやクラスの継承でできる部分をあえてアトリビュートにする必要はありませんが、それ以外の細かいところでは利用価値があり。
どちらもリフレクションAPIとの合わせ技で。
前処理や後処理などは良いんじゃないかな?