Dependency Injection in Drupal 8 Plugins

In my last blog post, I set out to learn more about D8's cache API. What I finally wrote about in that blog post made up only a small part of the journey taken.

Along the way I used:

  • drush cutie to build a stand-alone D8 environment
  • a custom module to build a block
  • Twig templates to output the markup
  • dependency injection for the service needed to obtain the current user's ID

It was the last one, dependency injection, that is the inspiration for this post. I have read about it, I feel like I know it when I see it, but when Adam pointed out that my sample code wasn't using dependency injection properly, I set out to right my wrong.

Briefly, Why Dependency Injection?

Using dependency injection properly:

  • Ensures decoupled functionality, which is more reusable.
  • Eases unit testing.
  • Is the preferred method for accessing and using services in Drupal 8.

So, Where Was I?

In the aforementioned D8 Cache API blog post, I was building a block Plugin and grabbing the current user's ID with the global Drupal class, based on an example from D.O.

$uid = \Drupal::currentUser->id();

With a single line of code placed right where I needed it, I could grab the current user's ID and it worked a charm. However, as Adam noted, that is not the preferred way to do things.

"Many of the current tutorials on Drupal 8 demonstrate loading services statically through the \Drupal class. Developers who are used to Drupal 7 may find it faster to write that way, but using dependency injection is the technically preferred approach." - From Acquia's Lesson 8.3 - Dependency injection

Oops, guilty as charged! That global Drupal class is meant to be a bridge between old, procedural code and D8's injected, object-oriented ethos. It’s located in core/lib/Drupal.php and begins with this comment:

  /**
* Static Service Container wrapper.
*
* Generally, code in Drupal should accept its dependencies via either
* constructor injection or setter method injection. However, there are cases,
* particularly in legacy procedural code, where that is infeasible. This
* class acts as a unified global accessor to arbitrary services within the
* system in order to ease the transition from procedural code to injected OO
* code.

Further down you find the Drupal::service() method, which is a global method that can return any defined service. Note the comment Use this method if the desired service is not one of those with a dedicated accessor method below. If it is listed below, those methods are preferred as they can return useful type hints.

  /**
* Retrieves a service from the container.
*
* Use this method if the desired service is not one of those with a dedicated
* accessor method below. If it is listed below, those methods are preferred
* as they can return useful type hints.
*
* @param string $id
* The ID of the service to retrieve.
*
* @return mixed
* The specified service.
*/

public static function service($id) {
return static::getContainer()->get($id);
}

To determine what it means by “...those with a dedicated accessor method below.”, refer to the methods mentioning the return of services, like Returns the time service. from Drupal::time() or Returns the form builder service. from Drupal::formBuilder(). These dedicated accessor methods are preferred because they can return useful type hints. For instance, in the case of returning the time service, @return \Drupal\Component\Datetime\TimeInterface.

Okay, so that’s some explanation of the global Drupal class, but I wanted to use proper dependency injection! I figured I would find a quick dependency injection example, alter it for my needs and finish off the blog post. Boy, was I in for a surprise.

The Journey

I found examples, read articles, but nothing seemed to work. Adam even sent me some sample code from another project, but it didn't help me. I focused on the wrong things or couldn't see the forest through the trees. Based on the examples, I couldn't figure out how to use dependency injection AND get the current user's ID! What was I doing wrong?

Maybe I was passing the wrong type of service into my __construct method?

public function __construct(WrongService $wrong_service) {

Maybe I was missing a crucial use statement?

use Drupal\Core\Session\RightService;

I always felt one step away from nirvana, but boy, was I taking a lot of wrong steps along the way. The structure of my code matched the examples, but the services I tried never returned the user ID I needed.

Troubleshooting in the Dark

My troubleshooting focused on all the use statements. I thought I hadn't pulled the right one(s) into my code - use ... this, use ... that. I always got the error ...expecting this, got NULL. I would Google the error message, find some other examples, try them, same error or different error. Which use statement was I missing?! I was working with block Plugin code. Wait, did the dependency injection have to be in the Controller and not the Plugin? Why not just in the Plugin? When did the room get dark? How long have I been standing in this dark room? What time is it?

Finally, I took a deep breath and focused in on the fact that I was building a Plugin. I Googled "dependency injection in plugins". Eureka! The very first result was titled Lesson 11.4 - Dependency injection and plugins (link below) and the results blurb had these crucial sentences:

"Plugins are the most complex component to add dependency injection to. Many plugins don't require dependency injection, making it sometimes challenging to find examples to copy." - From Acquia's Lesson 11.4 - Dependency injection and plugins

I reiterate: ...making it sometimes challenging to find examples to copy.

"The key to making plugins use dependency injection is to implement the ContainerFactoryPluginInterface. When plugins are created, the code first checks if the plugin implements this interface. If it does, it uses the create() and __construct() pattern, if not, it uses just the __construct() pattern." - From Acquia's Lesson 11.4 - Dependency injection and plugins

“In order to have your new plugin manager use the create pattern, the only thing your plugins need is to implement the ContainerFactoryPluginInterface.” - From https://www.lullabot.com/articles/injecting-services-in-your-d8-plugins

(Speaking of the plugin manager and its create pattern, you can also take a gander at FormatterPluginManager::createInstance)

So, I was incorrectly using this standard Block definition:

class HeyTacoBlock extends BlockBase {

When I should have also been adding implements ContainerFactoryPluginInterface as follows:

class HeyTacoBlock extends BlockBase implements ContainerFactoryPluginInterface {

And this must be why I get paid the big bucks; not because I know everything, but because I submit myself to the sublime torture of failing at the simplest of things!

In the vein of keeping it simple, when you are dealing with injecting services in plugin classes, you need to implement an additional interface - ContainerFactoryPluginInterface - and if you look at that interface, you see the create() method has extra parameters.

The Plugin create() Method With Extra Parameters from ContainerFactoryPluginInterface

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

Compare the Above With a Typical Controller’s create() Method

public static function create(ContainerInterface $container)

You Thought I was Done?

Just to drill the point home and help it sink in, here’s a full example of dependency injection in a Block Plugin. Note the use statements, the implements ContainerFactoryPluginInterface and the four parameters in the create() method. At the end of the day, this was all done to get the user’s account information into the __construct() method, using the $account variable, like so: $this->account = $account;. After that, I can grab the user ID anywhere in my class with a simple $user_id = $this->account->id();

<?php

namespace Drupal\heytaco\Plugin\Block;

use Drupal\Core\Session\AccountProxy;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
* Provides a Hey Taco Results Block
*
* @Block(
* id = "heytaco_block",
* admin_label = @Translation("HeyTaco! Leaderboard"),
* )
*/

class HeyTacoBlock extends BlockBase implements ContainerFactoryPluginInterface {

/**
* @var $account \Drupal\Core\Session\AccountProxyInterface
*/

protected $account;

/**
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
* @param array $configuration
* @param string $plugin_id
* @param mixed $plugin_definition
*
* @return static
*/

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

/**
* @param array $configuration
* @param string $plugin_id
* @param mixed $plugin_definition
* @param \Drupal\Core\Session\AccountProxyInterface $account
*/

public function __construct(array $configuration, $plugin_id, $plugin_definition, AccountProxyInterface $account) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->account = $account;
}

Compare all of the above with this example of dependency injection in a Controller (as opposed to a Plugin), where only AccountInterface is injected directly into the __construct() method. The reason it seems simpler is because ControllerBase already implements ContainerInjectionInterface for you and there are fewer inherited arguments to pass through create().

<?php

namespace Drupal\heytaco\Controller;

use Drupal\Core\Session\AccountInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Controller\ControllerBase;

/**
* Controller routines for HeyTaco! block.
*/

class HeyTacoController extends ControllerBase {

protected $account;

public function __construct(AccountInterface $account) {
$this->account = $account;
}

public static function create(ContainerInterface $container) {
return new static(
$container->get('current_user')
);
}

To Summarize

I wrote this blog post for two reasons:

  1. To get more Drupal 8 Plugin dependency injection content out there to help the next person find what he/she's Googling for.
  2. To vent the frustration of being a Drupal expert and feeling like a dolt.

So remember, when you're dealing with dependency injection in Drupal 8 plugins, there's an additional interface to implement and some extra parameters in the create() method!

p.s. Oct 2017 - I added parent::__construct($configuration, $plugin_id, $plugin_definition); to my HeyTacoBlock's __construct method after the Twitter-verse asked why it was missing.