ツイート
シェア
LINEで送る
B! はてぶでブックマーク
Pocketでブックマーク
RSSフィード

PHP8, アトリビュート(属性)とは何か?インターフェイスともちょっとちがう。

php
イラストダウンロードサイト【イラストAC】
の画像をもとに加工しています。

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

まずは関数を用意します。

attribute-func.php
<?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を使います。

attribute-func-info.php
<?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を使う処理は別ファイルで外出ししました。

今度は、属性から関数を実行してみます。

アトリビュートを使って関数を実行

アトリビュートを使ってプログラムを実行するファイルを用意します。

attribute-func-test.php
<?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公式ドキュメント

リフレクション

クラスオブジェクト

クラスオブジェクトはいろんなところにアトリビュートを付けれます。クラス、メソッド、メソッドのパラメータ、プロパティ。定数。

サンプルで見てみましょう。まずはクラスを用意します。

attribute-class.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を使って、アトリビュートを取得してみましょう。

attribute-class-info.php
<?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;
		}
	}
}

ポイントとしては、定数とプロパティは別個に考えられていることと、メソッドは関数とほぼ同じです。

(クラスの中にある関数がメソッドなので当たり前だけど。)

実行結果
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のときと同じ用にアトリビュートを使ってユニットテストちっくな実行をしましょう。

attribute-class-test.php
<?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のコンストラクタのパラメータはアトリビュートの設定

クラスの実行サンプルをちょっと変更してみます。

attribute-class-test.php
<?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-class.php
#[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との合わせ技で。

前処理や後処理などは良いんじゃないかな?

前の投稿
PHP8で非推奨、削除された関数や定数。非推奨のうちから使うのをやめよう!
PHP8, エラー制御演算子(@)でErrorは関係なく出力される
次の投稿
コメントを残す

*