Defining a Plugin Type: The Manager, Interface, and Annotationfor Drupal 11 , and 10

Last updated :  

This is where the plugin system stops being an abstract idea and becomes real code. We're going to read SandwichPluginManager.php — a genuinely small file, under 90 lines — and by the end you'll understand every piece required to invent your own brand-new plugin type in Drupal. The example is deliberately playful: a plugin type for defining different kinds of sandwiches. The mechanics are identical to how Drupal core defines its Block, Field Type, and Field Formatter plugin types.

What you'll learn in this lesson

  • The four pieces every plugin type needs, and what job each one does
  • How a plugin manager tells Drupal where to look for plugin implementations
  • How annotations and interfaces work together to define a plugin "contract"
  • How to wire a plugin manager into Drupal's service container so any code can use it

The four pillars of a plugin type

Defining a new plugin type always means writing these four coordinated pieces:

  • Plugin Manager (SandwichPluginManager.php) — orchestrates discovery, instantiation, and caching of plugins
  • Annotation class (Annotation/Sandwich.php) — defines what metadata a plugin is allowed/required to declare
  • Plugin Interface (SandwichInterface.php) — the contract every plugin implementation must fulfil
  • Abstract Base class (SandwichBase.php) — an optional helper that removes boilerplate for whoever writes a plugin

The Plugin Manager is the piece that ties everything together, so that's where we'll spend most of our time.

The source file

Path: modules/plugin_type_example/src/SandwichPluginManager.php

<?php

namespace Drupal\plugin_type_example;

use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\plugin_type_example\Annotation\Sandwich;

/**
 * A plugin manager for sandwich plugins.
 *
 * The SandwichPluginManager class extends the DefaultPluginManager to provide
 * a way to manage sandwich plugins. A plugin manager defines a new plugin type
 * and how instances of any plugin of that type will be discovered, instantiated
 * and more.
 *
 * Using the DefaultPluginManager as a starting point sets up our sandwich
 * plugin type to use annotated discovery.
 *
 * The plugin manager is also declared as a service in
 * plugin_type_example.services.yml so that it can be easily accessed and used
 * anytime we need to work with sandwich plugins.
 */
class SandwichPluginManager 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) {
    // We replace the $subdir parameter with our own value.
    // This tells the plugin manager to look for Sandwich plugins in the
    // 'src/Plugin/Sandwich' subdirectory of any enabled modules. This also
    // serves to define the PSR-4 namespace in which sandwich plugins will live.
    // Modules can put a plugin class in their own namespace such as
    // Drupal\{module_name}\Plugin\Sandwich\MySandwichPlugin.
    $subdir = 'Plugin/Sandwich';

    // The name of the interface that plugins should adhere to. Drupal will
    // enforce this as a requirement. If a plugin does not implement this
    // interface, Drupal will throw an error.
    $plugin_interface = SandwichInterface::class;

    // The name of the annotation class that contains the plugin definition.
    $plugin_definition_annotation_name = Sandwich::class;

    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: hook_sandwich_info_alter().
    // In this example, we implement this hook to change the plugin definitions:
    // see plugin_type_example_sandwich_info_alter().
    $this->alterInfo('sandwich_info');

    // This sets the caching method for our plugin definitions. Plugin
    // definitions are discovered by examining the $subdir defined above, for
    // any classes with an $plugin_definition_annotation_name. The annotations
    // are read, and then the resulting data is cached using the provided cache
    // backend. The second argument is a cache key prefix. Out of the box Drupal
    // with the default cache backend setup will store our plugin definition in
    // the cache_default table using the sandwich_info key. All that is
    // implementation details, however; all we care about it is that caching for
    // our plugin definition is taken care of by this call.
    $this->setCacheBackend($cache_backend, 'sandwich_info', ['sandwich_info']);
  }

}

How it works, piece by piece

Extending DefaultPluginManager

class SandwichPluginManager extends DefaultPluginManager {

DefaultPluginManager is a class Drupal core provides for exactly this situation: it already knows how to scan every enabled module's files, find annotated classes, instantiate them, and cache the results. By extending it, you inherit that entire pipeline for free. You don't reimplement plugin discovery — you just tell the parent class three things about your plugin type in the constructor, and it does the rest.

The constructor's injected services

Three services arrive as constructor parameters, all supplied automatically by Drupal's service container (you never instantiate this class with new yourself):

  • \Traversable $namespaces — every enabled module's namespace and filesystem path, so the discovery system knows where to look
  • CacheBackendInterface $cache_backend — the cache service, so plugin definitions don't need to be re-scanned from disk on every request
  • ModuleHandlerInterface $module_handler — used to fire an alter hook after discovery, so other modules get a chance to modify the results

$subdir — where Drupal looks for plugins

$subdir = 'Plugin/Sandwich';

This one string does two jobs at once. First, it tells Drupal's discovery system to scan the folder src/Plugin/Sandwich/ inside every enabled module — not just this one. Any module, including ones you write yourself later, can drop a class in that folder and have it discovered automatically. Second, it defines the PSR-4 namespace fragment those classes must live in: a sandwich plugin inside a module called my_module would live at Drupal\my_module\Plugin\Sandwich\MyPlugin.

$plugin_interface — enforcing a contract

$plugin_interface = SandwichInterface::class;

This tells the manager: every class it discovers must implement SandwichInterface, or Drupal throws an error. This guarantee is what lets any other code safely call description(), calories(), and order() on a Sandwich plugin without needing to know or care which concrete class it actually is.

$plugin_definition_annotation_name — linking to the annotation

$plugin_definition_annotation_name = Sandwich::class;

This tells the discovery system which annotation class marks a valid plugin. A PHP class sitting in src/Plugin/Sandwich/ only counts as a real Sandwich plugin if it carries a @Sandwich(...) docblock annotation matching this class. We'll look at the annotation class itself in a moment.

Calling parent::__construct()

parent::__construct($subdir, $namespaces, $module_handler, $plugin_interface, $plugin_definition_annotation_name);

Notice the argument order here doesn't match the order the constructor received its own parameters in — that's intentional. This single call to the parent class is what actually wires everything together and builds Drupal's internal AnnotatedClassDiscovery object. Everything before this line was just preparing the values DefaultPluginManager needs.

alterInfo() — making the plugin type extensible

$this->alterInfo('sandwich_info');

This one line registers a brand-new alter hook: hook_sandwich_info_alter(). After all plugins are discovered and their definitions built, any enabled module — including this one — can implement hook_sandwich_info_alter(array &$definitions) to add, remove, or modify entries before anyone uses them. This is the exact same alter-hook pattern you learned back in the Hooks topic, just applied to plugin definitions instead of a form.

setCacheBackend() — caching plugin definitions

$this->setCacheBackend($cache_backend, 'sandwich_info', ['sandwich_info']);

Scanning the filesystem and parsing annotations on every single page request would be slow. This call tells Drupal to cache the discovered definitions instead, using 'sandwich_info' as the cache key (stored in the cache_default bin by default) and ['sandwich_info'] as a cache tag you could use to invalidate that entry later, the same way you learned in the Cache API topic.

Notice the pattern: this constructor doesn't contain any actual sandwich logic. It's pure configuration — telling a generic discovery-and-caching engine where to look, what contract to enforce, and how to cache the result. That's the whole trick behind Drupal's plugin system: write the wiring once per plugin type, and every plugin implementation plugs into it for free.

Registering the manager as a service

A plugin manager is only useful if other code can get hold of it. That happens in plugin_type_example.services.yml:

services:
  plugin.manager.sandwich:
    class: Drupal\plugin_type_example\SandwichPluginManager
    parent: default_plugin_manager

The parent: default_plugin_manager line is doing a lot of work here — it tells Drupal's service container to automatically supply the three constructor arguments ($namespaces, $cache_backend, $module_handler) without you writing any wiring code. Once registered, any part of Drupal can retrieve the manager with \Drupal::service('plugin.manager.sandwich') and start creating sandwich plugin instances.

The other three pieces, briefly

The annotation class

Annotation/Sandwich.php extends Drupal\Component\Annotation\Plugin and declares two public properties:

class Sandwich extends Plugin {
  public $description;  // @var \Drupal\Core\Annotation\Translation
  public $calories;     // @var int
}

Each public property becomes a field that plugin authors can (or must) fill in inside their own @Sandwich(...) annotation. Marking $description as a Translation signals that its value should be translatable.

The interface

SandwichInterface declares three methods every plugin must implement: description(), calories(), and order(array $extras). Any code that consumes a Sandwich plugin can call these three methods with total confidence, regardless of which concrete plugin class it's actually holding.

The abstract base class

SandwichBase extends core's PluginBase and implements SandwichInterface, providing ready-made implementations of description() and calories() that simply read from $this->pluginDefinition (the array built from the annotation):

public function description() {
  return $this->pluginDefinition['description'];
}

public function calories() {
  return (float) $this->pluginDefinition['calories'];
}

It deliberately leaves order() as abstract, forcing every concrete plugin to supply its own version — because that's the one behavior that genuinely differs between sandwiches.

The Plugin Type Example page listing Sandwich plugin definitions and rendered plugin output

Quick check: which of the four pieces is responsible for enforcing that every plugin implements order()? If you said the interface and the abstract base class both play a role — the interface declares the requirement, the base class leaves it unimplemented — you're exactly right.

Key takeaways

  • A new plugin type is defined by extending DefaultPluginManager and configuring three values in the constructor: $subdir, $plugin_interface, and $plugin_definition_annotation_name.
  • $subdir (like 'Plugin/Sandwich') controls both where Drupal scans across all enabled modules and the required PSR-4 namespace fragment for implementations.
  • The annotation class must be decorated with @Annotation and declares the metadata schema as public properties — this is what plugin authors fill in.
  • Registering the manager in *.services.yml with parent: default_plugin_manager lets Drupal auto-inject the standard dependencies — you never wire them by hand.
  • alterInfo() exposes a new alter hook so any module can modify discovered plugin definitions, following the same pattern you already know from hook_form_alter().
  • An abstract base class that reads from $this->pluginDefinition removes boilerplate for implementors while abstract methods force them to always write the genuinely custom behavior themselves.

Coming up next

The plugin type is fully wired — now let's see the payoff. In the final lesson of this topic, we'll read two real, working Sandwich plugin implementations: one about as simple as a plugin can be, and one that injects a Drupal service and reacts to the current day of the week.