Plugin Implementations: Writing Real Sandwich Pluginsfor Drupal 11 , and 10

Last updated :  

You've seen the plugin type — the manager, interface, annotation, and base class that make Sandwich plugins possible. Now let's look at the payoff: two real, working plugin implementations. One is about as simple as a Drupal plugin can be. The other injects a Drupal service and changes its behavior based on the day of the week. Together they show the full range of what a single plugin implementation can look like — and this exact pattern is how you'd write a custom Block plugin, a custom Field Formatter, or a plugin for any other pluggable system in Drupal.

What you'll learn in this lesson

  • The minimum a class needs to be discovered as a valid plugin — no registration file required
  • How annotation values become available inside your plugin at runtime
  • How to inject Drupal services into a plugin using ContainerFactoryPluginInterface
  • How individual plugins can override inherited behavior safely, without touching any other plugin

The source files

Paths: modules/plugin_type_example/src/Plugin/Sandwich/ExampleHamSandwich.php and ExampleMeatballSandwich.php

ExampleHamSandwich.php — the minimal plugin

<?php

namespace Drupal\plugin_type_example\Plugin\Sandwich;

use Drupal\plugin_type_example\SandwichBase;

/**
 * Provides a ham sandwich.
 *
 * Because the plugin manager class for our plugins uses annotated class
 * discovery, our ham sandwich only needs to exist within the Plugin\Sandwich
 * namespace, and provide a Sandwich annotation to be declared as a plugin.
 * This is defined in
 * \Drupal\plugin_type_example\SandwichPluginManager::__construct().
 *
 * The following is the plugin annotation. This is parsed by Doctrine to make
 * the plugin definition. Any values defined here will be available in the
 * plugin definition.
 *
 * This should be used for metadata that is specifically required to instantiate
 * the plugin, or for example data that might be needed to display a list of all
 * available plugins where the user selects one. This means many plugin
 * annotations can be reduced to a plugin ID, a label and perhaps a description.
 *
 * @Sandwich(
 *   id = "ham_sandwich",
 *   description = @Translation("Ham, mustard, rocket, sun-dried tomatoes."),
 *   calories = 426
 * )
 */
class ExampleHamSandwich extends SandwichBase {

  /**
   * Place an order for a sandwich.
   *
   * This is just an example method on our plugin that we can call to get
   * something back.
   *
   * @param array $extras
   *   Array of extras to include with this order.
   *
   * @return string
   *   A description of the sandwich ordered.
   */
  public function order(array $extras) {
    $ingredients = ['ham, mustard', 'rocket', 'sun-dried tomatoes'];
    $sandwich = array_merge($ingredients, $extras);
    return 'You ordered an ' . implode(', ', $sandwich) . ' sandwich. Enjoy!';
  }

}

ExampleMeatballSandwich.php — service injection and conditional behavior

<?php

namespace Drupal\plugin_type_example\Plugin\Sandwich;

use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\plugin_type_example\SandwichBase;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Provides a meatball sandwich.
 *
 * Because the plugin manager class for our plugins uses annotated class
 * discovery, our meatball sandwich only needs to exist within the
 * Plugin\Sandwich namespace, and provide a Sandwich annotation to be declared
 * as a plugin. This is defined in
 * \Drupal\plugin_type_example\SandwichPluginManager::__construct().
 *
 * The following is the plugin annotation. This is parsed by Doctrine to make
 * the plugin definition. Any values defined here will be available in the
 * plugin definition.
 *
 * This should be used for metadata that is specifically required to instantiate
 * the plugin, or for example data that might be needed to display a list of all
 * available plugins where the user selects one. This means many plugin
 * annotations can be reduced to a plugin ID, a label and perhaps a description.
 *
 * @Sandwich(
 *   id = "meatball_sandwich",
 *   description = @Translation("Italian style meatballs drenched in irresistible marinara sauce, served on freshly baked bread."),
 *   calories = "1200"
 * )
 */
class ExampleMeatballSandwich extends SandwichBase implements ContainerFactoryPluginInterface {

  // Use Drupal\Core\StringTranslation\StringTranslationTrait to define
  // $this->t() for string translations in our plugin.
  use StringTranslationTrait;

  /**
   * The day the sandwich is ordered.
   *
   * Since meatball sandwiches have a special behavior on Sundays, and since we
   * want to test that behavior on days other than Sunday, we have to store the
   * day as a property so we can test it.
   *
   * This is the string representation of the day of the week you get from
   * date('D').
   *
   * @var string
   */
  protected $day;

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    // This class needs to translate strings, so we need to inject the string
    // translation service from the container. This means our plugin class has
    // to implement ContainerFactoryPluginInterface. This requires that we make
    // this create() method, and use it to inject services from the container.
    // @see https://www.drupal.org/node/2012118
    $sandwich = new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('string_translation')
    );
    return $sandwich;
  }

  /**
   * {@inheritdoc}
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, TranslationInterface $translation) {
    // Store the translation service.
    $this->setStringTranslation($translation);
    // Store the day so we can generate a special description on Sundays.
    $this->day = date('D');
    // Pass the other parameters up to the parent constructor.
    parent::__construct($configuration, $plugin_id, $plugin_definition);
  }

  /**
   * {@inheritdoc}
   */
  public function order(array $extras) {
    $ingredients = ['meatballs', 'irresistible marinara sauce'];
    $sandwich = array_merge($ingredients, $extras);
    return 'You ordered an ' . implode(', ', $sandwich) . ' sandwich. Enjoy!';
  }

  /**
   * {@inheritdoc}
   */
  public function description() {
    // We override the description() method in order to change the description
    // text based on the date. On Sunday we only have day old bread.
    if ($this->day == 'Sun') {
      return $this->t("Italian style meatballs drenched in irresistible marinara sauce, served on day old bread.");
    }
    return parent::description();
  }

}

How it works, piece by piece

Namespace and file placement — the discovery contract

Both classes live in src/Plugin/Sandwich/, matching exactly the $subdir value the plugin manager was configured with in the previous lesson. Their namespace, Drupal\plugin_type_example\Plugin\Sandwich, maps directly to that folder path following PSR-4. This is the entire "registration" step — there is no YAML list of plugins to maintain anywhere. Put the file in the right place, with the right annotation, and Drupal finds it automatically.

The @Sandwich annotation — plugin metadata

Each class carries a docblock starting with @Sandwich(...), parsed at discovery time and stored as the plugin's definition array:

  • id — the machine-readable string other code uses to reference this specific plugin, e.g. $manager->createInstance('ham_sandwich').
  • description — wrapped in @Translation(...) to mark it for Drupal's translation system.
  • calories — a custom field defined by the Sandwich annotation class itself. Notice ham_sandwich uses an integer (426) while meatball_sandwich uses a string ("1200") — SandwichBase::calories() handles this by casting to float either way.

ExampleHamSandwich — the minimal plugin pattern

This is about as small as a valid plugin gets: no constructor, no injected services, no extra properties. It only needs to carry the @Sandwich annotation and implement the one abstract method, order(array $extras). Inside that method, a hardcoded ingredients list is merged with whatever $extras the caller supplied, and imploded into a friendly string. This proves the point: a plugin doesn't need to be complicated to be valid — it just needs to satisfy the contract.

ExampleMeatballSandwich — injecting a service with ContainerFactoryPluginInterface

This plugin needs the string_translation service to call $this->t(). Since plugins aren't normally instantiated the way services are, it implements ContainerFactoryPluginInterface to signal that Drupal should call a static create() method instead of the constructor directly.

public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition)

create() receives the full service container plus the usual three plugin arguments, pulls string_translation out of the container, and passes it along to new static(...). Using new static() rather than new self() is a small but important detail: if another module ever extends this class, static ensures the correct subclass gets instantiated.

The constructor stores the injected translation service via setStringTranslation() (provided by the StringTranslationTrait mixed in near the top of the class), records the current day of the week into $this->day, and hands the rest off to the parent constructor.

Overriding description() — context-aware behavior

public function description() {
  if ($this->day == 'Sun') {
    return $this->t("Italian style meatballs drenched in irresistible marinara sauce, served on day old bread.");
  }
  return parent::description();
}

On Sundays this plugin swaps in a different description warning about day-old bread; every other day, it falls back to parent::description(), which just reads the value from the annotation — exactly what ExampleHamSandwich relies on by not overriding anything at all. This is the real strength of the plugin pattern: any single implementation can override inherited behavior without affecting any other plugin sharing the same base class.

Why store $this->day in the constructor instead of calling date('D') directly inside description()? It makes the class testable. A unit test can create a subclass that overrides $this->day with a fixed value, letting it verify the Sunday behavior without waiting for an actual Sunday or mocking PHP's global date functions.

How Drupal chooses which path to take

When code calls $manager->createInstance('ham_sandwich'), Drupal:

  • Looks up the cached definition for ham_sandwich in the plugin registry.
  • Checks whether the class implements ContainerFactoryPluginInterface.
  • If yes, calls ClassName::create($container, $configuration, $plugin_id, $plugin_definition).
  • If no, calls new ClassName($configuration, $plugin_id, $plugin_definition) directly.

ExampleHamSandwich takes the simple path. ExampleMeatballSandwich takes the factory path. Both end up as fully valid, interchangeable Sandwich plugins from the outside — the caller never needs to know or care which path was taken.

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

Quick check: if you wrote a third sandwich plugin that needed the current logged-in user (not just translation), which interface would you implement, and which method would you add? If you said ContainerFactoryPluginInterface and a static create() method that pulls current_user out of the container — exactly the pattern you just read.

Key takeaways

  • A plugin implementation only needs two things to be discovered: correct namespace/folder placement, and a correctly matching annotation — no manual registration anywhere.
  • Annotation properties beyond id are custom metadata defined by the plugin type's own annotation class, and become available as $this->pluginDefinition['key'] inside the plugin.
  • Extending a provided base class reduces boilerplate — you inherit default implementations and only write the methods that make your plugin unique.
  • When a plugin needs Drupal services, implement ContainerFactoryPluginInterface and add a static create() factory method — this is the standard, testable way to inject dependencies into a plugin.
  • Any individual plugin can override inherited methods to provide its own context-aware behavior without touching any other plugin in the system.
  • Capturing runtime values (like the current day) as class properties in the constructor, rather than computing them inline, keeps plugin behavior controllable and testable.

Coming up next

You now understand the plugin system end to end — the same architecture behind blocks, field types, and formatters throughout Drupal core. Next, we turn to the Cache API: the system that makes all of this fast enough for production, and the concepts (cache bins, tags, and contexts) you'll need to keep your own plugins and pages performing well at scale.