Accessing services in plugins or other classesfor Drupal 11 , 10 , 9 , and 8

Last updated :  

Controllers and forms aren't the only classes that need services — plugins (block plugins, queue workers, field formatters, and dozens of other plugin types) need them just as often, but plugins have their own container-aware interface instead of ControllerBase's create().

What you'll learn in this lesson

  • Why plugins use ContainerFactoryPluginInterface instead of the pattern you've seen so far
  • Why a plugin's create() and constructor both need three extra parameters controllers and forms don't
  • How to inject a service into a real queue worker plugin

A real service-injected plugin

use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Queue\QueueWorkerBase;
use Drupal\Core\State\StateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

abstract class ReportWorkerBase extends QueueWorkerBase implements ContainerFactoryPluginInterface {

  protected $state;
  protected $logger;

  public function __construct(array $configuration, $plugin_id, $plugin_definition, StateInterface $state, LoggerChannelFactoryInterface $logger) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->state = $state;
    $this->logger = $logger;
  }

  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('state'),
      $container->get('logger.factory')
    );
  }

}

Three extra parameters, every time

Compare this to a controller's create(ContainerInterface $container) — a plugin's signature is create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition). Those three extra parameters are how the plugin system passes along a specific plugin instance's own configuration (for a block, that might be its placement settings; for a queue worker, usually just plugin metadata). Your constructor must accept them too and forward them to parent::__construct() before adding your own injected services after them.

Implementing the interface is what makes injection possible

implements ContainerFactoryPluginInterface is the signal that tells Drupal's plugin manager "don't just instantiate me with new — call my create() method instead." Skip this interface, and the plugin manager falls back to plain instantiation, silently ignoring any create() method you wrote.

The order matters in the constructor call. parent::__construct($configuration, $plugin_id, $plugin_definition) must come first inside your constructor, before you assign your own injected properties — QueueWorkerBase's own constructor needs those three values to set up its internal plugin state correctly.

Quick check: you write a new block plugin, inject a service into its constructor, but forget to add implements ContainerFactoryPluginInterface to the class declaration. What breaks? (Answer: the plugin manager instantiates your class with plain new, calling the constructor with only $configuration, $plugin_id, $plugin_definition — your extra injected-service parameter is never supplied, causing an immediate "too few arguments" fatal error.)

Key takeaways

  • Plugins use ContainerFactoryPluginInterface, not ControllerBase's pattern — implementing the interface is what activates container-based instantiation.
  • A plugin's create() and constructor both need $configuration, $plugin_id, $plugin_definition in addition to whatever services you inject.
  • Always call parent::__construct($configuration, $plugin_id, $plugin_definition) first, before assigning your own injected properties.

Coming up next

You've now injected services into a service, a controller, and a plugin. The last piece of the puzzle is tagging — how a service can announce itself to Drupal core as "I want to participate in event dispatching" or "collect me into this other service's list," without anyone hardcoding a reference to it.