Object-oriented Hooks 入門:属性ベースで書く次世代 Drupal 拡張

Object-oriented Hooks 入門
目次

Drupalのモジュール開発において、従来のプロシージャル型フック実装から、よりモダンなオブジェクト指向型へのパラダイムシフトが進んでいます。本記事では、Drupal CMS 1.xで導入されたObject-oriented Hooks(OO Hooks) について、実践的な視点から解説します。

なぜ今OO Hooksなのか

大規模なDrupalプロジェクトで直面する課題を振り返ってみましょう。

  • フック関数の散在: mymodule_node_presave()、mymodule_form_alter()など、モジュール全体にフック実装が分散
  • テストの困難性: グローバル関数のモックが難しく、単体テストの実装が複雑
  • 型安全性の欠如: IDEの補完機能が限定的で、実行時エラーのリスク
  • 依存性管理の煩雑さ: サービスコンテナへのアクセスが冗長

OO Hooksは、これらの課題をPHPの属性(Attributes) を活用して解決します。

OO Hooksの基本構造

従来のプロシージャル型フック

// mymodule.module
function mymodule_node_presave(NodeInterface $node) {
  if ($node->bundle() === 'article') {
    $node->set('field_processed', TRUE);
    \Drupal::logger('mymodule')->notice('Article processed');
  }
}

 

OO Hooksによる実装

// src/Hook/MyModuleHooks.php
namespace Drupal\mymodule\Hook;

use Drupal\Core\Hook\Attribute\Hook;
use Drupal\node\NodeInterface;
use Psr\Log\LoggerInterface;

class MyModuleHooks {
  
  public function __construct(
    private readonly LoggerInterface $logger
  ) {}
  
  #[Hook('node_presave')]
  public function processArticleNode(NodeInterface $node): void {
    if ($node->bundle() === 'article') {
      $node->set('field_processed', TRUE);
      $this->logger->notice('Article processed');
    }
  }
}

 

実装ステップ:3分で始めるOO Hooks

Step 1: Drushジェネレーターで雛形作成

drush generate hook-attribute

このコマンドで、以下のファイルが自動生成されます。

  • src/Hook/MyModuleHooks.php
  • 必要に応じてmymodule.services.yml(オートワイヤリング対応のため省略可能)

Step 2: フック実装の追加

#[Hook('form_alter')]
public function customizeUserForm(array &$form, FormStateInterface $form_state, string $form_id): void {
  if ($form_id === 'user_login_form') {
    $form['#submit'][] = [$this, 'trackUserLogin'];
  }
}

#[Hook('entity_presave')]
#[Hook('entity_insert')]  // 複数のフックを1つのメソッドに適用可能
public function handleEntityChanges(EntityInterface $entity): void {
  // 共通処理
}

 

Step 3: 依存性注入(Dependency Injection)の活用

class MyModuleHooks {
  
  public function __construct(
    private readonly EntityTypeManagerInterface $entityTypeManager,
    private readonly CacheBackendInterface $cache,
    private readonly EventDispatcherInterface $eventDispatcher
  ) {}
  
  #[Hook('cron')]
  public function performMaintenanceTasks(): void {
    $nodes = $this->entityTypeManager
      ->getStorage('node')
      ->loadByProperties(['status' => 0]);
    
    // キャッシュを利用した効率的な処理
    $this->cache->set('maintenance_nodes', $nodes);
  }
}

 

高度な機能:実行順制御

複数のモジュールが同じフックを実装する場合、orderパラメータで優先度を明示できます。

#[Hook('node_presave', order: -100)]  // より早く実行
public function validateNodeData(NodeInterface $node): void {
  // バリデーション処理
}

#[Hook('node_presave', order: 100)]   // より遅く実行
public function finalizeNodeData(NodeInterface $node): void {
  // 最終処理
}

 

段階的移行戦略

既存プロジェクトでは、レガシーフックとの共存が重要です。

// mymodule.module
#[LegacyHook]
function mymodule_node_view(array &$build, NodeInterface $node, EntityViewDisplayInterface $display, $view_mode) {
  // 既存のプロシージャル実装を維持
}

// src/Hook/MyModuleHooks.php
class MyModuleHooks {
  #[Hook('node_presave')]
  public function modernNodeHandler(NodeInterface $node): void {
    // 新規実装はOO Hooksで
  }
}

 

テスト戦略:モックとユニットテスト

OO Hooksの最大の利点の一つは、テストの容易さです。

class MyModuleHooksTest extends UnitTestCase {
  
  public function testProcessArticleNode(): void {
    $logger = $this->createMock(LoggerInterface::class);
    $logger->expects($this->once())
      ->method('notice')
      ->with('Article processed');
    
    $hooks = new MyModuleHooks($logger);
    
    $node = $this->createMock(NodeInterface::class);
    $node->method('bundle')->willReturn('article');
    $node->expects($this->once())
      ->method('set')
      ->with('field_processed', TRUE);
    
    $hooks->processArticleNode($node);
  }
}

 

パフォーマンスとベストプラクティス

サービスの遅延読み込み

class MyModuleHooks {
  
  public function __construct(
    private readonly ContainerInterface $container
  ) {}
  
  #[Hook('cron')]
  public function heavyProcessing(): void {
    // 必要な時だけサービスを取得
    $processor = $this->container->get('mymodule.heavy_processor');
    $processor->execute();
  }
}

 

適切な粒度での分割

// X 避けるべき:巨大な単一クラス
class MyModuleHooks {
  // 50個のフック実装...
}

// O 推奨:機能別に分割
class NodeHooks { /* ノード関連のフック */ }
class FormHooks { /* フォーム関連のフック */ }
class CacheHooks { /* キャッシュ関連のフック */ }

 

今後の展望:任意サービスクラスへの拡張

Drupalコアでは、Hook属性を任意のサービスクラスに適用できる機能が検討されています(Issue #3481903)。

// 将来的に可能になる実装例
#[AsEventSubscriber]
class MyCustomService {
  
  #[Hook('node_presave')]
  public function handleNode(NodeInterface $node): void {
    // サービスクラス内でフック実装
  }
}

これにより、Single-Directory Components(SDC)やEventDispatcherとの統合がさらに進み、Drupal CMS 2.0以降のAPI変更にも柔軟に対応できるようになります。

まとめ

OO Hooksは、Drupalモジュール開発に以下のメリットをもたらします。

  • コード整理: フック実装がクラスに集約され、IDEナビゲーションが向上
  • テスト容易化: 依存性注入により、モックを使った単体テストが簡単に実現
  • 型安全性: PHPの型システムを最大限活用し、実行時エラーを削減
  • 段階的移行: #[LegacyHook]により、既存コードと共存しながら移行が可能

Drupal CMS 1.xを使用している、または移行を検討しているプロジェクトでは、OO Hooksの採用により、より保守性の高い、テスタブルなコードベースを実現できます。まずは小さなモジュールから始めて、チーム全体でこの新しいパラダイムを体験してみてください。

参考リンク

カテゴリ