Annotations-based plugins

Last updated on
9 January 2017

Most of the plugins in Drupal 8 will use annotations to register themselves and describe their metadata. Some of the plugins types provided by the core are:

  • Blocks (see */src/Plugin/Block/* for many examples)
  • Field formatters, Field widgets (see */src/Plugin/Field/* for many examples)
  • All Views plugins (see */src/Plugin/views/* for many examples)
  • Conditions (used for Block visibility in the core)
  • Migrate source, process & destination plugins

Please have a look at them in addition to this documentation to see real-life examples in the wild.

PSR-4

Plugins using annotations are registered in PHP files using the PSR-4 standard, which is followed by Drupal core.

To register your plugin, put a file in a folder relative to your Drupal module root: src/Plugin/$plugin_type/Example.php.

Example: core/modules/ckeditor/src/Plugin/CKEditorPlugin/Internal.php

In order to tell the system that something is a plugin you have to place the following comment right before your class definition (with @Plugin replaced with the particular plugin type):

/**
 * @Plugin(
 *
 * )
 */

An example of a plugin using this annotation class is the UserNameUnique found in core/modules/user/src/Plugin/Validation/Constraint/UserNameUnique.php.

This annotation contains just the ID and label:

/**
 * Checks if a user name is unique on the site.
 *
 * @Constraint(
 *   id = "UserNameUnique",
 *   label = @Translation("User name unique", context = "Validation"),
 * )
 */
class UserNameUnique extends Constraint {
...
}

Why annotations?

In contrast to other discovery mechanisms, the annotation meta-data lives in the same file and is an integral part of the class that implements the plugin. This makes it easier to find and easier to create a new custom plugin by simply copying an existing one.

Annotations allow for complex structured data, and you can indicate that certain strings are to be translated. In many cases, plugins have an associated custom annotation class that can be used to both documents and set default values for the meta-data.

In addition, there is a performance bonus as it makes Drupal use less memory when discovering plugins. The original implementation had a getInfo() method on each class, similar to Drupal 7 test classes. This meant each class had to be loaded into memory to get its information and the memory was not freed until the end of the request, thus greatly increasing the peak memory requirement for PHP. Instead, the implementation used by Drupal to parse the annotation simply tokenizes the text of the file without including it as a PHP file, so memory use is minimized.

The annotation syntax

In a nutshell, annotations are inherently key-value data structures, with support for nesting.

The annotations syntax comes from the Doctrine project (for details, see the documentation), though Drupal has a slightly different coding style, such as a new line after each value, so that will be used here.

  • You should start with a plugin ID, equivalent to a Drupal 7 "machine name", which is unique for your plugin type
  • Keys on the root level MAY use double quotes.
  • Keys in sublevels MUST use double quotes.
  • Do not use single quotes, only double quotes. Single quotes throw an exception.
  • Available data types for values are:
    • Strings: MUST use double-quotes (for example "foo"). If your string includes a double-quote character, use a pair of double-quotes to escape it (for example "The ""On"" value").
    • Numbers: MUST NOT use quotes (for example 21) — will be parsed as a string if quotes are used.
    • Booleans: MUST NOT use quotes (TRUE or FALSE) — will be parsed as a string if quotes are used.
    • Lists: use curly brackets.
      base = {
        "node",
        "foo",
      }

      Note the comma at the end of the last list element; This is not a typo! It helps prevent parsing errors if another element is placed at the end of the list later.

    • Maps: using curly brackets and an equality sign to separate the key and the value.
      edit = {
        "editor" = "direct",
      }
  • Constants are allowed.

Custom annotation classes

New plugin types should always use a custom annotation class. This allows for documentation, and a consistent developer experience.

Let's look at an example in the wild, the plaintext field formatter which is located at text/src/Plugin/field/formatter/TextPlainFormatter.php.

It uses its own annotation class, FieldFormatter, which extends \Drupal\Component\Annotation\Plugin.

/**
 * Plugin implementation of the 'text_plain' formatter.
 *
 * @FieldFormatter(
 *   id = "text_plain",
 *   label = @Translation("Plain text"),
 *   field_types = {
 *     "text",
 *     "text_long",
 *     "text_with_summary", 
 *   },
 *   edit = {
 *     "editor" = "direct",
 *   },
 * )
 */
class TextPlainFormatter {

Using annotations in your own plugin type

If you write your own plugin type and want to use annotations, you just need to extend the DefaultPluginManager which uses AnnotatedClassDiscovery by default - see the following piece of code.

The first parameter to the DefaultPluginManager constructor is the sub-directory/sub-namespace in which plugins of this type will be located. So, in this example, plugins will be searched for in the $module/src/Plugin/Field/FieldFormatter folders. The final argument is the custom annotation class defined above.

use Drupal\Component\Plugin\Factory\DefaultFactory;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;

class FormatterPluginManager extends DefaultPluginManager {

  /**
   * Constructs a FormatterPluginManager object.
   */
  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, FieldTypePluginManagerInterface $field_type_manager) {
    parent::__construct('Plugin/Field/FieldFormatter', $namespaces, $module_handler, 'Drupal\Core\Field\FormatterInterface', 'Drupal\Core\Field\Annotation\FieldFormatter');

    $this->setCacheBackend($cache_backend, 'field_formatter_types_plugins');
    $this->alterInfo('field_formatter_info');
    $this->fieldTypeManager = $field_type_manager;
  }
}

The injected namespaces come from the dependency injection container. For example, the FieldBundle:

/**
 * @file
 * Contains Drupal\field\FieldBundle.
 */

namespace Drupal\field;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;

/**
 * Field dependency injection container.
 */
class FieldBundle extends Bundle {

  /**
   * Overrides Symfony\Component\HttpKernel\Bundle\Bundle::build().
   */
  public function build(ContainerBuilder $container) {
    // Register the plugin managers for our plugin types with the dependency injection container.
    $container->register('plugin.manager.field.widget', 'Drupal\field\Plugin\Type\Widget\WidgetPluginManager')
      ->addArgument('%container.namespaces%');
    $container->register('plugin.manager.field.formatter', 'Drupal\field\Plugin\Type\Formatter\FormatterPluginManager')
      ->addArgument('%container.namespaces%');
  }

}

To call your custom plugin manager, you'll need to inject Drupal's namespaces into the class construction call.

$type = new CustomPluginManager(\Drupal::getContainer()->getParameter('container.namespaces'));