クラスベースフックのアトリビュートについて


こんにちは。スタジオ・ウミの寺尾です。
先日、「Umi Unite Session」こと社員総会をオフラインで行いました。
その中でDrupalの開発情報を取り上げる時間があり、話題に上がったのがクラスベースフックです。
ブログ名:Drupal 11.1 から一部クラスベースになる予定 の hook を試してみる
この記事は、Drupalのhookがどのように進化したのかを非常にわかりやすく解説しており、新しい方法を学ぶ上で非常に参考になりました。
今回はDrupalのクラスベースフックを支えるPHPのアトリビュートという仕組みを解説します。
概要
Drupal 11.1にて、クラスベースフックが実装されました。 その際に、以下のような見慣れない書き方に気づいた方も多いのではないでしょうか?
#[Hook('help')]
これはアトリビュートという、php8から実装された標準の書き方です。
※重要な箇所はライブラリに依存していないところです。
アトリビュートとは
アトリビュートとは、コードに目印をつけるための機能です。 目印をつけると言うとコメント文と似ています。 コメント文はプログラムの実行には関係ありませんが、アトリビュートはプログラムのコードに対して、実行時に付加情報を与えるという点が異なります。 正確に解説するとコードにメタデータを付与し、リフレクションを活用して取得・利用できる仕組みです。
付加情報を与えると聞くと、プラグインシステムのアノテーションと似ていますが、まさに、アノテーションをPHPの標準でできるようにしたのがアトリビュートです。 アトリビュートはPHPの標準機能として提供されているため、アノテーションのようにPHPDocにコメントベースでの記述ではなく、ネイティブな構文で記述できるのが特徴です。
※アノテーション自体は、SymfonyやDrupalに含まれているライブラリです。
Drupalのモジュール開発に慣れている方だと、プラグインシステムのアノテーションを使った以下の書き方に馴染みがあると思います。
/**
* Provides a 'Fax' block.
*
* @Block(
* id = "fax_block",
* admin_label = @Translation("Fax block"),
* )
*/
class FaxBlock extends BlockBase {
}
アトリビュートで同等の事ができるようになった事で、事実上アノテーションの役目は終了し、今後はアトリビュートを使っていく流れになると予測しています。
Drupalのコア実装方法をバージョン毎に確認
実際にDrupalコアのブロックを定義しているコード見てみましょう。
アノテーションから、アトリビュートにコアのコードが書き換わっています。 今後開発する際はフックだけでは無く、プラグインシステムもアトリビュートを使った書き方にしていく方向のようです。
コードを書きながら、アトリビュートの仕組みを確認
アノテーションはライブラリのため、内部の仕組みを知らなくても、そういったライブラリだと納得して使う事ができました。 ただ、アトリビュートは標準の書き方のため、どういう風に実装したらフックをクラスベースに実装できるか内部の仕様が気になってきます。
Drupalのクラスベースフックのコアのコードの内容を読み、ほぼ同じ仕組みを簡易に再現したコードです。
フックを表す「Hookアトリビュート」を作る
アトリビュートを定義すると、アトリビュートを呼びだす際に、「__construct」で定義したデータを渡せるようになります。 メソッドやクラスで使う追加情報の設定を宣言します。
// Hookアトリビュートを定義 (メソッドのみで使用可能とする)
#[Attribute(Attribute::TARGET_METHOD)]
class Hook
{
public function __construct(
public string $hook_name,
public string $method = ''
) {}
public function setMethod(string $method): static {
$this->method = $method;
return $this;
}
}
メソッドにアトリビュートをつける
クラスに先ほど定義したフックのアトリビュートを書きます。 (page_attachments)という値は、hook_nameプロパティから取得できるようになります。
class TestHook
{
#[Hook('page_attachments')]
public function pageAttachments(array &$attachments)
{
$attachments['#attached']['library'][] = 'core/drupalSettings';
}
}
アトリビュートを取得する関数を定義する
- ReflectionClass → クラスの情報を取得可能(クラスに設定したアトリビュートの値)
- ReflectionMethod → メソッドに設定した情報を取得可能(メソッドに設定したアトリビュートの値)
リフレクション関連のクラスを使うことで、アトリビュートで設定した値を取得する事が可能になります。
他にも色々あり、ドキュメントを参考
にしてみてください。
/**
* クラス内のフックアトリビュートを取得する
*
* @param string $class
* クラス名.
*
* @return array
* フックのアトリビュートを持つオブジェクトの配列
*/
function getHookAttributesInClass(string $class): array {
$reflection_class = new ReflectionClass($class);
$class_implementations = [];
foreach ($reflection_class->getMethods(ReflectionMethod::IS_PUBLIC) as $method_reflection) {
foreach ($method_reflection->getAttributes(Hook::class,ReflectionAttribute::IS_INSTANCEOF) as $attribute_reflection) {
$hook = $attribute_reflection->newInstance();
$class_implementations[] = $hook->setMethod($method_reflection->getName());
}
}
return $class_implementations;
}
※ アトリビュートの定義でsetMethodを実装してメソッド名を配列に保存している箇所が重要です。
フックを実行する
先ほど定義したgetHookAttributesInClassを実行すると指定したクラス内でアトリビュート(#[Hook('フック名')])がついたメソッドのみを抽出し配列として取得できます。 その配列をループしてcall_user_func_arrayを使って動的にメソッドを実行します。
$hooks = getHookAttributesInClass('TestHook');
// TestHook のインスタンスを作成.
$instance = new TestHook();
// 引数を定義.
$args = [
'attachments' => [],
];
// クラス内のHookアトリビュートがついたメソッドのみをループ.
foreach ($hooks as $hook) {
if ($hook->hook_name == 'page_attachments') {
call_user_func_array([$instance, $hook->method], [&$args['attachments']]);
}
}
print_r($args);
すごく簡単にしましたが、このようにしてDrupalのフックをクラスベースで実現しています。
- アトリビュートと呼ばれる設計図を作る
- 設計図を元にメソッドやクラスの直上に値を記載する
- リフレクション関連のクラスからメソッドやクラスで定義したアトリビュートの値を取得する
- アトリビュートの値を元に実行する
まとめ
簡単なコードを書くことでアトリビュートとDrupalのコアで書かれている挙動の認識が深まりました。
これまで、同じフックは1つのモジュール内で1つしか実行できないという制限がありました。 しかし、アトリビュートを使用することで、1つのクラス内に異なるメソッドとして同じフックを複数回定義できるようになりました。 また、クラス単位で処理を整理できるため、コードのモジュール化が進み、分割・再利用が容易になるというメリットもあります。
クラスベースフックの利点
- 1つのフッククラスに機能ごとで処理を書いたり統一的に管理
- オブジェクト指向的に設計しやすくなり、メンテナンス性が向上
- Traitなどを使った柔軟な拡張が可能
- アトリビュートを使えば、1つのクラス内で別のメソッドとして同じフックを定義できる
また、フックは手軽にカスタマイズできる利点はありましたが、複雑なカスタマイズをしようとすると冗長になりがちでした。 今回のアップデートでクラスベースのフックがサポートされたことで、より整理されたコードでフックを実装できるようになり、Drupal開発の大きな進化といえるでしょう。 これからは、積極的にクラスベースのフックを活用して、より洗練されたDrupalのカスタマイズを実現していきたいです。