How to make a Drupal 8 Annotation Plugin

Submitted by cerium on Sat, 12/15/2018 - 21:59

The Annotation Plugin

One of the great new developments in Drupal 8 is the Annotation-based plugin system, which replaces a number of module hooks, and permits users to declare classes that may be used to modify behaviors in other modules. Under the old hook system, if you wanted to allow one module to extend another, you would need to create a hook, and then that hook would have to load the .module file of every enabled module to look for and load hooks. When you consider that several modules use hooks, you are talking about a lot of code that needs to be loaded for every page!

The plugin system, in contrast, lets you do a couple of interesting things from an efficiency perspective:

  • Provide information about your plugin in an annotation, so that a module does not have to load all of another module's plugin code if it does not match certain criteria
  • Each annotation-based plugin may specify a particular location under the /src folder, which means you do not have to load a /module file or anything other than the specific code you are looking for

From a functionality perspective, the main benefit of adding an annotation-based plugin to your modules is that you will enable other modules to dynamically change your module's behavior, or extend what it can do. For example, in the Views Add Button module, one can override the default means of creating an "add entity button" to handle a custom entity's particular needs, or to add behavior that is particular to your site. An example of this in action is Views Add Button: Group.

Making an Annotation - step by step

To make an annotation-based plugin that may be used by other modules, follow these steps:

  1. In your module, make a PluginInterface and PluginManager in the /src folder
  2. Next, define the annotation in /src/Annotation
  3. Create or modify a controller in /src/Controller to include the necessary function calls
  4. Register a service in a mymodule.services.yml file
  5. (optional) Create a "default" plugin in case your module will rely on a plugin
  6. Start using your annotation-based plugin!

1) PluginInterface and PluginManager

Let's start by making these two file in our src folder. They are a part of registering the plugin with Drupal

src/MyPluginInterface.php

<?php
/**
 * @file
 * Provides Drupal\mymodule\MyPluginInterface;
 */
namespace Drupal\mymodule;
/**
 * An interface for all MyPlugin type plugins.
 */
interface MyPluginInterface {
    /**
     * Provide a description of the plugin.
     * @return string
     *   A string description of the plugin.
     */
    public function description();
}

src/MyPluginManager.php

This one is critical - you will define where other developers should place their plugins in their module's src folder, as well as define hooks in the event they are used. In our case, we will create a MyPlugin plugin, and these will be placed in src/Plugin/mymodule

<?php
/**
 * @file
 * Contains \Drupal\mymodule\MyPluginManager.
 */
namespace Drupal\mymodule;

use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
/**
 * Manages mymodule plugins.
 */
class MyPluginManager extends DefaultPluginManager {
    /**
     * Creates the discovery object.
     *
     * @param \Traversable $namespaces
     *   An object that implements \Traversable which contains the root paths
     *   keyed by the corresponding namespace to look for plugin implementations.
     * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
     *   Cache backend instance to use.
     * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
     *   The module handler to invoke the alter hook with.
     */
    public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
        // This tells the plugin system to look for plugins in the 'Plugin/mymodule' subfolder inside modules' 'src' folder.
        $subdir = 'Plugin/mymodule';
        // The name of the interface that plugins should adhere to.  Drupal will enforce this as a requirement.
        $plugin_interface = 'Drupal\mymodule\MyPluginInterface';
        // The name of the annotation class that contains the plugin definition.
        $plugin_definition_annotation_name = 'Drupal\mymodule\Annotation\MyPlugin';
        parent::__construct($subdir, $namespaces, $module_handler, $plugin_interface, $plugin_definition_annotation_name);
        // This allows the plugin definitions to be altered by an alter hook. The parameter defines the name of the hook, thus: mymodule_info_alter().
        $this->alterInfo('mymodule_info');
        // This sets the caching method for our plugin definitions.
        $this->setCacheBackend($cache_backend, 'mymodule_info');
    }
}

2) Next, define the annotation in /src/Annotation

Here, we create the plugin! Note that the "Plugin Namespace" was defined in MyPluginManager, above.

src/Annotation/MyPlugin.php

<?php

namespace Drupal\mymodule\Annotation;

use Drupal\Component\Annotation\Plugin;

/**
 * Defines a MyPlugin annotation object.
 *
 * Plugin Namespace: Plugin\mymodule
 *
 * @see plugin_api
 *
 * @Annotation
 */
class MyPlugin extends Plugin {

    /**
     * The plugin ID.
     *
     * @var string
     */
    public $id;

    /**
     * The human-readable name of the MyPlugin.
     *
     * @ingroup plugin_translatable
     *
     * @var \Drupal\Core\Annotation\Translation
     */
    public $label;

    /**
     * The category under which the MyPlugin should be listed in the UI.
     *
     * @var \Drupal\Core\Annotation\Translation
     *
     * @ingroup plugin_translatable
     */
    public $category;

}

 

3) Create a controller in /src/Controller

This is used for finding and calling instances of your plugin, when they can be found in other modules. Note that you will make use of you MyPluginManager.

src/Controller/MyPluginController.php

<?php
/**
 * @file
 */

namespace Drupal\mymodule\Controller;

use Drupal\mymodule\MyPluginManager;
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Class MyPluginController
 *
 * Provides the route and API controller for mymodule.
 */
class MyPluginController extends ControllerBase
{

  protected $MyPluginManager; //The plugin manager.

  /**
   * Constructor.
   *
   * @param \Drupal\mymodule\MyPluginManager $plugin_manager
   */

  public function __construct(MyPluginManager $plugin_manager) {
    $this->MyPluginManager = $plugin_manager;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    // Use the service container to instantiate a new instance of our controller.
    return new static($container->get('plugin.manager.mymodule'));
  }
}

 

4) Register a service in a mymodule.services.yml file

This step makes your plugin controller a callable service, this letting you load plugins.

mymodule.services.yml

services:
  # The machine name of the service. This is the string that must be passed to
  # Drupal::service() to get the instantiated plugin manager.
  plugin.manager.mymodule:
    # This tells the service container the name of our plugin manager class.
    class: Drupal\mymodule\MyPluginManager
    arguments: ['@container.namespaces', '@cache.default', '@module_handler']

 

5) (optional) Create a "default" plugin in case your module will rely on a plugin

Depending on the way you will use plugins, you may want to create a default plugin if no others are available. The nature of the plugin will depend heavily on how you want to use it, but a few common rules are in order:

  • The plugin should be in the location you noted in your plugin manager class (MyPluginManager)
  • The comments immediately above your plugin class should contain an annotation, which starts with an @, names the annotation class, and in parentheses you use a key=value format to spell out an array of settings. These are loaded when you call a plugin.
  • You may learn more about annotation formatting at https://www.drupal.org/docs/8/api/plugin-api/annotations-based-plugins

Here is an example of a very basic implementation:

src/Plugin/mymodule/DefaultPlugin.php

<?php

namespace Drupal\mymodule\Plugin\mymodule;

use Drupal\Core\Plugin\PluginBase;
use Drupal\mymodule\MyPluginInterface;

/**
 * @MyPlugin(
 *   id = "default_plugin",
 *   label = @Translation("DefaultPlugin"),
 *   key = "value"
 * )
 */
class DefaultPlugin extends PluginBase implements MyPluginInterface {

  /**
   * @return string
   *   A string description.
   */
  public function description()
  {
    return $this->t('This is a description of the default plugin.');
  }

  /**
   * Since this is a default, just return what we have.
   */
  public static function doStuff() {
    return 'I did stuff';
  }
}

6) Start using your annotation-based plugin!

In your module's code, you may load the annotation-based plugins as a service. This is the same as what you defined in the services.yml file

$plugin_manager = \Drupal::service('plugin.manager.mymodule');
$plugin_definitions = $plugin_manager->getDefinitions();

The plugin definitions will be keyed by the 'id' parameter of your annotation, and inside of that will be a keyed array containing everything you defined. Also, there will be a key, 'class' , which has the plugin class. For example, if you defined a plugin like this:

/**
 * @MyPlugin(
 *   id = "taco_plugin",
 *   label = @Translation("TacoPlugin"),
 *   favorite_food = "tacos"
 * )
 */

You could load this plugin and others like it with the following code:

$plugin_manager = \Drupal::service('plugin.manager.mymodule');
$plugin_definitions = $plugin_manager->getDefinitions();
foreach ($plugin_definitions as $pd) {
  if (!empty($pd['favorite_food']) && $pd['favorite_food'] === 'tacos') {
    // We have a match! call $pd['class'] and do something
    $pd['class']::doStuff();
  }
}

More information and code examples will be going into an upcoming module developer's course, in which we will show practical applications of this module, including how it was used in Views Add Button, Spectra Analytics, and more.