In December I gave a talk at the SCDUG meeting on Drupal 8 plugins. This is based on that talk with a few improvements and additions from things I’ve learned since.
Drupal 8 provides many great improvements, but one I’ve been finding to be the most exciting is the easy ability to create my own plugins. They are actually a bit addictive, and it’s easy to start seeing projects as an excuse to create a new plugin manager. At first it can take a little bit of effort to get your head around the process, but I got great details from Unraveling the Drupal 8 Plugin System from Drupalize.me. But the problem with those, and all the examples I could find, was they were either too deep (the drupalize.me article should be considered required reading, but the example is too abstract for most people) or too shallow (the drupal.org Plugin API examples currently lack context about why I would want to do this in practice).
So my goal is to straddle that gap.
What’s a plugin?
Plugins are a design pattern that let you extend a module or service. Drupal 8 has at least four plugin discovery systems:
- Annotated ← I’m mostly talking about this one, its used for blocks, views, rules, Queue API, and more.
- YAML ← These are used for menus, routes, and services.
- Hook ← D7 carry over hook system, still used in many places.
- Discovery Decorators ← Wrap around other plugin systems to replace/improve on the classic info alter hooks from previous Drupal versions.
- Static ← These are used for test code, and I’m ignoring here.
Basic Commonly Used Plugins
As a Drupal 8 developer you probably use several plugins when building a site, particularly if there is any custom functionality required. When creating controllers you’ll need a route, and likely a menu item, which means you’ll use YAML files to define those aspects. For blocks, the queue api, or Rules you’ll create an annotated plugin. Theme functions are still commonly defined with hook_theme().
Why do I create new plugin?
Between core, and major contrib modules (like Rules and Search API), plugins options abound, but it’s not always obvious why you might want to create one for a more focused project. For large projects that may have a large number of related functions, poorly defined use cases, or use cases that are expected to expand over time Plugins provide three major advantages over other approaches:
- Plugins make it easy to easily keep classes small and focused.
- Plugins make it easy to extend a service.
- Plugins allow one module to easily extend the function of another.
Cyberwoven has a client who needed an internal training portal. They want to provide some basic gamification, particularly badges, and they wanted a more detailed tracking of user behavior than the core statistics module provides. The badges present a function that has many related, but different, use cases and is likely to grow over time. So I created a pluggable service that allows us to easily define a set of conditions for creating a new badge. Each plugin’s annotation lists the events it cares about, and when one of those events is fired the service runs the plugin’s test function. If the badge has been earned the plugin returns the information needed to generate that entity. All the event listening, entity generation, and error checking happens in the service, so each plugin is only focused on the details of the one badge. The event tracker is similar, with each plugin defining a filter for the major system events to determining what details should be recorded, and the service handling the logging of those details and reporting. As the client needs have evolved we’ve been able to add and modify the plugins to meet their needs.
The second place I’ve created a custom plugin system for was an internal project at Cyberwoven (it was actually the project I used to learn how to create custom plugins). Our testing server has always had a service to perform some basic operations on the server, like pulling repos via webhooks and similar tools to help developers. Setting up our development environments for D8 meant it was time for a new test server, and therefore new tools to manage it. From the start I wanted to create a tool that helped both developers and account managers. So I created a D8 site to manage the sites that provides various tools to perform task operations we all need (not only code handling, but also checking if security releases apply to any sites). All the various operations we want exposed through that site are created as plugins to our custom manager. Each task plugin is focused on just that one task. All the other various needed to track the sites, route requests to run a task, and render their responses are all handled in other places. It’s also now making it easy to add features that wrap around the plugins since the plugins all have a consistent interface (like adding new displays and running some tasks through cron).
Introducing Site Manager
The main module defines an entity for each site, a couple displays (like the listing page displayed above), and the annotated plugin base so we can add new features easily. Plugins can be enabled and disabled on a site-by-site basis, and can be run from the plugins dropbutton.
Once we had the basics in place (like getting the git and drush status’s via simple plugins) we started to add more powerful features to turn the tool into a dashboard. We added the module search block you see on the left as a separate module that searches all sites for a specific Drupal module (nice when security updates are released). The search module also provides a plugin for each site that lists the modules for just that site.
The last site in the screen shot has a reference to scanning the HTTPS certificate for a specific site. This is another feature we added recently as this tool increasingly starts to serve as a dashboard. The HTTPS Scanner module again provides both a batch job to scan all sites, and a task plugin that scans just one.
As of this writing, all told there are nine working task plugins across four modules (there are a couple more underway as well). Tasks can also have their results displayed in a block when you look at the details for a site.
Building your own manager
Hopefully by now your convinced that it’s worth having your own plugin manager at least sometimes. So the next question is how to build them.
Elements of a custom plugin
Plugins have several parts, most of which are small and simple:
- Annotation Plugin Definition
- Plugin Manager Service
- Plugin Interface
- Plugin Base
- Example Implementation
- Controller (optional)
The first step is to define the annotation for your plugin. This is a simple class that extends Drupal\Component\Annotation\Plugin to define the variables you want listed in the annotation comments of the plugin. This file goes in custom_module/src/Annotation, and as always the file name and class name match:
Plugin Manager Service
The plugin manager itself is a service, but the vast majority of its function comes from the parent classes. The convention is to place this file in custom_module/src/Plugin (as will the rest of the base definitions we’re about to provide). In this case we call it: TaskPluginManager.php.
The full version of this provides a few more improvements to this class (I overrode the getDefinitions() method to provide some filtering options to support disabling plugins), but those aren’t actually required for it to work well.
In short, this class just provides some general definitions to separate it from all the other annotated plugins.
And since the plugin manager is a service, we add it to our module’s service.yml file:
services: plugin.manager.task_plugin.processor: class: Drupal\sitemanager\Plugin\TaskPluginManager parent: default_plugin_manager
Next we define the plugin’s interface.
For this plugin I defined 4 critical public functions, three just provide basic information about a plugin, but fourth (run) will be the interesting one in a minute.
Finally we’re getting to something interesting (or at least something that requires code). The plugin base is an abstract class which all the actual plugins will extend. This goes in custom_module/src/Plugin/TaskPluginBase.php.
The things worth taking notice of here are the fact that I inject several services into the base class, so I have them for each plugin: translation, main sitemanager module configuration, and a wrapper on Symfony’s process service to make running drush and git commands easier. The three informational functions are fully defined here, but the run function stays abstract since it’s the function that actually justifies creating a plugin at all.
In theory I could stop right there. I’ve created a plugin manager and defined all the things someone else needs to be able to use it. But there are two technically optional parts that are useful if you like to work and play well with others: an example implementation and a controller for use or testing purposes.
We went through all the trouble to create the plugin manager, before you stop you should create at least one plugin to prove all the work we just did actually works.
This is a simple task to use drush to run cron for a site. The site entities know the location on the file system for Drupal root, and the simple task service handles the extra settings to control where the commands are run (and time outs and error trapping), so we can safely feed that to command and the location to get the response.
The method returns a render array, that can be used as part of a response.
The controller is truly 100% optional. There are two reasons you might want to create one: to actually run the plugin if that makes sense (it does for the site manager), and to provide for easy testing of a plugin.
For site manager I created the controller because I actually needed it to run the tasks from the dropbutton (its the link used by all the links on those buttons). But when I created the others for clients, I realized they can be very hard to test. They are buried inside several layers of complexity, making a test suite is a challenge to create correctly. And since are of the reason to use a plugin for a project with growing definitions, the test suite is often of limited use for rush additions. But by having a carefully built (and secured) controller and route, you can create an endpoint to use to test the plugin.
Because it’s optional, and well covered elsewhere (or heck just use Drupal console to generate it), I’m going to skip over most of the details of actually building the route, and linking that to the controller, and focus on the single function of running the task itself. To keep this short I’ve removed lots of extraneous details like security and error trapping – creating a tool like this isn’t for beginners so I’m assuming you know how to do those things.
The plugin manager service was injected into the controller (code not shown) so I just have it create an instance of the task requested. The controller calls the run function. If the task is successful the output (a render array) is used for the response, otherwise a simple message is send. This sends an AjaxResponse to work with Drupal’s standard JS handlers, but it could generate a full page just as easily.
Being able to create your own plugins easily is really nice. In Drupal 7 custom plugins where generally considered an advanced developer task. The improvements in the abstraction (and the fact that true plugin support is now in core not pulled from ctools or other critical modules), means that while its still not a beginner task it is something all Drupal developers should be learning.
But remember you don’t always need or want the complexity of your own plugins. I’ve found the idea a bit addictive, and I get unreasonably excited when I find a place plugins are used or that it makes sense for me to create a new plugin type. For custom work you can almost always do the same thing using extra functions on a service, controller, or event listener, so this is a judgement call in the end.