Skip to main content

Understanding Drupal 8's plugin system

Drupal 8 comes with a brand new plugin system. This article provides a high-level overview of both the annotation and hook discovery mechanisms by examining a real world implementation in the form of core's Field API Widgets.

by lee.rowlands /

Some background

Drupal 8 features a brand-new plugin system. This is akin to ctools plugins but with much more flexibility. For a great background see EclipseGC's presentation from MWDS or the handbook pages on Drupal.org.

As indicated in Kris's presentation and the handbook pages, there are four forms of discovery. Anyone who's built any form of Drupal module would be familiar with one of the discovery mechanisms - hook discovery. This is analagous to a hook_info style hook such as hook_field_info or hook_entity_info

The second method is static discovery, which is useful for things like testing where the plugins are declared as a static property on the plugin manager class.

The third method is PersistentVariableDiscovery which is essentially a wrapper around config based plugins such as cache backends, basically anything you might have defined in settings.php.

The fourth and perhaps most interesting mechanism is annotation based discovery. This allows Drupal to discover plugins based on comments in the plugin's docblock. This was committed as part of #1683644 Use Annotations for plugin discovery.

Why annotation based discovery is awesome

Firstly, annotation based discovery means the plugin metadata is defined with the actual plugin, in the same file, right there with the plugin class. Previously you may have had an info hook in your module where the plugin was defined and then the class itself lived in a separate file. Having them in the same place is obviously better DX.

Early versions of the plugin system included a getInfo() method on each class. This was analagous to test classes and returned name, description etc. about the plugin. However loading each file into memory in order to call the getInfo() method on the class has a memory overhead.

The next approach was to utilise Doctrine's annotation based discovery. However this too required loading each file into memory.

This is where chx came in and wrote a patch for Doctrine that utilises php's tokeniser. This allows loading each file in turn as text, instead of php, parsing the plugin information and registering the plugin. The advantage to loading the file in as text is that once the file has been read, the memory is released. Loading the file in as php would mean that allocated memory was required until the request was finalised.

Discovery Decorators

The plugin system also includes decorators. Basically a discovery decorator is a wrapper around a discovery class. The decorator wraps around a DiscoveryInterface to provide additional functionality. The best example of a decorator at use is the CacheDecorator. The CacheDecorator implements a caching layer on top of another DiscoveryInterface. Calls to the getDefinitions() method utilise caching, invoking the decorated interface when the cache isn't primed.

Putting plugins to use

As an example of how to use the plugin system, we'll examine the patch from Widgets as Plugins which converts hook_field_widget_info to use the plugin mechanism. We're using this as an example because it utilises both discovery methods. As this patch is a proof-of-concept it only includes code to migrate some of core's field api widgets to the new plugins system. Conversion of other widgets will occur in follow up issues. The widgets that have been converted use the annotation based discovery. The widgets that are still to be ported are handled by a Legacy plugin type which is essentially a backwards compatability plugin type that maps the old hooks to the new plugin methods. Hence this patch provides a good example of both methods.

Declaring your plugin types

In order to define a plugin type, you basically need to declare a plugin manager. A plugin manager is a class that extends from PluginManagerBase or rather Drupal\Component\Plugin\PluginManagerBase in PSR-0 talk. Basically a plugin manager consists of a discovery mechanism which handles discovery of defined plugins and a factory which handles instantiation of plugins. Looking at WidgetPluginManager (Drupal\field\Plugin\Type\Widget\WidgetPluginManager) it's constructor looks like so:

  
/**
 * Constructs a WidgetPluginManager object. 
 */
public function __construct() {
  $this->baseDiscovery = new LegacyDiscoveryDecorator(new AnnotatedClassDiscovery('field', 'widget'));
  $this->discovery = new CacheDecorator($this->baseDiscovery, $this->cache_key, $this->cache_bin);

  $this->factory = new WidgetFactory($this);
}

There's actually a bit going on here, as the Widget plugin manager is using a discovery decorator to handle legacy hook_field_widget_info implementations.

What's happening here is three layers of discovery. The base discovery layer is the first layer, this is an instance of LegacyDiscoveryDecorator (Drupal\field\Plugin\Type\LegacyDiscoveryDecorator) which itself decorates (think wraps) an AnnotatedClassDiscovery (Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery) that is passed as an argument to its constructor. The third layer is the CacheDecorator which wraps the LegacyDiscoveryDecorator.

In terms of the actual implementation, the LegacyDiscoveryDecorator uses the passed AnnotatedClassDiscovery to fetch the widgets using the annotation based discovery but then initialises a HookDiscovery class (Drupal\Core\Plugin\Discovery\HookDiscovery) to wrap the old hook_field_widget_info implementations in a LegacyWidget class. The relevant code is contained in LegacyDiscoveryDecorator:getDefinitions():

public function getDefinitions() {
  $definitions = $this->decorated->getDefinitions();

  $legacy_discovery = new HookDiscovery('field_widget_info');
  if ($legacy_definitions = $legacy_discovery->getDefinitions()) {
    // Process each legacy definition, wrap it with a LegacyWidget
    // and add it to the $definitions.
    ....  
  }
  return $definitions
}

Using the Annotation based discovery

As seen in the code above, using the annotation based discovery is as simple as instantiating a new AnnotatedClassDiscovery object.

new AnnotatedClassDiscovery('field', 'widget')

The arguments passed are the owning module ('field') and the plugin type ('widget').

Using the Hook based discovery

Hook based plugin discovery is also as simple as instantiating a class

new HookDiscovery('field_widget_info');

Here the argument is the hook name

Defining plugins using annotations

We won't go into detail about how to define plugins using hook based discovery, as that's nothing new to a seasoned Drupal developer.

Defining a plugin using annotations is as simple as documenting the plugin in the class's docblock. Consider an example from TextfieldWidget (Drupal\text\Plugin\field\widget\TextfieldWidget).

namespace Drupal\text\Plugin\field\widget;

use Drupal\Core\Annotation\Plugin; 
use Drupal\Core\Annotation\Translation; 
use Drupal\field\Plugin\Type\Widget\WidgetBase;

/**
 * Plugin implementation of the 'text_textfield' widget.
 *
 * @FieldWidget(
 *   id = "text_textfield",
 *   label = @Translation("Text field"),
 *   field_types = {
 *     "text"
 *   },
 *   settings = {
 *     "size" = "60",
 *     "placeholder" = ""
 *   }
 * )
 */
class TextfieldWidget extends WidgetBase {
   // ..methods/properties required to implement the plugin
}

Take note of the use statements here. The format of the plugin should look familiar to anyone who's ever implemented hook_field_widget_info(), the plugin definition largely mimics the format of the return from that hook.

Summary

Drupal 8's plugin system will revolutionize developers' approach to extending Drupal. There is still a lot of work to be done. Want to help out? Here are some issues that include porting core systems to the plugin system - as always, the best way to learn a new technique and keep abreast with what is happening in the next major release of core is to be involved in shaping it!