Dependency Injection in Drupal 8

If you're like me, you were shown the concept of Dependency Injection and it pretty much made sense. Then you encountered the term in Drupal related to services and containers, and it didn't make so much sense any more. I've been studying it in Drupal core recently, and it makes more sense to me now. In this post, I'll try to clarify what it is, and how it's used in Drupal 8.

June 27, 2018

If you're like me, you were shown the concept of Dependency Injection and it pretty much made sense. Then you encountered the term in Drupal related to services and containers, and it didn't make so much sense any more. I've been studying it in Drupal core recently, and it makes more sense to me now. In this post, I'll try to clarify what it is, and how it's used in Drupal 8. And I'll call it "DI" for brevity.

Here are the important nuggets:

  • DI is a design pattern used in programming.
  • DI uses composition.
  • DI achieves inversion of control.
  • Dependency == service that your class needs == object of a certain type.
  • Inject == provide == compose == assemble.
  • Container == service container == dependency container.
  • Instead of using \Drupal::service('foo_service'), get the service from the $container if using a class.

And the important reasons:

  • Externalizing dependencies makes code easier to test.
  • It allows dependencies to be replaced without interfering with other functionality.
  • Retrieving dependencies from the container is better for performance.

Let's dive right in to some examples, and I'll talk our way through them.

Services: node.grant_storage

The easiest examples to find are services that have arguments, because you can search *.services.yml files for the word "arguments".

In node.services.yml for example, there is this entry:

  node.grant_storage:
    class: Drupal\node\NodeGrantDatabaseStorage
    arguments: ['@database', '@module_handler', '@language_manager']
    tags:
      - { name: backend_overridable }

That is saying that for the node.grant_storage service, the Drupal\node\NodeGrantDatabaseStorage class will be used, and three arguments will be passed to it when creating an instance of it. The @ symbol means that these are instances of other services. An instance of a database service, a module_handler service, and a language_manager service will be provided to this node.grant_storage service. These services are just objects of designated types.

Here's the relevant portion of the NodeGrantDatabaseStorge class. I've added line breaks to this and other code samples for readability.

/**
 * Defines a storage handler class that handles the node grants system.
 *
 * This is used to build node query access.
 *
 * @ingroup node_access
 */
class NodeGrantDatabaseStorage implements NodeGrantDatabaseStorageInterface {

  /**
   * The database connection.
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected $database;

  /**
   * The module handler.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

  /**
   * The language manager.
   *
   * @var \Drupal\Core\Language\LanguageManagerInterface
   */
  protected $languageManager;

  /**
   * Constructs a NodeGrantDatabaseStorage object.
   *
   * @param \Drupal\Core\Database\Connection $database
   *   The database connection.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler.
   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
   *   The language manager.
   */
  public function __construct(
    Connection $database,
    ModuleHandlerInterface $module_handler,
    LanguageManagerInterface $language_manager
  ) {
    $this->database = $database;
    $this->moduleHandler = $module_handler;
    $this->languageManager = $language_manager;
  }
}

The three arguments used in the constructor match the three arguments defined in the services file. The passed objects are then stored as properties of the class. In other words, the three objects (dependencies) were injected into the client class (NodeGrantDatabaseStorage) by the services system.

That's it. That's all there is to implementing constructor injection in Drupal. Define a service in a module's .services.yml file, and use arguments specified there in a class constructor.

Two of the three arguments have Interface as part of the type hint. That's setting the expectation that they will be of a certain type. Interfaces are very frequently used this way.

Controller: NodeViewController

The Services and dependency injection in Drupal 8 page on drupal.org says:

Many of the controller and plugin classes provided by modules in core make use of this pattern and serve as a good resource for seeing it in action.

So let's look at a controller, the NodeViewController. It extends EntityViewController which implements ContainerInjectionInterface, so it is container aware. This is a very frequently used interface for classes that are container aware.

The main thing to notice about it is its create() method. It's a factory method. Whenever an instance is created, the create() method is used instead of the constructor. See ClassResolver. So create() is the entry point, instead of__construct(). And return new static() means "return a new instances of the current class, using these passed arguments in the class's constructor".

Let's look at the actual code.

/**
 * Defines a controller to render a single node.
 */
class NodeViewController extends EntityViewController {

  /**
   * The current user.
   *
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected $currentUser;

  /**
   * Creates an NodeViewController object.
   *
   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
   *   The entity manager.
   * @param \Drupal\Core\Render\RendererInterface $renderer
   *   The renderer service.
   * @param \Drupal\Core\Session\AccountInterface $current_user
   *   The current user. For backwards compatibility this is optional, however
   *   this will be removed before Drupal 9.0.0.
   */
  public function __construct(
    EntityManagerInterface $entity_manager,
    RendererInterface $renderer,
    AccountInterface $current_user = NULL
  ) {
    parent::__construct($entity_manager, $renderer);
    $this->currentUser = $current_user ?: \Drupal::currentUser();
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('entity.manager'),
      $container->get('renderer'),
      $container->get('current_user')
    );
  }
}

There are three services that this class needs: entity.manager, renderer, and current_user. The container knows about every single service available in the site, so in create(), the three services we need are retrieved and passed to the constructor.

The parent class is already handling the entity_manager and current_user services, so we only concern ourselves with current_user. That service can potentially be null, so a ternary operator is used. (The currentUser property is set to the passed $current_user if it's not empty. Otherwise, we look it up fresh using \Drupal::currentUser().)

The dependency injection is getting the current_user service in the create() method, passing it to the constructor, and using it there.

Plugin: FileWidget

Let's look at a plugin now, and start with the FileWidget. Here's its beginning, which is the part we're interested in.

/**
 * Plugin implementation of the 'file_generic' widget.
 *
 * @FieldWidget(
 *   id = "file_generic",
 *   label = @Translation("File"),
 *   field_types = {
 *     "file"
 *   }
 * )
 */
class FileWidget extends WidgetBase implements ContainerFactoryPluginInterface {

  /**
   * {@inheritdoc}
   */
  public function __construct(
    $plugin_id,
    $plugin_definition,
    FieldDefinitionInterface $field_definition,
    array $settings,
    array $third_party_settings,
    ElementInfoManagerInterface $element_info
  ) {
    parent::__construct(
      $plugin_id,
      $plugin_definition,
      $field_definition,
      $settings,
      $third_party_settings
    );
    $this->elementInfo = $element_info;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(
    ContainerInterface $container,
    array $configuration,
    $plugin_id,
    $plugin_definition
  ) {
    return new static(
      $plugin_id,
      $plugin_definition,
      $configuration['field_definition'],
      $configuration['settings'],
      $configuration['third_party_settings'],
      $container->get('element_info')
    );
  }
}

It implements ContainerFactoryPluginInterface which is similar to ContainerInjectionInterface, except it defines for the create() method 3 more parameters in addition to $container. The important thing is that the first parameter is still the container.

Since the interface defines four parameters, the implementation must use all four. This particular class extends WidgetBase(), so that's one reason that it's passing more arguments to the constructor: to match WidgetBase.

Also notice the last argument used in the constructor call of return new static(): it's $container->get('element_info'). It's not used by the parent's constructor (it's not used in parent::__construct()), but it is used in this class's constructor. It sets the elementInfo property. I think that this class should define it explicitly, instead of creating it implicitly in the constructor.

Again, the point is to get the object/service/dependency that your class needs from the container in the create() method, pass it to the constructor, and store a reference to that service for use elsewhere in your class. The fact that there are a lot of other parameters bouncing around is only because it's a plugin (which requires more parameters in the create() method), and because it's extending WidgetBase (which requires more parameters in the constructor).

Plugin: LinkFormatter

Let's look at a field formatter plugin now, the LinkFormatter.

/**
 * Plugin implementation of the 'link' formatter.
 *
 * @FieldFormatter(
 *   id = "link",
 *   label = @Translation("Link"),
 *   field_types = {
 *     "link"
 *   }
 * )
 */
class LinkFormatter extends FormatterBase implements ContainerFactoryPluginInterface {

  /**
   * The path validator service.
   *
   * @var \Drupal\Core\Path\PathValidatorInterface
   */
  protected $pathValidator;

  /**
   * {@inheritdoc}
   */
  public static function create(
    ContainerInterface $container,
    array $configuration,
    $plugin_id,
    $plugin_definition
  ) {
    return new static(
      $plugin_id,
      $plugin_definition,
      $configuration['field_definition'],
      $configuration['settings'],
      $configuration['label'],
      $configuration['view_mode'],
      $configuration['third_party_settings'],
      $container->get('path.validator')
    );
  }

  /**
   * Constructs a new LinkFormatter.
   *
   * @param string $plugin_id
   *   The plugin_id for the formatter.
   * @param mixed $plugin_definition
   *   The plugin implementation definition.
   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
   *   The definition of the field to which the formatter is associated.
   * @param array $settings
   *   The formatter settings.
   * @param string $label
   *   The formatter label display setting.
   * @param string $view_mode
   *   The view mode.
   * @param array $third_party_settings
   *   Third party settings.
   * @param \Drupal\Core\Path\PathValidatorInterface $path_validator
   *   The path validator service.
   */
  public function __construct(
    $plugin_id,
    $plugin_definition,
    FieldDefinitionInterface $field_definition,
    array $settings,
    $label,
    $view_mode,
    array $third_party_settings,
    PathValidatorInterface $path_validator
  ) {
    parent::__construct(
      $plugin_id,
      $plugin_definition,
      $field_definition,
      $settings,
      $label,
      $view_mode,
      $third_party_settings
    );
    $this->pathValidator = $path_validator;
  }
}

Just like the file field plugin, this implements ContainerFactoryPluginInterface, so the create() method starts with the same four arguments as the other plugin. Because it extends FormatterBase instead of WidgetBase, there are additional, different arguments expected in create(), and a different set of arguments passed to the constructor.

The only places where dependency injection shows up here is in create(), with $container->get('path.validator'), which is passed to the constructor which stores it using $this->pathValidator = $path_validator;.

Here's an illustration of the flow.

Dependency Injection flow

Plugin: Extending LinkFormatter

One final example. What if you want to extend Linkformatter, and you want to use an additional service. Do you have to repeat all that boilerplate code, passing an increasing number of parameters around? I don't think so! Let's look.

/**
 * Plugin implementation of the 'special_link' formatter.
 *
 * Adds page title to URLs of links.
 *
 * @FieldFormatter(
 *   id = "special_link",
 *   label = @Translation("Link with title added to URL"),
 *   field_types = {
 *     "link"
 *   }
 * )
 */
class SpecialLinkFormatter extends LinkFormatter {

  /**
   * Title resolver service.
   *
   * @var TitleResolverInterface $titleResolver
   */
  private $titleResolver;

  /**
   * {@inheritdoc}
   */
  public static function create(
    ContainerInterface $container,
    array $configuration,
    $plugin_id,
    $plugin_definition
  ) {
    $instance = parent::create(
      $container,
      $configuration,
      $plugin_id,
      $plugin_definition
    );
    $instance->titleResolver = $container->get('title_resolver');
    return $instance;
  }
}

We still have to call parent::create() with all of the needed parameters. But we don't have to define our own constructor. Here's how. In our create() method, we call parent::create(), but instead of immediately returning it, we store it in a local variable $instance. Then we use the $container to get the title_resolver service, and store it on titleResolver property of $instance. That $instance really is an instance of our SpecialLinkFormatter (because of the magic of late static binding described in a good Stack Overflow post), so once we do that $instance->titleResolver = $container->get('title_resolver');, the dependency injection is complete. I picked up this technique from the post Safely extending Drupal 8 plugin classes without fear of constructor changes on previousnext.com.au.

Recap

To summarize all of these examples:

  • Services have dependencies passed directly to their constructors.
  • Controllers and plugins normally implement a container aware interface and have a create() method, which should be used for retrieving needed services from the $container.

Resources

Other people have written about this topic in more detail and more eloquently. I've learned a ton from these other posts.

Articles I found most helpful in piecing these thoughts together:

Other good reference:

Thanks for reading!

I hope I've helped clarify what Dependency Injection is and how it is primarily used in Drupal 8. If you have any questions, or if there are any glaring omissions, please leave a comment or contact me. I'll do my best to find the answer. Happy Drupalling!