
How to create a custom Drupal plugin
Over the recent weeks and months I have been creating custom Drupal plugins for various modules so now is the ideal time for me to write a "how to..." article on this topic.
Introduction
For a full explanation of the definition and purpose of Drupal plugins I recommend reading the Plugin API overview. For the purposes of this article I will quote the pithy sentence at the beginning of the documentation:
Plugins are small pieces of functionality that are swappable.
To create a custom plugin you will require the following five elements:
- Plugin Manager
- Interface
- Plugin Base
- Attribute
- A Plugin
The following sections will cover each of these elements.
Interface
(optional)
As in vanilla php object oriented classes, it is optional to include a plugin interface, but is strongly recommended if there are methods and properties that the plugin must implement.
In the example below all plugins of this type must carry out some form of processing in the method process(). For simplicity I am creating this in a custom module called my_module and the interface is called MyPluginInterface.
<?php
namespace Drupal\my_module;
interface MyPluginInterface {
public function process(array $data): array;
}
Plugin Base
(optional)
Similar to the interface, a plugin base is optional, but is recommended to define methods and properties that are common across all plugins on this type, as in vanilla php objected oriented class abstraction.
In the example below all plugins of this type will have a method called getId() that will return the value of the protected property $id.
<?php
namespace Drupal\my_module;
use Drupal\Component\Plugin\PluginBase;
abstract class MyPluginBase extends PluginBase implements MyPluginInterface {
protected $id;
public function getId(): int {
return $this->id;
}
}
Attribute-based
Since Drupal 10.2.0, plugins should be defined with PHP attributes. For more information read Attribute-based plugins on drupal.org.
What meta-data can be provided in the plugin's attributes are defined in the attribute class. The attribute class below defines that all plugins of type "MyPlugin" are will have three attributes:
- id
- (optional) label
- (optional) deriver class
<?php
namespace Drupal\my_module\Attribute;
use Drupal\Component\Plugin\Attribute\AttributeBase;
use Drupal\Core\StringTranslation\TranslatableMarkup;
#[\Attribute(\Attribute::TARGET_CLASS)]
class MyPlugin extends AttributeBase {
public function __construct(
public readonly string $id,
public readonly ?TranslatableMarkup $label,
public readonly ?string $deriver = NULL,
) {}
}
It is strongly recommended to include the DocBlock for the __construct() method, but it has been omitted here for the sake of brevity.
Plugin Manager
Service definition
The plugin manager must be registered as a service. Here is the entry in my_module.services.yml:
services:
plugin.manager.my_plugin:
class: Drupal\my_module\MyPluginPluginManager
parent: default_plugin_manager
It is best practice to begin the service id with plugin.manager. In this convoluted example, because the plugin is called MyPlugin, the plugin manager class is MyPluginPluginManager. It is recommended to extend DefaultPluginManager hence the parent is default_plugin_manager.
Plugin manager class
The code for the plugin manager class is:
namespace Drupal\metatag_link_preview;
use ...;
class MyPluginPluginManager extends DefaultPluginManager {
public function __construct(
\Traversable $namespaces,
CacheBackendInterface $cache_backend,
ModuleHandlerInterface $module_handler
) {
parent::__construct(
'Plugin/MyPlugin',
$namespaces,
$module_handler,
MyPluginInterface::class,
MyPlugin::class
);
$this->alterInfo('my_plugin_info');
$this->setCacheBackend($cache_backend, 'my_plugin_plugins');
}
}
(I have omitted the use statements for brevity, but don't forget to include them in your file)
The class extends DefaultPluginManager, as mentioned in the Service Definition section. The __construct() method is overwritten to define the specifics of this plugin type. The parent constructor requires:
- "Plugin/MyPlugin" - the subdirectory of the plugins.
- $namespaces - the parameter containing the namespace to look for plugin implementations.
- $module_handler - the parameter containing the module handler.
- MyPluginInterface::class - the name of the interface each plugin should implement (see Interface section above).
- MyPlugin::class - the name of the attribute that contains the plugin definition(see Attribute-based section above).
Other modules can alter the defined plugins using hook_my_plugin_info_alter() and plugin definitions are cached with the ID my_plugin_plugins.
For more information read Creating your own Plugin Manager on drupal.org.
An Example Plugin
An example instance of the plugin type I have just defined could look like this:
namespace Drupal\my_module\Plugin\MyPlugin;
#[MyPlugin(
id: "example_plugin",
label: new TranslatableMarkup("An example plugin")
)]
class ExamplePlugin extends MyPluginBase {
public function process(array $data): array {
// Do some processing.
}
}
(I have omitted the use statements for brevity, but don't forget to include them in your file).
This code would be found in a file called "ExamplePlugin.php". In the Plugin Manager section I defined the subdirectory for this plugin type to be "Plugin/MyPlugin", so the path for this file (within the module) would be "src/Plugin/MyPlugin/ExamplePlugin.php".
The attribute of the class refers back to that which I created in the Attribute-based section and includes an id of "example_plugin" and (optional) label of "An example plugin". Don't forget that the class name should match the file name, in this case ExamplePlugin.
The class extends the base class I defined earlier, MyPluginBase, which implements the Interface defined earlier, MyPluginInterface, so must include a process() method (the details of which I have omitted from this example).
Drush Generate
I am a big proponent of the Drush Generate command and it can be particularly helpful when creating a plugin manager. I recommend reading my article that walks through using this command. For now I suggest you review the documentation for drush generate plugin:manager.
Using a plugin
It is now easy to create an instance of the ExamplePlugin I have defined. First invoke the plugin manager service:
$manager = \Drupal::service('plugin.manager.my_plugin');
In many cases the service will be accessed by dependency injection, so the above line would be something like:
$manager = $this->MyPluginManager;
Then create the instance of the plugin:
$plugin = $manager->createInstance('example_plugin');
In this example the plugin type is used for processing a data array so would be used as follows:
$processed_data = $plugin->process($data);
For more uses of the plugin manager read Using the Plugin Manager on drupal.org.
My examples
Here are some real-world examples where I have used plugins:
Token Modifier
https://www.drupal.org/project/token_modifier
Token Modifier is a module I maintain, its purpose is to "provide meta tokens that allow you to modify the output of other tokens". Examples Token Modifier plugins include Upper Case (which converts the token to upper case) and Length (which restricts a token to a given maximum length.
Metatag Link Preview
https://github.com/jofitz81/metatag_link_preview
This module provides a rendering of the link preview of an entity (node / taxonomy term) when it is shared on social media. Each Link Preview plugin is a different platform, e.g. X, LinkedIn, Facebook.
Markdown Easy (merge request)
https://git.drupalcode.org/project/markdown_easy/-/merge_requests/28/diffs
The Markdown Easy module uses a couple of converters to display markdown as html. I proposed the feature of creating a plugin for each converter. Not only does this simplify the addition of further converters, but it makes them available for use beyond the scope of the module.