Defining a new plugin type with annotationsfor Drupal 8 , 9 , 10 , and 11

Last updated :  

In the last lesson you saw the three pieces every plugin type needs — an interface, a manager, and a discovery mechanism. This lesson builds all three from scratch. Think of it like designing a new kind of electrical socket before any appliance exists for it: you have to decide the shape (the interface) and build the wall-mounted receptacle (the manager) before anyone can plug anything in.

What you'll learn in this lesson

  • The four files a brand-new plugin type needs: interface, annotation class, base class, and manager
  • How to register the manager as a service so the rest of your module can use it
  • Why the manager extends DefaultPluginManager instead of implementing plugin discovery yourself

The interface: the contract every implementation must honor

Start with the interface. It defines the minimum set of methods every plugin of this type must provide — nothing about how they're discovered, just what they must be able to do:

namespace Drupal\plugin_type_example;

interface SandwichInterface {

  /**
   * Place an order for a sandwich.
   *
   * @param array $extras
   *   Array of extras to include with this order.
   *
   * @return string
   *   A description of the sandwich ordered.
   */
  public function order(array $extras);

}

Any class implementing this interface can be discovered as a Sandwich plugin — as long as it also carries the right annotation, which is what the manager actually scans for.

The manager: where discovery actually happens

This is the piece from the last lesson, now shown as a complete, standalone class you'd write for a brand-new plugin type:

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;

class SandwichPluginManager extends DefaultPluginManager {

  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
    $subdir = 'Plugin/Sandwich';
    $plugin_interface = SandwichInterface::class;
    $plugin_definition_annotation_name = Sandwich::class;

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

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

}

Extending DefaultPluginManager is what saves you from writing discovery logic yourself — it already knows how to scan every enabled module's namespace for a given subdirectory and annotation, cache the results, and instantiate a class on request. Your job is just to tell it three things: where to look, what interface to enforce, and which annotation class to read.

Registering the manager as a service

A plugin manager is only useful if other code can get hold of it, so it's declared as a service 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 real work here — it means this service definition inherits the standard constructor arguments ($namespaces, $cache_backend, $module_handler) that every plugin manager needs, so you don't have to wire them up by hand. From anywhere in your code, \Drupal::service('plugin.manager.sandwich') now gives you a working manager that can list and instantiate every Sandwich plugin on the site.

Order matters less than you'd think: the interface, annotation class, and base class can be written in almost any order, but the manager's constructor references all three by class name, so it won't actually run correctly until they all exist. Write the interface first — everything else is defined in terms of the contract it establishes.

Quick check: if you renamed the subdirectory from Plugin/Sandwich to Plugin/Sandwiches in the manager but a module's plugin class still lived in src/Plugin/Sandwich/, what would happen? (The plugin would silently stop being discovered — no error, it just wouldn't appear in the manager's list, since discovery is purely based on matching that subdirectory path.)

Key takeaways

  • A new plugin type needs an interface (the contract), an annotation class (the metadata schema), a base class (shared implementation), and a manager (discovery + instantiation).
  • Extending DefaultPluginManager means you never write discovery logic by hand — you just configure where to look and what shape to expect.
  • Declaring the manager with parent: default_plugin_manager in your *.services.yml file wires up its standard constructor arguments automatically.
  • Plugin discovery is purely path- and annotation-based — get the subdirectory name wrong and a plugin vanishes with no error message.

Coming up next

The manager and interface are the socket — next you'll look closely at the annotation class itself, the piece that defines exactly what metadata every implementation is required (or allowed) to declare.