Drupal 8 API: plugins

Pre-requisites

Plugins are discoverable code, classified by "type" across all modules

A description of what plugins mean in Drupal can sound so vague that it's hard to explain what's special about them; for example:

plugins are reusable and interchangeable bits of code (classes), grouped by "plugin type" independent of what module they might be in, and made discoverable to Drupal by some means so that they can be retrieved simply by a combination of type and ID.

We can improve our understanding of what makes plugins useful, by referring to previous tutorials:

  • We've already discussed how naming conventions let Drupal discover modules ("look for files called modulename/modulename.yml") and let PHP discover classes ("look for class name(space)s that match file paths"). So we know that following such standards make autodiscovery possible.
  • We've even already built a plugin: the custom block in the previous tutorial could be more accurately described as "a single plugin, of plugin type 'block.'" While that code didn't feel very "reusable" in any kind of "library of code" sense, we can imagine situations in which a block is placed more than once on a page, and each instance has different configuration.The separate instances in this situation could again be described as "derivatives of a single plugin, of plugin type 'block'".

Blocks are a classic use of the plugin architecture: blocks are plugins precisely because a site administrator will want to select any one of many blocks (and any one of potentially many instances, or derivatives, of a block), and have it always "behave" in similar ways; blocks need to be assignable to regions, and weighted up and down, to share containing markup etc. In effect, all block plugins share "blockish" behaviour, and that "formalized equivalent of a PHP interface" the whole point of a plugin type. But lots of other things in core are plugins too, for the same reason: text formats, field formatters, email backends, image toolkits....

Plugin types provided by core are such that, if you want to write a plugin of an existing type, you need to:

  • Follow the relevant naming and folder conventions, usually: put the code in the subfolder src/Plugin/[PluginType]/ (or sometimes a sub-subfolder of that); and have the class in the file declare the namespace Drupal\[modulename]\Plugin\[PluginType].
  • Extend the class, or implement the interface, that all plugins of a given type require.
  • Add an annotation comment, as discussed previously, to the top of the class.

The annotation is where you register as a minimum the plugin's ID, but can also register a (translatable) label for the plugin etc. It's feasible to write a custom method of plugin discovery, which doesn't use annotations, or could demand a completely different file location convention: in practice, though, unless you've got a good reason not to, you should follow core's conventions for discovery.

(By the way, when you're developing plugins, if you need to find out what existing plugins and types are available, the third-party Plugin module can really help.)

Establishing your own plugin type

As we've already created plugins of type "block", let's consider creating a custom plugin type, and then creating a plugin of that type. Let's try to think beyond generic Drupal or CMS concepts, to imagine just what we could use the plugin architecture to accomplish.

What if we could create a Cat plugin?

Four little white paws

Wouldn't that be amazing? Of course, a Cat plugin would be of plugin type Animal. This plugin type would constitute a whole library of different animals: third-party modules could even extend it with their own Horse and Badger plugins.

To define the Animal plugin type, we need the following:

  1. A common interface (more flexible than a parent class) that all plugins of that type promise to implement.
  2. An annotation class, to convert the annotation text on a plugin's class-level comment into a separate object.
  3. A plugin manager class, implementing the PluginManagerInterface and defining the plugin discovery rules.
  4. An entry in the module's services.yml, to register the plugin manager with Drupal as a service that can be used elsewhere.

We discussed services previously, in the context of dependency injection. Here the service is the main point of contact between our custom code—plugin type, annotation class, discovery method—and the rest of Drupal.

1. Common interface for all plugins of the new type

Animals can do lots of things, obviously, but let's imagine two behaviours we care about when it comes to an Animal plugin:

  1. Is the Animal capable of sustaining its own body heat?
  2. What does the Animal's call sound like?

The first method confers different behaviour on the Cat plugin and Frog plugin, for example.

Here's an interface which defines the required behaviour common to all plugins of Animal plugin type; save it in the d8api module as src/Plugin/Animal/AnimalInterface.php:

<?php
 
namespace Drupal\d8api\Plugin\Animal;
 
/**
 * Define interface common to all animals.
 */
interface AnimalInterface {
 
  /**
   * Is the animal capable of sustaining its body temperature?
   *
   * @return bool
   *   Response to the question.
   */
  public function isHomeothermic();
 
  /**
   * The animal's call.
   *
   * @return string
   *   Animal's response.
   */
  public function retrieveCall();
 
}

By introducing two methods, we'll show below how you can both implement interfaces and also inherit from abstract classes, in building your first plugin.

2. The wrapper class for annotations on plugins of the new type

When plugin discovery finds an annotation, it attempts to parse it and then create an annotation object, which as discussed before is an extension of the core Plugin annotation class plus properties. Save the following in the d8api module as src/Annotation/Animal.php:

<?php
 
namespace Drupal\d8api\Annotation;
 
use Drupal\Component\Annotation\Plugin;
 
/**
 * Defines an Animal annotation object.
 *
 * @Annotation
 */
class Animal extends Plugin {
 
  /**
   * The plugin ID.
   *
   * @var string
   */
  public $id;
 
  /**
   * The plugin name.
   *
   * @var string
   */
  public $name;
 
}

3. Plugin manager to discover plugins of the new type

We're going to follow Drupal core's method of discovering plugins. This means we very simply inherit from DefaultPluginManager, and merely specify:

  • The path pattern we expect plugin files to be found on, relative to src/ in the module.
  • The common interface that all plugins of type Animal must implement.
  • The annotation template to be used to parse the plugin's annotation comment.

This is all done in the plugin manager's constructor, as you can see; save this in the d8api module as src/Plugin/Animal/AnimalManager.php:

<?php
 
namespace Drupal\d8api\Plugin\Animal;
 
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
 
/**
 * Manages discovery and instantiation of animal plugins.
 */
class AnimalManager extends DefaultPluginManager {
 
  /**
   * {@inheritDoc}
   */
  public function __construct(
    \Traversable $namespaces,
    CacheBackendInterface $cache_backend,
    ModuleHandlerInterface $module_handler
  ) {
    parent::__construct(
      'Plugin/Animal',
      $namespaces,
      $module_handler,
      'Drupal\d8api\Plugin\Animal\AnimalInterface',
      'Drupal\d8api\Annotation\Animal'
    );
  }
 
}

4. Registration of the plugin manager with Drupal's services

Finally, we need to tell Drupal where to find the plugin manager (which will then tell it where to find plugins) by registering it as a service; add the following entry to d8api.services.yml in the top-level directory of the d8api module:

services:
  plugin.manager.d8api:
    class: 'Drupal\d8api\Plugin\Animal\AnimalManager'
    parent: default_plugin_manager

We will then use dependency injection below, to provide our custom code with this plugin manager.

Creating a discoverable plugin of the new type

Creating the new plugin type was the hard part; creating a plugin of that type should be easier. But we're going to introduce a slight complication:

  1. Create an abstract class AbstractMammal, that declares the plugin type interface implements AnimalInterface, but only provides one of the two methods. This could then be inherited by lots of other plugins
  2. Create a concrete class Cat extends AbstractMammal, that provides the other method unique to a cat: the retrieveCall().

This makes our test code more similar to a real-world example, where the key criterion for a plugin to be recognized—that it's a concrete class satisfying the relevant interface—can sometimes be obscured by layers of class inheritance; yet plugin discovery can still work with this less simple case.

Here are the two classes; src/Plugin/Animal/AbstractMammal.php (which could be a trait):

<?php
 
namespace Drupal\d8api\Plugin\Animal;
 
/**
 * Define the abstract mammal class for extension.
 */
abstract class AbstractMammal implements AnimalInterface {
 
  /**
   * {@inheritDoc}
   */
  public function isHomeothermic() {
    return TRUE;
  }
 
}

and src/Plugin/Animal/Cat.php:

<?php
 
namespace Drupal\d8api\Plugin\Animal;
 
use Drupal\d8api\Annotation\Animal;
 
/**
 * Define a concrete class for a cat.
 *
 * @Animal(
 *   id = "animal_cat",
 *   name = @Translation("Cat")
 * )
 */
class Cat extends AbstractMammal {
 
  /**
   * {@inheritDoc}
   */
  public function retrieveCall() {
    return "Meow";
  }
 
}

The only point to note about Cat is the annotation comment we referred to earlier, with the textual keys (id = "animal_cat" etc.) matching the object properties ($id etc.) You should also use the annotation class, even though it's not explicitly used by the PHP code, just referenced in the comment. Core annotation class discovery should work anyway, but this permits automatic code documentation systems to work out what class the @Animal() annotation refers to.

You can see that, once we've put all the architecture in place, the actual plugin file is very simple; the complexity of the discovery process, annotation comment parsing etc. is all elsewhere in code that need not be repeated. It would be very quick to write those Horse and Badger plugins, and not much more work to write an AbstractAmphibian class to support a Frog! The result, furthermore, would be easy to understand and maintain.

Optional improvements

All of the above code should work (see later for a demonstration.) But here are two optional improvements you could implement.

1. Performance: clearing out the Plugin subfolder

So far, we've put four files in the src/Plugin/Animal/ subfolder of the d8api module:

  • AbstractMammal.php
  • AnimalInterface.php
  • AnimalManager.php
  • Cat.php

Generally speaking, it's good design to put files together, related by "domain knowledge" rather than simply by software-developer reasons. However, you should note that standard plugin discovery checks every file in the folder specified. This means that, when caches are rebuilt and plugins discovered, all files in every module's src/Plugin/Animal/ subfolder will be parsed and checked to see if they satisfy the interface and have the correct annotation. It's not a huge performance hit, and only happens occasionally, but it will slow down cache rebuilding.

You should therefore avoid putting any files in a plugin subfolder, that don't have to be there. In this case, you could move all except Cat.php into some other folder, outside of src/Plugin/. But this is optional: just make sure you update the namespaces on any references to files you move around.

2. Reporting: expose your plugin to the Plugin module

Above we mention the Plugin module, a contributed third-party module that provides documentation for all plugins available to Drupal. You can extend the code in this tutorial to expose the Animal plugin type to the Plugin module's UI.

However, there's a small catch. Every plugin that appears in the UI must implement the Plugin module's PluginDefinitionDecoratorInterface. It so happens that the Plugin module comes bundled with implementations for all plugins in core, but that's as far as it goes.

Because these tutorials focus on core APIs, this one won't cover development against contributed modules: you can consider this as an exercise for you to try out in your own time!

Modify the existing custom block to get a Cat

Finally, for testing purposes, let's write some code that makes use of our new Animal plugin type by trying to find the Cat plugin through its ID: the annotation property id = "animal_cat". The simplest change to make is to modify our existing TutorialBlock to do the following:

  1. Retrieve the plugin manager for our new plugin type, through dependency injection as discussed previously.
  2. Later, when build()ing the block content, ask the plugin manager for animal_cat and return the resulting plugin's retrieveCall() text.

Below we show only (a) the constructor and create() additions and (b) the changes to build(), required in TutorialBlock.php to implement this:

<?php
 
/* (unchanged) */
use Drupal\Component\Plugin\PluginManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 
class TutorialBlock extends BlockBase implements ContainerFactoryPluginInterface {
 
  /**
   * @var PluginManagerInterface
   */
  protected $pluginManager;
 
  /**
   * Standard block constructor, but with addition of a plugin manager.
   *
   * {@inheritDoc}
   */
  public function __construct(
    array $configuration, $plugin_id, $plugin_definition,
    PluginManagerInterface $pluginManager
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->pluginManager = $pluginManager;
  }
 
  /**
   * {@inheritDoc}
   */
  public static function create(
    ContainerInterface $container,
    array $configuration, $plugin_id, $plugin_definition
  ) {
    return new static(
      $configuration, $plugin_id, $plugin_definition,
      $container->get('plugin.manager.d8api')
    );
  }
 
  /* (unchanged, except:) */
 
  public function build() {
    $plugin = $this->pluginManager->createInstance('animal_cat');
    $markup = $this->t('Cat goes @call', ['@call' => $plugin->retrieveCall()]);
    return [
      '#type' => '#markup',
      '#markup' => $markup
    ];
  }
 
}

You should leave the rest of the code unchanged.

What you should see

As previously discussed, always clear caches when registering new configuration like services, routes etc.

If you then navigate to the homepage you should see the changes to the block you've already enabled, as per the bottom left of this screenshot:

If so, then congratulations! you have implemented a custom plugin type, manager and plugin.

Further reading etc.

Comments

I think, it's worth mentioning that the following clauses should be added at the beginning of TutorialBlock.php :

use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Component\Plugin\PluginManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

I don't see it in the previous lesson about blocks (http://www.jpstacey.info/blog/2016-09-16/drupal-8-api-blocks) but it is neccessary to make the TutorialBlock works properly.

Thanks, kndr. You're quite right: I think I originally thought they'd come in from some other tutorial; but there's nothing in between the blocks tutorial and this tutorial that would add them. Edited now!

Add new comment