【Drupal】独自のプラグインタイプを実装してプラグインを追加する
はじめに
Drupalを開発に使用するメリットは、ユーザーインターフェイスが優れていてノーコードである程度のことができることが挙げられますが、それ以外にもコードの追加変更を容易にしたりコードの再利用を可能にしたりする「仕組み」が整っている点もDrupalのメリットの一つです。
Drupalでコードの追加変更や再利用を容易にする仕組みとして、Drupal8以降から導入されたプラグインというものがあります。Drupalでいうプラグインとは、よく似た仕様のロジックをテンプレート化して任意に追加変更できるようにした「最小単位のプログラムパーツ」です。
よく比較されるのがWordPressのプラグインですが、こちらはWordPressに機能を追加するモジュールのことで、Drupalでは「モジュール」と呼ばれています。
DrupalのプラグインはDrupal独自の仕様で、はじめてDrupalで開発する人がDrupalのコアロジックの機能を理解するまでにすこし時間がかかるかもしれませんが、一度理解してしまえばとても便利な機能です。
ちなみに、当サイトでもさまざまなところで独自プラグインタイプを採用しています。プラグインタイプを利用することで処理の効率化と再利用が容易になり、可読性と開発効率の向上により、メンテナンス性が格段によくなりました。
プラグインの例
Drupalではいろいろなところでプラグインが使用されています。それぞれプラグインタイプが定義、実装されていて、そのプラグインタイプのルールに基づいてプラグインが存在します。
Drupalコアで使用されているプラグインタイプをいくつか紹介します。
プラグインタイプ | プラグイン |
---|---|
ブロック | 検索フォーム |
言語スイッチャー | |
カスタムプラグイン | |
イメージエフェクト | スケール |
切り出し | |
リサイズ |
プラグインタイプとは
プラグインをコードの観点でみると、「 特定のインタフェースを実装したアノテーション付きの PHP クラス 」ということができます。
プラグインタイプとはある特定のプラグインが従うべきルールを定義したものということができます。
これを実装するにはプラグイン仕様に基づいて特定のインターフェイスとアノテーションのルールとそれを管理するPHPクラス(プラグインマネージャー)、そしてそれらにしたがってアノテーション付きのクラス(プラグイン)を作成する必要があります。
項目 | 説明 |
---|---|
プラグインマネージャー | 対象プラグインタイプのルールを定義する |
プラグインインタフェース | 対象プラグインタイプの各プラグインのインターフェイスを定義する |
プラグインアノテーション | プラグインのメタ情報を定義する |
ベースクラス(オプション) | 各プラグインのベースとするクラスを定義する |
このうち中心的な役割を担うのはプラグインマネージャーです。 プラグインマネージャーにプラグインアノテーションやプラグインインタフェースなどを定義します。Drupalコアはこの情報をもとにプラグインタイプを認識して、新しいプラグインタイプを利用できるようになります。
プラグインタイプの作り方
ここでは提供モジュールで公開されているサンプルコードを使用して説明します。
サンプルコードの提供モジュールについてはこちらの記事をご参照ください。
https://chatdeoshiete.com/node/1787
ChatGPTプラグインタイプを定義して、プラグインマネージャー、プラグインインターフェイス、アノテーション、プラグインベースクラスを実装しています。
サンプルコードは、サンドイッチのプラグインタイプです。
プラグインマネージャーの実装
プラグインマネージャーです。 プラグインマネージャーがプラグインアノテーションやプラグインインタフェースをプラグインシステムとのインターフェイスになるため、ここにはDrupalがプラグインタイプとして検出実行するために必要な情報が定義されています。
コンストラクタの設定
プラグインマネージャーの作成で特に重要なのはこのコンストラクタ __construct()
の中身です。
ここでは親クラスのメソッドを利用してプラグインファイルの配置場所やインタフェースを定義しています。 この記述によって、プラグインシステムがこのプラグインタイプのプラグインをファイルシステムの中から見つけ出せるようになります。
parent::__construct()
の各引数のは次のとおりです。
'Plugin/Block'
: プラグインクラスを格納したファイルを置くべきサブディレクトリ。$namespaces
: 対象の名前空間。$module_handler
: モジュールハンドラサービス。'Drupal\Core\Block\BlockPluginInterface'
: プラグインクラスが実装すべきインタフェース(オプション)。'Drupal\Core\Block\Annotation\Block'
: プラグインを定義するプラグイン(オプション)。
フックの追加
次に alter フックを追加します。 この記述により hook_sandwich_alter()
というフック関数が使えるようになります。
つまり、プラグインは hook_sandwich_alter()
という alter フック関数を書いて書き換えができるということです。
キャッシュの設定
プラグインは毎回探索されるのではなくデータベーステーブルなどに格納されてキャッシュされることになります。ここでは、キャッシュをするための情報を設定します。
キャッシュが利用されているため、プラグインを追加しただけでは反映しません。キャッシュをクリアすると追加したプラグインがシステムで認識できるようになり使用できます。
ちなみにこの形で有効化されたプラグインのキャッシュはデフォルトではデータベーステーブルのcache_discovery
に格納されます。
<?php
namespace Drupal\plugin_type_example;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\plugin_type_example\Annotation\Sandwich;
/**
* A plugin manager for sandwich plugins.
*
* The SandwichPluginManager class extends the DefaultPluginManager to provide
* a way to manage sandwich plugins. A plugin manager defines a new plugin type
* and how instances of any plugin of that type will be discovered, instantiated
* and more.
*
* Using the DefaultPluginManager as a starting point sets up our sandwich
* plugin type to use annotated discovery.
*
* The plugin manager is also declared as a service in
* plugin_type_example.services.yml so that it can be easily accessed and used
* anytime we need to work with sandwich plugins.
*/
class SandwichPluginManager extends DefaultPluginManager {
/**
* Creates the discovery 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) {
// We replace the $subdir parameter with our own value.
// This tells the plugin manager to look for Sandwich plugins in the
// 'src/Plugin/Sandwich' subdirectory of any enabled modules. This also
// serves to define the PSR-4 subnamespace in which sandwich plugins will
// live. Modules can put a plugin class in their own namespace such as
// Drupal\{module_name}\Plugin\Sandwich\MySandwichPlugin.
$subdir = 'Plugin/Sandwich';
// The name of the interface that plugins should adhere to. Drupal will
// enforce this as a requirement. If a plugin does not implement this
// interface, Drupal will throw an error.
$plugin_interface = SandwichInterface::class;
// The name of the annotation class that contains the plugin definition.
$plugin_definition_annotation_name = Sandwich::class;
parent::__construct($subdir, $namespaces, $module_handler, $plugin_interface, $plugin_definition_annotation_name);
// This allows the plugin definitions to be altered by an alter hook. The
// parameter defines the name of the hook, thus: hook_sandwich_info_alter().
// In this example, we implement this hook to change the plugin definitions:
// see plugin_type_example_sandwich_info_alter().
$this->alterInfo('sandwich_info');
// This sets the caching method for our plugin definitions. Plugin
// definitions are discovered by examining the $subdir defined above, for
// any classes with an $plugin_definition_annotation_name. The annotations
// are read, and then the resulting data is cached using the provided cache
// backend. For our Sandwich plugin type, we've specified the @cache.default
// service be used in the plugin_type_example.services.yml file. The second
// argument is a cache key prefix. Out of the box Drupal with the default
// cache backend setup will store our plugin definition in the cache_default
// table using the sandwich_info key. All that is implementation details
// however, all we care about it that caching for our plugin definition is
// taken care of by this call.
$this->setCacheBackend($cache_backend, 'sandwich_info', ['sandwich_info']);
}
}
プラグインインターフェイスの実装
ここではプラグインのインターフェイスを定義します。プラグインを追加作成するときはこのインターフェイスに定義されているメソッドを実装することになります。
<?php
namespace Drupal\plugin_type_example;
/**
* An interface for all Sandwich type plugins.
*
* When defining a new plugin type you need to define an interface that all
* plugins of the new type will implement. This ensures that consumers of the
* plugin type have a consistent way of accessing the plugin's functionality. It
* should include access to any public properties, and methods for accomplishing
* whatever business logic anyone accessing the plugin might want to use.
*
* For example, an image manipulation plugin might have a "process" method that
* takes a known input, probably an image file, and returns the processed
* version of the file.
*
* In our case we'll define methods for accessing the human readable description
* of a sandwich and the number of calories per serving. As well as a method for
* ordering a sandwich.
*/
interface SandwichInterface {
/**
* Provide a description of the sandwich.
*
* @return string
* A string description of the sandwich.
*/
public function description();
/**
* Provide the number of calories per serving for the sandwich.
*
* @return float
* The number of calories per serving.
*/
public function calories();
/**
* Place an order for a sandwich.
*
* This is just an example method on our plugin that we can call to get
* something back.
*
* @param array $extras
* An array of extra ingredients to include with this sandwich.
*
* @return string
* Description of the sandwich that was just ordered.
*/
public function order(array $extras);
}
アノテーションの実装
アノテーションにはプラグインのメタ情報を定義します。
具体的には、プラグインに記載するコメントに意味を持たせて、Drupalコアのプラグインシステムから参照して使用できるようにするものです。
<?php
namespace Drupal\plugin_type_example\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a Sandwich annotation object.
*
* Provides an example of how to define a new annotation type for use in
* defining a plugin type. Demonstrates documenting the various properties that
* can be used in annotations for plugins of this type.
*
* Note that the "@ Annotation" line below is required and should be the last
* line in the docblock. It's used for discovery of Annotation definitions.
*
* @see \Drupal\plugin_type_example\SandwichPluginManager
* @see plugin_api
*
* @Annotation
*/
class Sandwich extends Plugin {
/**
* A brief, human readable, description of the sandwich type.
*
* This property is designated as being translatable because it will appear
* in the user interface. This provides a hint to other developers that they
* should use the Translation() construct in their annotation when declaring
* this property.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $description;
/**
* The number of calories per serving of this sandwich type.
*
* This property is a float value, so we indicate that to other developers
* who are writing annotations for a Sandwich plugin.
*
* @var int
*/
public $calories;
}
ベースクラスの実装
ベースクラスは、プラグインが共通にメソッドを使用する場合に使用します。あらかじめベースクラスを作製して、プラグイン追加時にこのベースクラスを継承するようにします。
<?php
namespace Drupal\plugin_type_example;
use Drupal\Component\Plugin\PluginBase;
/**
* A base class to help developers implement their own sandwich plugins.
*
* This is a helper class which makes it easier for other developers to
* implement sandwich plugins in their own modules. In SandwichBase we provide
* some generic methods for handling tasks that are common to pretty much all
* sandwich plugins. Thereby reducing the amount of boilerplate code required to
* implement a sandwich plugin.
*
* In this case both the description and calories properties can be read from
* the @Sandwich annotation. In most cases it is probably fine to just use that
* value without any additional processing. However, if an individual plugin
* needed to provide special handling around either of these things it could
* just override the method in that class definition for that plugin.
*
* We intentionally declare our base class as abstract, and don't implement the
* order() method required by \Drupal\plugin_type_example\SandwichInterface.
* This way even if they are using our base class, developers will always be
* required to define an order() method for their custom sandwich type.
*
* @see \Drupal\plugin_type_example\Annotation\Sandwich
* @see \Drupal\plugin_type_example\SandwichInterface
*/
abstract class SandwichBase extends PluginBase implements SandwichInterface {
/**
* {@inheritdoc}
*/
public function description() {
// Retrieve the @description property from the annotation and return it.
return $this->pluginDefinition['description'];
}
/**
* {@inheritdoc}
*/
public function calories() {
// Retrieve the @calories property from the annotation and return it.
return (float) $this->pluginDefinition['calories'];
}
/**
* {@inheritdoc}
*/
abstract public function order(array $extras);
}
プラグインの作り方
ここでは上記で説明したプラグインタイプのルールにしたがって作成されたプラグインについて説明します。
以下は、提供モジュールのプラグインサンプルで、ハムサンドイッチとミートサンドイッチのプラグインです。
ハムサンドイッチのプラグイン
ハムサンドウッチのプラグインです。ベースプラグインを継承してプラグインを作成します。ハムサンドイッチの商品情報を返します。
<?php
namespace Drupal\plugin_type_example\Plugin\Sandwich;
use Drupal\plugin_type_example\SandwichBase;
/**
* Provides a ham sandwich.
*
* Because the plugin manager class for our plugins uses annotated class
* discovery, our meatball sandwich only needs to exist within the
* Plugin\Sandwich namespace, and provide a Sandwich annotation to be declared
* as a plugin. This is defined in
* \Drupal\plugin_type_example\SandwichPluginManager::__construct().
*
* The following is the plugin annotation. This is parsed by Doctrine to make
* the plugin definition. Any values defined here will be available in the
* plugin definition.
*
* This should be used for metadata that is specifically required to instantiate
* the plugin, or for example data that might be needed to display a list of all
* available plugins where the user selects one. This means many plugin
* annotations can be reduced to a plugin ID, a label and perhaps a description.
*
* @Sandwich(
* id = "ham_sandwich",
* description = @Translation("Ham, mustard, rocket, sun-dried tomatoes."),
* calories = 426
* )
*/
class ExampleHamSandwich extends SandwichBase {
/**
* Place an order for a sandwich.
*
* This is just an example method on our plugin that we can call to get
* something back.
*
* @param array $extras
* Array of extras to include with this order.
*
* @return string
* A description of the sandwich ordered.
*/
public function order(array $extras) {
$ingredients = ['ham, mustard', 'rocket', 'sun-dried tomatoes'];
$sandwich = array_merge($ingredients, $extras);
return 'You ordered an ' . implode(', ', $sandwich) . ' sandwich. Enjoy!';
}
}
ミートボールサンドイッチのプラグイン
ミートボールサンドウッチのプラグインです。ベースプラグインを継承してプラグインを作成します。ミートボールサンドウッチの商品情報を返します。
<?php
namespace Drupal\plugin_type_example\Plugin\Sandwich;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\plugin_type_example\SandwichBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a meatball sandwich.
*
* Because the plugin manager class for our plugins uses annotated class
* discovery, our meatball sandwich only needs to exist within the
* Plugin\Sandwich namespace, and provide a Sandwich annotation to be declared
* as a plugin. This is defined in
* \Drupal\plugin_type_example\SandwichPluginManager::__construct().
*
* The following is the plugin annotation. This is parsed by Doctrine to make
* the plugin definition. Any values defined here will be available in the
* plugin definition.
*
* This should be used for metadata that is specifically required to instantiate
* the plugin, or for example data that might be needed to display a list of all
* available plugins where the user selects one. This means many plugin
* annotations can be reduced to a plugin ID, a label and perhaps a description.
*
* @Sandwich(
* id = "meatball_sandwich",
* description = @Translation("Italian style meatballs drenched in irresistible marinara sauce, served on freshly baked bread."),
* calories = "1200"
* )
*/
class ExampleMeatballSandwich extends SandwichBase implements ContainerFactoryPluginInterface {
// Use Drupal\Core\StringTranslation\StringTranslationTrait to define
// $this->t() for string translations in our plugin.
use StringTranslationTrait;
/**
* The day the sandwich is ordered.
*
* Since meatball sandwiches have a special behavior on Sundays, and since we
* want to test that behavior on days other than Sunday, we have to store the
* day as a property so we can test it.
*
* This is the string representation of the day of the week you get from
* date('D').
*
* @var string
*/
protected $day;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
// This class needs to translate strings, so we need to inject the string
// translation service from the container. This means our plugin class has
// to implement ContainerFactoryPluginInterface. This requires that we make
// this create() method, and use it to inject services from the container.
// @see https://www.drupal.org/node/2012118
$sandwich = new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('string_translation')
);
return $sandwich;
}
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, TranslationInterface $translation) {
// Store the translation service.
$this->setStringTranslation($translation);
// Store the day so we can generate a special description on Sundays.
$this->day = date('D');
// Pass the other parameters up to the parent constructor.
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public function order(array $extras) {
$ingredients = ['meatballs', 'irresistible marinara sauce'];
$sandwich = array_merge($ingredients, $extras);
return 'You ordered an ' . implode(', ', $sandwich) . ' sandwich. Enjoy!';
}
/**
* {@inheritdoc}
*/
public function description() {
// We override the description() method in order to change the description
// text based on the date. On Sunday we only have day old bread.
if ($this->day == 'Sun') {
return $this->t("Italian style meatballs drenched in irresistible marinara sauce, served on day old bread.");
}
return parent::description();
}
}
さいごに
プラグインマネージャーはDrupal8で導入された機能です。それまで新しいロジックを追加するときは主にフックが使用されていました。BlockモジュールもDrupal7まではフック機能によりカスタムブロックの追加により実現していましたが、Drupal8以降はプラグインマネージャーに置き換わりました。
処理に共通性があり今後ロジックが追加される可能性があるものは、プラグインマネージャーを利用して実装することをおすすめします。
ここでは、静的にプラグインを追加するケースをご紹介しましたが、動的にプラグインを追加することも可能です。動的にプラグインを追加または更新する機能をプラグインデリバティブといいます。
プラグインデリバティブを利用すると、例えばユーザが入力した情報をもとにプラグインを追加したり、データベースに連携してプラグインを動的に追加または更新したりといったことができるようになります。
次回は、プラグインデリバティブの仕組みや実装の方法を説明したいと思います。
この記事に関するご質問やご意見などございましたらお問い合わせフォームからお気軽にご連絡ください。