Drupal 8 プラグイン その 3: カスタムプラグインタイプの作り方

前回前々回に引き続き CMS 「 Drupal 」のバージョン 8.x で導入されたプラグインのお話です。

前々回は Drupal 8 におけるプラグイン活用の基本となるプラグインの概念とその作り方を、前回は応用的なプラグインデリバティブの概念と作り方を見てきました。 ここまで見てきたプラグインやプラグインデリバティブはプラグインタイプあってこそのものですが、そうなると、そもそもプラグインタイプというのはどのように定義され、どのように作るものなのでしょうか。

今回は「 Drupal 8 のカスタムプラグインタイプの作り方 」について見ていきたいと思います。

想定読者

今回の想定読者は Drupal 8 のプラグインの知識をお持ちの方 です。 プラグインシステムについて理解を深めたい、プラグインタイプ提供側のロジックも押さえておきたいという方を対象にしています。

プラグインタイプの作り方

では早速、プラグインタイプの作り方を見ていきましょう。 今回はサンプルとして次の形で利用できる Calculator プラグインタイプを作ってみることにします。

<?php

$calculator_manager = \Drupal::service('plugin.manager.calculator');
$calculator = $calculator_manager->createInstance('sample_calculator');
$result = $calculator->calculate(10);  // => 70

?>

プラグインの説明のところで見たとおり、 Drupal 8 におけるプラグインタイプの実体は「 プラグインマネージャーと付随するいくつかのクラス 」です。 プラグインタイプを作るというのは「カスタムモジュールを作成して、プラグインマネージャーと付随するクラスを作成する」という作業になります。

作業の流れ

プラグインタイプ作成の流れは以下のとおりとなります。

  1. プラグインマネージャーを作成
  2. プラグインアノテーションクラスを作成
  3. プラグインインタフェースを作成
  4. プラグインマネージャーをサービスとして登録
  5. 確認

前提として、 mymodule というマシン名のカスタムモジュールが作られていることとします。

modules/custom/mymodule/mymodue.info.yml:

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

上の手順に沿って順番に作業を進めていきましょう。

1. プラグインマネージャーを作成

最初にすべきことはプラグインマネージャークラスの作成です。

プラグインマネージャークラスの作成にはいくつかのポイントがあります。 まず、プラグインマネージャークラスはインタフェース Drupal\Component\Plugin\PluginManagerInterface を実装する必要があります。 また、コアのプラグインタイプをいくつか見るかぎり、プラグインマネージャーの名前空間は Drupal\モジュール名\Plugin\PluginManager とすることがルールとなっているようです。

これらのポイントを押さえた上でコードを書いてみます。

mymodule/src/Plugin/PluginManager/CalculatorPluginManager.php:

<?php

namespace Drupal\mymodule\Plugin\PluginManager;

use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;

use Drupal\Core\Plugin\DefaultPluginManager;

/**
 * Manages calculator plugins.
 */
class CalculatorPluginManager extends DefaultPluginManager implements PluginManagerInterface {

  /**
   * Constructs a new 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/Calculator', $namespaces, $module_handler, 'Drupal\mymodule\Plugin\PluginManager\CalculatorInterface', 'Drupal\mymodule\Plugin\PluginManager\Calculator');

    $this->alterInfo('calculator');
    $this->setCacheBackend($cache_backend, 'calculator_plugins');
  }

}

?>

いくつかコードのポイントを説明します。

  • プラグインマネージャーとして CalculatorPluginManager クラスを定義している。
  • CalculatorPluginManager クラスはコアが提供するプラグインタイプのベースクラス DefaultPluginManager を継承している。
  • ベースクラス DefaultPluginManager はプラグインマネージャーに必要なロジックのほとんどを実装してあるので、カスタムプラグインに必要な最小限のパーツはコンストラクタ __construct() のみ。

コンストラクタ __construct() の各行はその 1 で見たとおりそれぞれ次のことを行っています。

  • 1 行目: Calculator プラグインの定義方法を定義
  • 2 行目: alter hook を追加
  • 3 行目: 探索したプラグインのキャッシュ機能を追加

この中で欠かせないのは 1 行目です。 「プラグイン探索対象のパス」「個々のプラグインが実装すべきインタフェース」「プラグインアノテーションを定義したクラス」の指定がすべてこの 1 行目だけで完結しており、プラグインタイプの定義はこれだけで十分です。 この 1 行目により、このプラグインタイプでは次のルールが定められました。

  • プラグインはモジュールディレクトリ内の src/Plugin/Calculator/ 以下に配置すること
  • 個々のプラグインはインタフェース Drupal\mymodule\Plugin\PluginManager\CalculatorInterface を実装すること
  • プラグインのメタ情報の指定はアノテーションで行い、アノテーションには Drupal\mymodule\Plugin\PluginManager\Calculator を使用すること

DefaultPluginManager クラスはアノテーションでメタ情報記述を行うタイプのプラグインタイプを定義したい場合にはほぼ間違いなく利用できるので、覚えておいて損はないかと思います。 コードを眺めると学ぶべきところがいろいろありますので、興味のある方はご覧になってみてください。

もしプラグインのメタ情報の記述にアノテーション以外の方法ーーフック関数や yaml などを使いたい場合は DefaultPluginManager ではなくもうひとつの上のベースクラス PluginManagerBase を使うと便利です。

プラグインマネージャーの作成はこれで完了です。

2. プラグインアノテーションクラスを作成

続いて、プラグインマネージャーの中で指定したアノテーションクラスを定義します。

アノテーションクラスは Drupal\Component\Annotation\Plugin クラスを継承して作ります。

mymodule/src/Plugin/PluginManager/Calculator.php:

<?php

namespace Drupal\mymodule\Plugin\PluginManager;

use Drupal\Component\Annotation\Plugin;

/**
 * Defines an calculator annotation object.
 *
 * @Annotation
 */
class Calculator extends Plugin {

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

  /**
   * The human-readable name.
   *
   * @ingroup plugin_translatable
   *
   * @var \Drupal\Core\Annotation\Translation
   */
  public $label;

  /**
   * A brief description.
   *
   * @ingroup plugin_translatable
   *
   * @var \Drupal\Core\Annotation\Translation (optional)
   */
  public $description = '';

}

?>

Plugin クラスを継承しさえすれば、やるべきことはシンプルです。 アノテーションで利用したいプロパティを可視性 public で定義しましょう。

コードを見れば一目瞭然ですが今回は 3 つのプロパティを定義しました。

  • id
  • label
  • description

プラグインアノテーションクラスの作成はこれにて完了です。

3. プラグインインタフェースを作成

さらに続いて、今回作成するプラグインタイプのプラグインが共通で実装すべきインタフェースを定義しましょう。

mymodule/src/Plugin/PluginManager/CalculatorInterface.php:

<?php

namespace Drupal\mymodule\Plugin\PluginManager;

/**
 * Provides an interface for Calculator plugin.
 */
interface CalculatorInterface {

  /**
   * Execute a special calculation.
   */
  public function calculate($value);

}

?>

ここはただの PHP インタフェースになるので、プラグインの目的に沿って自由に定義すれば OK です。 また、このインタフェースはできるかぎり定義した方がいいと思いますが、インタフェースを定義しない形で通すことも物理的に可能は可能です。

今回は calculator プラグインということで calculate() メソッドを必要とする形にしてみました。

プラグインインタフェースの定義はこれで完了です。

4. プラグインマネージャーをサービスとして登録

プラグインタイプの構成要素が一式準備できたので、最後にこのプラグインタイプをサービスに登録しましょう。

具体的には、プラグインタイプの核となるプラグインマネージャーのクラスをサービスとして登録すれば OK です。 サービスの登録には MYMODULE.sevices.yml を利用します。

mymodule/mymodule.services.yml:

services:
  plugin.manager.calculator:
    class: Drupal\mymodule\Plugin\PluginManager\CalculatorPluginManager
    arguments: ['@container.namespaces', '@cache.discovery', '@module_handler']

規約として、プラグインマネージャーのサービス名は plugin.manager.xxx とすることに決まっています。 名前の衝突に注意して、一意な名前をつけてあげましょう。

今回のサービス名は plugin.manager.calculator としました。

arguments には __construct() に渡すオブジェクトを正しく指定する必要があるので注意してください。 ちなみに @ で始まるものは core/core.services.yml で定義されている別のサービスです。

ちなみに、この引数のパターンを持つ abstract サービスが default_plugin_manager という名前で core/core.services.yml の中で登録されているので、それを parent として指定しても OK です。 次の yaml コードは上の yaml と同じ意味になります。

services:
  plugin.manager.calculator:
    class: Drupal\mymodule\Plugin\PluginManager\CalculatorPluginManager
    parent: default_plugin_manager

ちなみに default_plugin_manager の中身は次のとおりとなっています。

core/core.services.yml:

services:
  # ...
  default_plugin_manager:
    abstract: true
    arguments: ['@container.namespaces', '@cache.discovery', '@module_handler']

最後に、 mymodule モジュールを有効化してキャッシュをクリアしましょう。 ここまで来たらプラグインタイプの作成は完了です。

あとはこのプラグインタイプのプラグインを実際に作って動作確認してみましょう。

5. 確認

確認作業は次の 2 ステップで進めていきます。

  • 5a. 作成したプラグインマネージャーがサービスとして利用できることの確認
  • 5b. 作成したプラグインタイプのプラグインが登録でき利用できることの確認

5a. 作成したプラグインマネージャーがサービスとして利用できることの確認

コンソールなどを使って以下のコードでサービスを取得してみましょう。

<?php
$calculatorManager = \Drupal::service('plugin.manager.calculator');
?>

無事に読み込めたら Calculator プラグインマネージャーは無事サービスとして登録できています。

ここで対象サービスが正しく読み込めない場合はどこかが間違っています。 間違いが修正できるまでコードの修正とキャッシュのリビルドを繰り返しましょう。

5b. 作成したプラグインタイプのプラグインが登録でき利用できることの確認

最後にプラグインが登録できることを確認しましょう。

プラグインは多くの場合プラグインタイプを提供するモジュールとは別のモジュールで定義する形になりますが、ここでは動作確認したいだけなので、プラグインタイプを定義したモジュールと同じ mymodule モジュールを利用します。

mymodule/src/Plugin/Calculator/SampleCalculator.php:

<?php

namespace Drupal\mymodule\Plugin\Calculator;

use Drupal\mymodule\Plugin\PluginManager\CalculatorInterface;

/**
 * @Calculator(
 *   id = "sample_calculator",
 *   label = @Translation("Sample Calculator"),
 *   description = @Translation("A sample calculator."),
 * )
 */
class SampleCalculator implements CalculatorInterface {

  /**
   * {@inheritdoc}
   */
  public function calculate($value) {
    return $value * 7;
  }

}

?>

キャッシュをリビルドすると、いま作成した Calculator プラグインが利用できるようになっているはずです。 例えば、 Drush コンソールから以下のようなコードで確認してみましょう。

<?php

// プラグインマネージャーを取得
$calculator_manager = \Drupal::service('plugin.manager.calculator');

// sample_calculator のメタ情報を取得
$calculator_info = $calculatorManager->getDefinition('sample_calculator');
// => [
// "id" => "sample_calculator",
// "label" => ...
// "description" => ...
// "class" => "Drupal\mymodule\Plugin\Calculator\SampleCalculator",
// "provider" => "mymodule",
// ]

// sample_calculator をインスタンス化して利用
$calculator = $calculator_manager->createInstance('sample_calculator');
$result = $calculator->calculate(10);  
// => 70

?>

プラグインが無事に取得 / 利用できたら OK です。

以上です。

ちなみに余談ですが、今回行ったように DefaultPluginManager を継承してプラグインマネージャーを作成した場合、デフォルトでは Drupal\Component\Plugin\Factory\DefaultFactory というクラスがプラグインファクトリとなります。 このファクトリは、もしプラグインがインタフェース Drupal\Core\Plugin\ContainerFactoryPluginInterface を実装していれば 通常の new CLASS_NAME() ではなく、 CLASS_NAME::create() という形でクラスメソッド create() を使ってインスタンスを生成してくれます。

core/lib/Drupal/Core/Plugin/Factory/ContainerFactory.php:

<?php

namespace Drupal\Core\Plugin\Factory;

use Drupal\Component\Plugin\Factory\DefaultFactory;

/**
 * Plugin factory which passes a container to a create method.
 */
class ContainerFactory extends DefaultFactory {

  /**
   * {@inheritdoc}
   */
  public function createInstance($plugin_id, array $configuration = []) {
    $plugin_definition = $this->discovery->getDefinition($plugin_id);
    $plugin_class = static::getPluginClass($plugin_id, $plugin_definition, $this->interface);

    // If the plugin provides a factory method, pass the container to it.
    if (is_subclass_of($plugin_class, 'Drupal\Core\Plugin\ContainerFactoryPluginInterface')) {
      return $plugin_class::create(\Drupal::getContainer(), $configuration, $plugin_id, $plugin_definition);
    }

    // Otherwise, create the plugin directly.
    return new $plugin_class($configuration, $plugin_id, $plugin_definition);
  }

}

?>

この場合の第 1 引数は ContainerInjectionInterfacecreate() メソッドと同じインタフェース Symfony\Component\DependencyInjection\ContainerInterface を実装したオブジェクトなので、プラグインの中で何らかのサービスを利用したい場合には ContainerFactoryPluginInterface を実装してサービスコンテナを利用するとよいでしょう。

まとめ

今回は Drupal 8 のプラグインタイプの作り方を見てみました。

プラグインタイプの実体はモジュール内で定義されたプラグインマネージャークラスとそれに付随するいくつかのクラスであることを確認しました。 また、実際にサンプルとなるプラグインタイプを作成し、プラグインが無事プラグインシステムに認識されるかことを確認しました。

応用 / 発展はいろいろと考えられそうですが、 Drupal 8 のプラグインについてはこの 3 回でおおづかみに把握できたように思うので、 Drupal 8 プラグインシリーズはひとまずこれにて終了です。 また取り上げるとおもしろそうなものが見つかればシリーズで取り上げてみたいと思います。

参考

Drupal 8 のプラグインタイプの作成に関して参考になるページを以下のリンクに載せておきます。 より深いところを理解してみたい方はそれぞれリンク先をご覧になってみてください。

また、プラグインマネージャーについての基礎知識が得られれば、コアが提供する各種プラグインマネージャーのコードもおもしろく読むことができます。 日頃利用している各プラグインタイプがどのように定義され提供されているのかもし興味があればコアのプラグインのコードも覗いてみるとよいでしょう。