PHP8ではアトリビュートというものが追加されました。一言で言えばアノテーション。プログラムにメタ情報を追加するものです。
一般的には、テスト用やプログラムのドキュメント作成に使うものなんですが、PHPの公式ドキュメントを見ると、プログラミングでも使えるっぽい言い方をしています。
アノテーションとは何か?
アノテーション(annotation)は注釈・注記という意味で、あるデータに対して付加情報(メタデータ)を追加するものです。
xmlなどでタグつけすることが多いんですが、プログラミングではコメントを使って付けます。
プログラミングにはコメントの規約があり、クラスやメソッド、プロパティ、関数の説明を書くコメントはとくにルール化されています。
そこで使われるのが、"@param" , "@return" などですが、これがアノテーション。
このルールに従えば、アノテーションからphpDocumentorなどがプログラムのドキュメントを作成するので便利です。
知らない内にコメントの一部としてアノテーションを使ってる人もいるでしょう。
アトリビュートはPHP8から導入された新しいアノテーションの仕様です。
ただのコメントじゃなくなった書式
あまり知られてませんが、'#'を書いた後ろの文字列はPHPでも部分コメントになります。'//'と同じ使い方。
PHP7までは'#[Attribute]'はただのコメントとして扱いましたが、PHP8ではそうじゃなくなります。
変わったからと言って消す必要はありません。一応、コメントではあるので、プログラムに影響はありません。
PHP8からは属性として使われます。(属性構文とも言う。)
PHPは多言語からの移植を考えて、コメントや属性を多言語のまま使える仕様になっています。'#[]' はRustの属性。
PHP7とPHP8の違いはこのサンプルが一番分かりやすい。
消さなくてもいいと言いましたがこのパターンはまずいですね?
普通はこのコメント化はしないと思うんですが、多言語からの移植では起きるかもしれません。注意が必要です。
アトリビュートの書き方
さっそく、アトリビュートの書き方をひとつずつやっていきましょう。結果からいうとアトリビュートはアノテーションの領域を超えています。
アトリビュートの中に、クラスのインスタンスを作成するメソッドまで用意されています。
属性を読むだけじゃなくて、それを使ってプログラミングで利用するところまで想定している。
文章だけだと『どういうこと?』なのでサンプルコードを交えながら見てみましょう。
function
まずは関数を用意します。
属性はfuctionと同じように()で括ってパラメータの指定もできます。
今度は、アトリビュートの内容をプログラムで取得してみましょう。リフレクションAPIを使います。
リフレクションAPIを使う処理は別ファイルで外出ししました。
今度は、属性から関数を実行してみます。
アトリビュートを使って関数を実行
アトリビュートを使ってプログラムを実行するファイルを用意します。
func_test1()は、属性を付けるためだけのものなので名前はどうでもいいし処理は空でいいです。
これ単体テスト(ユニットテスト)そのものです。アトリビュートはphpUnitを使わなくてもテストができます。
リフレクションAPIって何?
リフレクションAPIは、プログラムの内容を取得するクラスです。『リフレクション = 反省、熟考』の意味でも分かりますね?
関数名やパラメータ、クラス名や持っているメソッド、プロパティなどの情報が取れます。関数やクラスに付けるタイトルのコメントも。
お察しの通り、phpDocumentorみたいな、プログラムからドキュメントを作成することもできます。
そしてリフレクションAPIは、オブジェクトならインスタンスを生成したり、関数を実行することもできる。
テストツールのphpUnitとかでも使ってそう。
PHP8では、このリフレクションAPIにアトリビュートのクラスと取得のメソッド(getAttributes())が追加されました。
PHP公式ドキュメント
クラスオブジェクト
クラスオブジェクトはいろんなところにアトリビュートを付けれます。クラス、メソッド、メソッドのパラメータ、プロパティ。定数。
サンプルで見てみましょう。まずはクラスを用意します。
今度はリフレクションAPIを使って、アトリビュートを取得してみましょう。
PHP公式ドキュメント
ポイントとしては、定数とプロパティは別個に考えられていることと、メソッドは関数とほぼ同じです。
(クラスの中にある関数がメソッドなので当たり前だけど。)
アトリビュートを使ってクラスを実行
functionのときと同じ用にアトリビュートを使ってユニットテストちっくな実行をしましょう。
アトリビュート名はクラス
リフレクションAPIのgetAttributes()の結果のオブジェクト(ReflectionAttribute)にはnewInstans()というメソッドがあります。
それを使って、Testクラスのアトリビュートを既存クラス(MyAttribute)のインスタンス生成の書式で書き、インスタンスを生成しました。
(そのインスタンスを使ってメソッドも実行している。)
これまでのサンプルでは、適当にアトリビュート名を付けていますが、newInstance()を使うときは必ず既存クラスがアトリビュート名じゃないといけません。
もちろん、パラメータもクラスのコンストラクターに合わせないといけません。
Attributeだってクラス
MyAttributeクラスのアトリビュート名にAttributeを使ってますが、これも既存クラスです。"use Attribute;" を書いているので気づいた人がいるかもしれませんが。
ちなみにこのuseがないとnewInstance()は失敗します。
『アトリビュートのクラスは属性じゃないものを使おうとしている。』
Attributeがクラスじゃないと認識されてるんですね? 既存クラスじゃないものは属性じゃないと表現しているところからも、アトリビュート名 = 既存クラスを想定しているようです。
Attributeのコンストラクタのパラメータはアトリビュートの設定
クラスの実行サンプルをちょっと変更してみます。
アトリビュートは複数もてるんだから、連続して実行しようというサンプル。でもこのままだとエラーになります。
Attributeクラスの初期設定では1回しか実行できず、繰り返し処理ができないもよう。アトリビュートを変更します。
アトリビュートを付けるターゲット(メソッドとかパラメータとか)の指定忘れ。初期値と同じALLを指定します。
オブジェクト指向でもかんたんなユニットテストができそうですね?
これらのサンプルは公式ドキュメントを参考にしました。
ただ、Attributeのコンストラクタで指定するパラメータの全部が書いてなかったので、VS Codeエディタの入力補完から列挙します。
IS_REPEATABLE |
TARGET_ALL |
TARGET_CLASS |
TARGET_CLASS_CONSTANT |
TARGET_FUNCTION |
TARGET_METHOD |
TARGET_PARAMETER |
TARGET_PROPERTY |
アトリビュートを複数個書くもうひとつの方法
アトリビュートを複数個書く方法はもうひとつあります。
#[]の中でカンマ区切りでもできる。
アトリビュートで使えない文字列
ひとつ言い忘れてました。アトリビュートに使える文字列には制限があります。予約語のキーワードは使えません。シンタックスエラーが出ます。
PHPの公式ドキュメントでは予約語が使えないというより、こういう文言で書かれている。
アトリビュートの名前は、 名前空間の基礎 で説明している 非修飾名、修飾名、完全修飾名が指定できます。
PHP公式ドキュメント - アトリビュートの文法
字面を見ると何を言ってるのか分からないところがありますが、シンタックスエラーが出たら変えましょう。
グローバルの定数、変数
定数や変数はクラスの中だけでしかアトリビュートを付けれません。変数はクラスの中ではプロパティになります。(定数はクラス定数)
関数の中の変数や定数も取得するリフレクションAPIが無いので無理。
ReflectionNamedType, ReflectionParameter, ReflectionPropertyでもしかして? と思ったので試しましたが、やっぱりダメでした。
アトリビュートは既存クラスを想定しているので、そうなっているのでしょう。
アトリビュートでインターフェイスっぽい処理をする。
PHPの公式ドキュメントには、アトリビュートでインターフェイスみたいな処理を実装するサンプルもあります。
インターフェイスはクラスの共通ルールを決めるもの。これを守らないとエラーになる仕組みです。これをアトリビュートを使って実現します。
今回は、MyDataというデータを保持するクラスを用意し、id, nameが入っていないとデータとして異常にする処理を行います。
executeAction()は、データを取得するかんたんな関数ですが、取得する前にアトリビュートRequiredでデータチェックメソッドを走査してデータの整合性を担保します。
またリフレクションには、インスタンスから取得できるReflectionObjectが用意されています。これならReflectionClassからわざわざnewInstance()する必要もないし、既存のインスタンスも使えます。
データをセットしていないのでエラーになりました。今度はidだけ設定してみます。
idチェックは通過して、nameチェックで引っかかりました。今度はデータを埋めてみましょう。
アトリビュートの使い方がなんとなく分かってきました。
PHP公式ドキュメント
アトリビュートの使いみち
アトリビュートはPHP8の新機能の中でも全面に出されているもののひとつなんですが、今いち使いどころが分かっていませんでした。
アノテーションの進化版みたいなもんで、そこまで使うことはないと思っていたほど。
でも、それなりに使いみちはありそうです。
まず、かんたんなユニットテストで使えそう。
そして、クラスオブジェクトの処理やデータに対して、何らかの制限や条件を加えることもできます。インターフェイスやクラスの継承でできる部分をあえてアトリビュートにする必要はありませんが、それ以外の細かいところでは利用価値があり。
どちらもリフレクションAPIとの合わせ技で。
前処理や後処理などは良いんじゃないかな?