Drupal 8 プラグイン その 1: プラグインの作り方

Drupal 8 で新たに導入された「 プラグイン 」の概念とその作り方について解説してみたいと思います。

まずはじめに Drupal 8 のプラグインとは何ぞやという概念の説明をした後に具体的な実装方法について説明していきます。

本稿では執筆時点で最新の Drupal 8.3.2 を対象としています。 バージョンが進むとコードの書き方が変わることなどもあるためその点にご注意ください。

想定読者

想定読者は「 プログラムは知っているけれど Drupal にはあまり馴染みがない技術者 」の方です。 コードのお話が出てきます。

プラグインとは

Drupal 8 においてプラグインとは 小さな機能を提供するプログラムパーツ のことです。

インタフェースが共通で同様の用途に使われるプラグインがグループとしてまとめられており、そのグループのことを「 プラグインタイプ 」と呼びます。 「プラグインタイプとプラグインの関係」はオブジェクト指向における「クラスとインスタンスの関係」に似ています。

CMS でプラグインと聞くと WordPress のプラグインを思い浮かべる方もいらっしゃるかと思いますが、 Drupal のプラグインは WordPress のプラグインとはまったくの別物です。 どちらかというと WordPress のプラグインに相当するものは Drupal では「 モジュール 」と呼ばれており Drupal 7 以前から存在します。 一方の Drupal のプラグインは Drupal 8 でコアに導入された新しい概念 / システムです。

Drupal 8 におけるプラグインとモジュールの概念を比較してみます。

共通点

  • 特定の機能を提供するプログラムのパーツである。
  • プラガブルであり、コアを改変することなく機能を追加することができる。

相違点

  • プラグインはモジュールよりも粒度が小さい。
  • プラグインの実体の最小構成はひとつのクラスファイルであり、モジュールは info.yml ファイルを格納したディレクトリである。
  • プラグインはプラグインタイプで分類されるが、モジュールにはそのような分類はない。

プラグインとモジュールの関係

  • プラグインはモジュールの中で定義される。
  • ひとつのモジュールは複数のプラグインを格納することができる。

drupal.org (d.o.) の説明も見ておきましょう。 本稿執筆時点で d.o. ではプラグインについて次のような説明がなされています。

Plugins are small pieces of functionality that are swappable. Plugins that perform similar functionality are of the same plugin type.

意訳: プラグインは特定の機能を提供する小さなパーツであり差し替え可能なものです。同様の機能を提供するプラグインは共通のプラグインタイプに所属します。

・・・概念的な説明だけを読んでもどうもよくわからない感じですよね。 具体的なコードの話に入っていきましょう。

コードの観点で見た場合、 Drupal 8 におけるプラグインとは「 特定のインタフェースを実装したアノテーション付きの PHP クラス 」です(ただし、後述しますがすべてのプラグインがこのパターンにあてはまるわけではありません)。

例えば、 Drupal 8 におけるブロックシステムはプラグインシステム上に構築されており、ブロックはそれぞれプラグインクラスとして実装されています( Drupal におけるブロックとはページ内ウィジェットのことです)。 一例として、コアに同梱のログインフォーム用のブロックについて見てみます。 このブロックの場合対応するプラグインクラスは UserLoginBlock です。

Drupal\user\Plugin\Block\UserLoginBlock:

<?php

namespace Drupal\user\Plugin\Block;

// 省略

/**
 * Provides a 'User login' block.
 *
 * @Block(
 *   id = "user_login_block",
 *   admin_label = @Translation("User login"),
 *   category = @Translation("Forms")
 * )
 */
class UserLoginBlock extends BlockBase implements ContainerFactoryPluginInterface {

  // 省略

}

?>

ここでは一見わからないのですが、 UserLoginBlock の親になっている BlockBase クラスが BlockPluginInterface というインタフェースを実装しており、この BlockPluginInterface がブロックの場合の「特定のインタフェース」に相当します。 そして、クラスの直前のコメント内の @Block( から ) までの部分が「アノテーション付き」の「アノテーション」にあたります。

Drupal 7 には hook_xxx_info() という形式のフック関数がたくさんありましたが、その多くが Drupal 8 ではプラグインシステムへと置き換えられました。 例えば、 Drupal 7 でカスタムブロックといえば hook_block_info()hook_block_view() hook_block_configure() などのフック関数で実装するものでした。 Drupal 8 ではカスタムブロックの追加はブロックプラグインクラスを実装する形で行います。 同様に、 Drupal 7 でイメージエフェクトを追加するにはフック関数 hook_image_effect_info() と各種コールバック関数を実装する形になっていましたが、 Drupal 8 ではイメージエフェクトプラグインのクラスを実装する形でイメージエフェクトを追加するようになっています。

ここであげたブロックやイメージエフェクトにはほんの一例です。 Drupal 8 では以下の要素等も含むありとあらゆる場所で新しいプラグインシステムが利用されています。

  • フィールドタイプ
  • フィールドフォーマッター
  • フィールドウィジェット
  • メール送信エンジン
  • キューワーカー
  • Views ハンドラー

プラグインの作り方

つづいて、実際のプラグインの作り方について見ていきましょう。 こちらが本稿のメイン部分です。

プラグインは上述のとおり「特定のインタフェースを実装したアノテーション付き PHP クラス」です。 これを実装するには「 特定のインタフェース 」と「 対応するアノテーション仕様 」に加えて「 ファイルの配置場所 」について知っておく必要があります。

これらについて正しく理解するには「プラグインタイプとは何ぞや」というそもそもの部分についてもう少し詳しく理解しておかなければなりません。

プラグインタイプとは

プラグインタイプとは何でしょうか。 プラグインタイプとはある種類のプラグインが従うべきルールを定めたものです。 その実体は次の 3 つのクラスの組み合わせです(すべてのプラグインがこの形になっているわけではありません)。

  • プラグインマネージャー プラグインの取り扱い方法を定義する
  • プラグインアノテーション プラグインのメタ情報を定義する
  • プラグインインタフェース 各プラグインが従うべきインタフェースを定義する

このうち中心的な役割を担うのはプラグインマネージャーです。 プラグインマネージャーがプラグインアノテーションやプラグインインタフェースをプラグインシステムに知らせる役割を果たします。

上述の「特定のインタフェース」というのは実はこのプラグインインタフェースのことで、「対応するアノテーション仕様」というのはプラグインアノテーションのことです。

ブロックの場合を例に具体的に見てみましょう。 ブロックの場合、プラグインのマネージャー / アノテーション / インタフェースはそれぞれ次のクラスです。

  • プラグインマネージャー: Drupal\Core\Block\BlockManager
  • プラグインアノテーション: Drupal\Core\Block\Annotation\Block
  • プラグインインタフェース: Drupal\Core\Block\BlockPluginInterface

つまりブロックの場合、特定のインタフェースというのは BlockPluginInterface であり、対応するアノテーションは Block クラスということになります。

プラグインマネージャーのポイント

プラグインマネージャーは特に重要なのでその中身を少しだけ確認してみましょう。 ブロックの場合のプラグインマネージャークラス BlockManager は 100 行以下の小さなクラスです。 主要な部分は以下のとおりとなっています。

Drupal\Core\Block\BlockManager:

<?php

// 省略

/**
 * Manages discovery and instantiation of block plugins.
 *
 * @todo Add documentation to this class.
 *
 * @see \Drupal\Core\Block\BlockPluginInterface
 */
class BlockManager extends DefaultPluginManager implements BlockManagerInterface, FallbackPluginManagerInterface {

  // 省略

  /**
   * Constructs a new \Drupal\Core\Block\BlockManager object.
   *
   * @param \Traversable $namespaces
   *   An object that implements \Traversable which contains the root paths
   *   keyed by the corresponding namespace to look for plugin implementations.
   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
   *   Cache backend instance to use.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler to invoke the alter hook with.
   */
  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
    parent::__construct('Plugin/Block', $namespaces, $module_handler, 'Drupal\Core\Block\BlockPluginInterface', 'Drupal\Core\Block\Annotation\Block');

    $this->alterInfo('block');
    $this->setCacheBackend($cache_backend, 'block_plugins');
  }

  // 省略

}
?>

プラグイン作成に関連して特に注目すべきポイントはこのコンストラクタ __construct() の中身です。 3 行あるので順番に見ていきましょう。

1 行目

parent::__construct('Plugin/Block', $namespaces, $module_handler, 'Drupal\Core\Block\BlockPluginInterface', 'Drupal\Core\Block\Annotation\Block');

ここでは親クラスのメソッドを利用してプラグインファイルの配置場所やインタフェースを定義しています。 この記述によって、プラグインシステムがこのプラグインタイプのプラグインをファイルシステムの中から見つけ出せるようになります。

parent::__construct() の各引数の意味合いはおおよそ次のとおりです。

  • 'Plugin/Block': プラグインクラスを格納したファイルを置くべきサブディレクトリ。
  • $namespaces: 対象の名前空間。
  • $module_handler: モジュールハンドラサービス。
  • 'Drupal\Core\Block\BlockPluginInterface': プラグインクラスが実装すべきインタフェース(オプション)。
  • 'Drupal\Core\Block\Annotation\Block': プラグインを定義するプラグイン(オプション)。

つまり、ブロックの場合、プラグインは以下のルールに従って作る必要があるということになります。

  • ファイルの配置場所: MYMODULE/src/Plugin/Block/
  • クラスのインタフェース: Drupal\Core\Block\BlockPluginInterface
  • アノテーション仕様: Drupal\Core\Block\Annotation\Block

ここで parent::__construct() の中身に興味のある方は次のページをご覧になってみてください。

2 行目

続く 2 行目です。

$this->alterInfo('block');

ここでは alter フックが追加されています。 この記述により hook_block_alter() というフック関数が使えるようになります。

つまり、ブロックプラグインは hook_block_alter() という alter フック関数を書いて書き換えができるということです。

alter フックについては Drupal 7 以前とほぼ同じものです。 このあたりについてはすでに多くの説明がなされているかと思いますので、興味のある方は「 Drupal alter hook 」などで検索して調べてみてください。

3 行目

3 行目です。

$this->setCacheBackend($cache_backend, 'block_plugins');

これはキャッシュシステムをセットしているところです。 この記述によって、プラグインは毎回探索されるのではなくデータベーステーブルなどに格納されてキャッシュされることになります。

ここでは「このプラグインタイプにはキャッシュが利用されている」「キャッシュのキーは 'block_plugins' である」との解釈をすればよいでしょう。 ちなみにこの形で有効化されたプラグインのキャッシュはデフォルトではデータベーステーブルの cache_discovery に格納されます。

これでブロックプラグインタイプについてざっくり理解することができました。 次に、実際にブロックプラグイン(=カスタムブロック)をひとつ作成してみましょう。

ブロックプラグインの作成

作業の流れは次のとおりです。

  1. 下準備: モジュールの作成
  2. 本作業: プラグインクラスの作成
  3. 確認: 動作することの確認

1. 下準備: モジュールの作成

プラグインを格納するカスタムモジュールを作成します。

Drupal ルート以下にディレクトリ modules/custom/mymodule を作成し、その中に YAML ファイル mymodule.info.yml を設置します。

App-root/
  modules/
    custom/
      mymodule/
        mymodule.info.yml

mymodule.info.yml には次のとおり記述して保存します。

mymodule.info.yml:

type: module
name: My Module
core: 8.x

正しく作成 / 設置できれば Drupal がこれを mymodule モジュールとして認識してくれるはずです。 PHP コードは含まれないので当然ですが特に機能はありません。 続くステップに備えて、管理画面からモジュールを有効化しましょう。

2. 本作業: プラグインクラスの作成

続いて、カスタムブロックに相当するプラグインクラスを mymodule モジュール内に作成します。

上で見たとおり、ブロックの場合にプラグインを作成する際のルールは以下のとおりでした。

  • ファイルの配置場所: MYMODULE/src/Plugin/Block/
  • クラスのインタフェース: Drupal\Core\Block\BlockPluginInterface
  • 対応するアノテーション仕様: Drupal\Core\Block\Annotation\Block

このルールに従ってプラグインクラスを作成していきます。

HelloBlock クラスを格納するための HelloBlock.php ファイルを以下の場所に作成します。 ファイルの配置場所が少しでも異なるとうまくいかないので、必要なディレクトリもすべて作成しましょう。

App-root/
  modules/
    custom/
      mymodule/
        src/
          Plugin/
            Block/
              HelloBlock.php

作成したばかりの HelloBlock.php をエディタで開き、以下の内容を記述しましょう。

HelloBlock.php:

<?php

namespace Drupal\mymodule\Plugin\Block;

use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;

/**
 * Provides hello block.
 *
 * Shows "Hello @username" in the content.
 *
 * @Block(
 *   id = "mymodule_hello_block",
 *   admin_label = @Translation("My Module Hello Block"),
 *   category = @Translation("My Module"),
 * )
 */
class HelloBlock extends BlockBase implements ContainerFactoryPluginInterface {

  /**
   * Current user.
   *
   * @var Drupal\Core\Session\AccountInterface
   */
  protected $currentUser;

  /**
   * {@inheritdoc}
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, AccountInterface $current_user) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->currentUser = $current_user;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('current_user')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function build() {
    return [
      '#markup' => $this->t('Hello @username!', [
        '@username' => $this->currentUser->getDisplayName(),
      ]),
    ];
  }

}

?>

ファイル末尾の PHP の閉じタグは本記事でのシンタックスハイライトのために入れているものなので、実際には削除してください。

現在のユーザーの名前を取得するためにサービスコンテナと current_user サービスを利用するなどしていますが、それらはここでは本質ではないので説明は割愛します。

ポイントは BlockBase クラスを継承しているところとコメント内の @Block アノテーションです。 BlockBase を利用する形で Drupal\Core\Block\BlockPluginInterface インタフェースを実装し、 Drupal\Core\Block\Annotation\Block アノテーションを使ってクラスの説明を行っています。

BlockPluginInterface の具体的な中身については実際のインタフェースの中身を確認する必要があります。

また、 Block アノテーションの仕様についてもクラスの実際の中身を確認するとわかります。

<?php

namespace Drupal\Core\Block\Annotation;

use Drupal\Component\Annotation\Plugin;

/**
 * Defines a Block annotation object.
 *
 * @ingroup block_api
 *
 * @Annotation
 */
class Block extends Plugin {

  /**
   * The plugin ID.
   *
   * @var string
   */
  public $id;

  /**
   * The administrative label of the block.
   *
   * @var \Drupal\Core\Annotation\Translation
   *
   * @ingroup plugin_translatable
   */
  public $admin_label = '';

  /**
   * The category in the admin UI where the block will be listed.
   *
   * @var \Drupal\Core\Annotation\Translation
   *
   * @ingroup plugin_translatable
   */
  public $category = '';

}

?>

上のコードを読むと、 Block アノテーションでセットすべきプロパティは id admin_label category の 3 つだということがわかりますね。

これで、以下の 3 つのポイントを押さえたプラグインクラスを作成することができました。

  • ファイルの配置場所: MYMODULE/src/Plugin/Block/
  • クラスのインタフェース: Drupal\Core\Block\BlockPluginInterface
  • 対応するアノテーション仕様: Drupal\Core\Block\Annotation\Block

これでプラグインの作成は完了です。 ファイルの配置場所やコードにまちがいがなければ、あとは実際に作られたブロックを確認するのみです。

3. 確認: 動作することの確認

実際に mymodule_hello_block ブロックがサイトで利用できることを確認しましょう。

上で見たとおりブロックのプラグインにはキャッシュが使われているので、新たに追加したプラグインを認識させるためにはキャッシュのクリア(キャッシュのリビルド)が必要です。 管理画面からキャッシュをクリア(キャッシュをリビルド)しましょう。

コードが問題がなく書けていればブロックレイアウト画面から「 My Module Hello Block 」というブロックが追加できるようになっているはずです。

実際にブロックを配置するとユーザー画面にログイン中のユーザーの名前を使ったブロックが表示されることが確認できます。 ログインしているユーザーの名前が Hayato の場合は「 Hello Hayato! 」という表示がされるはずです。

今回は画面キャプチャについては割愛します。

プラグインの作り方については以上です。 いかがだったでしょうか。

さいごに

今回は Drupal 8 のプラグインの概念と作り方について見てみました。

プラグインは「プラグインタイプ」というグループに分けられていて、プラグインを作るには次の 3 つの概念を押さえる必要があることを確認しました。

  • プラグインマネージャー
  • プラグインアノテーション
  • プラグインインタフェース

ひとつ注意点があります。途中でもちょこちょこお断りを入れていましたが、 Drupal 8 のプラグイン実装方法は今回ご紹介した方法だけではありません。 今回の方法は「 アノテーション付きクラスでプラグインを実装するタイプのプラグインタイプ 」にのみあてはまるものです。 プラグインタイプには他にも yaml ファイル、フック関数を使ったものなどもあるので、実際に Drupal 8 上で実装を始める際には「基本の考え方は同じだけれど実装方法はプラグインタイプによってちがう」という点にご注意いただければと思います。

参考

プラグインについて理解を深めたい方には以下リンク先が参考になるかと思います。

ありがたいことに英語の方ではとても詳しく丁寧に解説されているページや動画があります。 最新のバージョンにはあてはまらないところもあるため参考にされる際にはご注意ください。

概念をひととおり理解した後に理解を深めるには Examples for Developers プロジェクトが役立ちます。