Updating the Search API to D8 – Part 4: Creating plugin types

Fri, 2014-08-01 15:15 -- drunken monkey

The new plugin system is another large and important change in Drupal 8. While you were pretty much on your own if you wanted to provide some kind of plugin system in Drupal 7 (which a lot of modules do – think Views, Rules, etc.) there is now a very sophisticated framework to easily implement plugins and to define your own plugin types in a clean, extensible and very flexible way. And the good news for everyone familiar (code-wise) with the Search API in Drupal 7: the system is very much in line with how Search API implemented plugins itself, so the switch is pretty easy for us.

What is a plugin?

There have already been a lot of great blog posts about the plugin system in Drupal 8, so I won't go into much detail here. Generally speaking, a plugin is a self-contained unit providing a certain, well-defined functionality. A plugin type is defined by a single, central system (which is usually also the only place where the plugins are used), but other modules can then provide their own plugins with an individual implementation of that functionality.

Probably not a very helpful explanation, it's not that easy to explain – but, to use an example, blocks are now plugins in Drupal 8. The Block module provides the "Block" plugin type, which defines how blocks can be displayed on a page and how they can be configured. The Block module also provides all the code utilizing those plugins, letting the user place blocks on the site, ensuring they are displayed when and where appropriate, etc. Other modules then provide implementations of that "Block" plugin type – i.e., individual plugins – that implement different kinds of blocks: the User module provides a block with the login form, the Language module the "Language switcher" block, the Book module a block displaying the current book's navigation, etc.
But there are literally dozens of other types of plugins already included in Drupal core, and contrib is bound to add countless more – four (currently) by the Search API, for example. Entity types, the various Views handlers (fields, filters, sorts, …) and plugins (display, row, …), field types, WYSIWYG editors – all of those are now just different types of plugins, all using one new, large, unified framework to provide their functionality.

Ingredients for a new plugin type

So, what do we need to add our own plugin type to that growing list? Well, basically just a plugin manager, which (as explained last time) is a special type of service – that's it. Your new plugin type becomes available by just implementing a single method. Of course, to make your plugin type more useful and easier to use for others, you should add a few more things: an interface describing what plugins of that type should be able to do, an abstract base class for implementing that interface (unless it doesn't make sense in your case) and (optionally) a dedicated annotation for your plugin type.

Let's go through those with the example of Search API processors.

The plugin manager

As mentioned earlier, the plugin manager is simply a special kind of service. There is a base class (DefaultPluginManager) which already takes care of 95% of what's needed, you literally only need to override the constructor. So, we save the following in search_api/src/Processor/ProcessorPluginManager.php:

<?php
class ProcessorPluginManager extends DefaultPluginManager {

  public function
__construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
   
parent::__construct('Plugin/search_api/processor', $namespaces, $module_handler, 'Drupal\search_api\Annotation\SearchApiProcessor');
   
$this->setCacheBackend($cache_backend, 'search_api_processors');
   
$this->alterInfo('search_api_processor_info');
  }

}
?>

(To save space, I skip the namespace, imports and comments for all examples.)
This can be used as kind of a template, the base class will then take care of everything that's necessary to make the plugin system work. Here's what each of the lines do:

  • The __construct() method signature contains the service's dependencies, which will be injected when the plugin manager is constructed. $namespaces basically contains all root folders for sub-namespaces – i.e., every module's src/ and/or lib/ directory, plus a few more. The others are the services that handle caching and module interaction (e.g., invoking hooks).
  • The parent constructor sets the plugin manager up for this specific plugin type. The two variables are of course just passed on as-is, the other two parameters represent the following:
    • Plugin/search_api/processor is the sub-dir (and therefore sub-namespace) in which plugins of this type have to be located for automatic detection. That means, e.g., if the Views module would want to provide a Search API processor, it would have to be in namespace \Drupal\views\Plugin\search_api\processor, otherwise it wouldn't be found.
      The general standard for this parameter is either Plugin/[module]/[component] (like here) or Plugin/[ModuleComponent] (so, Plugin/SearchApiProcessor in this case). I think the former is generally preferred if your module defines more than one plugin type, while the latter can be cleaner/more practical if your module only defines a single one – I'm not completely sure myself, though. They probably shouldn't be mixed in a single module, in any case.
      (As a side note, I cheated here a bit, we currently use the sub-dir Plugin/SearchApi/Processor for processors. As I'm pretty sure that's wrong, though, we'll change that shortly.)
    • \Drupal\search_api\Annotation\SearchApiProcessor is the fully qualified class name of the annotation that will be used for defining plugins of your type. (See here if you have no idea what I'm talking about.) You can just skip this parameter, in which case it will default to \Drupal\Component\Annotation\Plugin – i.e., the generic @Plugin annotation. Core's validation constraints, e.g., use this instead of an individual annotation. Don't ask me what the decision process is here, but I'm pretty sure it's generally encouraged to use a dedicated annotation for your plugin types.
  • The second line just sets up caching for the plugin type, to avoid having to re-discover the available plugins each time they are required. search_api_processors here is the cache key that will be used to store the cached entries. You can also pass an array of cache tags as an optional third parameter to setCacheBackend(), which can be useful in special scenarios (i.e., when the cached plugin data should always be cleared alongside other cached data) but usually shouldn't be done.
  • The last line just determines that modules can alter the available processor definitions using hook_search_api_processor_info_alter(array &$definitions). Make sure to also document that hook in your module's MODULE.api.php file!

And that's already it for the class, all very straight-forward. Now, the only thing that's missing for the service is its entry in the search_api.services.yml file:

services:
  plugin.manager.search_api.processor:
    class: Drupal\search_api\Processor\ProcessorPluginManager
    parent: default_plugin_manager

The service ID here is generally plugin.manager.[module].[component]. Then we simply specify the class we just created and use the handy parent shortcut to copy over the same constructor dependencies that (usually) all plugin managers have. This is no higher magic but just references (and partly copies over) the following definition from core.services.yml:

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

You'll recognize the arguments as exactly those we used in our plugin manager's constructor.

The @SearchApiProcessor annotation

Since we wanted a dedicated annotation instead of the generic @Plugin, we still have to create the SearchApiProcessor annotation class to get the plugin type basically working. This is also very easy, we just put the following into search_api/src/Annotation/SearchApiProcessor.php:

<?php
class SearchApiProcessor extends Plugin {

  public
$id;

  public
$label;

  public
$description;

}
?>

That's all – you just define your plugin annotation's properties and inherit from Plugin, that base class will take care of everything else. The properties aren't even necessary, they are just there to document for other developers what properties the plugin type expects in plugin definitions. (And, though not used in this case, to set defaults for optional properties.) The base class doesn't care about that, it will just save all the properties it encounters in the definition, regardless of whether they are defined or not. (Note, though, that the id property is "magic" – if you want to use that for anything other than the plugin ID (and have the plugin ID in a different property), then you'll have to override getId() in your annotation class.)
The only thing you do have to take care of is that annotation autloading will only work if you put the annotation into the \Drupal\[module]\Annotation namespace.

The plugin interface and base class

With the above done, you already have a fully functional new plugin type: other modules can define their own Search API processors and we can easily get all defined processors with the following bit of code:

<?php
$processor_plugin_manager
= \Drupal::service('plugin.manager.search_api.processor');
$processor_definitions = $processor_plugin_manager->getDefinitions();
foreach (
$processor_definitions as $processor_id => $processor_definition) {
 
$processors[$processor_id] = $processor_plugin_manager->createInstance($processor_id, $processor_settings[$processor_id]);
}
?>

However, while this is a complete working example as far as Drupal's plugin framework is concerned, it is of course not really practical since we don't specify anywhere what we expect from processor plugins, so modules that want to provide their own processors don't know which methods to provide and how they should behave (without combing the whole module code of the Search API for any calls to processor plugins). So, it is more or less required to also define an interface for plugins of your new type that implementing modules would have to use.

The specific methods in the interface of course differ from case to case, but there are a lot of interfaces (and corrsponding base classes or traits) provided in Drupal Core for functionality that is often required for plugins. E.g., PluginInspectionInterface lets you retrieve a plugin's ID and definition, DerivativeInspectionInterface helps dealing with plugin derivatives (look out for an upcoming blog post about those) and PluginBase is a great base class for plugins which implements both of these interfaces and additionally provides a few other handy methods for child classes (first and foremost, a t() method for doing dependency-injected translation). PluginFormInterface provides methods for plugins that should have a configuration form, usually used in conjunction with ConfigurablePluginInterface, which represents plugins with configuration. And ContainerFactoryPluginInterface, as the last one used in the Search API, provides a static create() method for easily implementing proper dependency injection. There are more, though, so take a look through the \Drupal\Component\Plugin and \Drupal\Core\Plugin namespaces before adding custom methods to your plugin interface.

The whole interface then looks like this (conceptually):

<?php
interface ProcessorInterface extends PluginInspectionInterface, DerivativeInspectionInterface, DrupalConfigurablePluginInterface, PluginFormInterface, ContainerFactoryPluginInterface {

 
// Put plugin type-specific methods here.

}
?>

This interface is usually put into the same directory (and, therefore, namespace) as the plugin manager (since there is nothing else that really links the plugin manager to the interface), as is a default base class for the plugin type that implements the interface and helps modules avoid boilerplate code when providing their own plugins:

<?php
abstract class ProcessorPluginBase extends PluginBase implements ProcessorInterface {

 
// Here, provide default implementations for all methods of the interface (for which it makes sense).

}
?>

And that's all, now you've completely defined a new plugin type for your module. Modules can now provide plugins of that type like this:

<?php
/**
 * @SearchApiProcessor(
 *   id = "example_some_processor",
 *   label = @Translation("Some processor"),
 *   description = @Translation("Description of the processor.")
 * )
 */
class SomeProcessor extends ProcessorPluginBase {

 
// Plugin code.

}
?>

Other posts in this series

Image credit: findicons.com

Comments

Submitted by Moshe Weitzman (not verified) on

Just want to thank you for this blog series. These are exactly the topics and level of detail that experienced Drupal devs need to get up to speed on D8. I'm finding it very helpful, and surely others are too.

Submitted by drunken monkey on

Always great to hear such feedback. I'm glad if this helps others start more quickly.

Add new comment

To prevent spam, submitting full URLs in comments is not allowed. Please omit the "http[s]://" portion of the URL and I will restore the complete URL on review.

Filtered HTML

  • Lines and paragraphs break automatically.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <em> <strong> <q> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd> <sup> <sub> <p> <br>
  • To post pieces of code, surround them with <code>...</code> tags. For PHP code, you can use <?php ... ?>, which will also colour it based on syntax.

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.