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.
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:
- Drupal 8: Properly Injecting Dependencies Using DI on code.tutsplus.com. This is the article that really helped things make sense to me. Nice work, Danny Sipos!
- Drupal 8 Dependency Injection, Service Container And All That Jazz on webomelette.com. Good companion or prerequisite to the tutsplus article, also by Danny Sipos.
- Is dependency injection basically just composition on quora.com. My paraphrased answer: "Dependency Injection uses composition to achieve Inversion of Control".
- Dependency Injection in Drupal 8 Plugins on chromatichq.com. Really nice story form exploration of using dependency injection specifically in plugins.
- Injecting services in your D8 plugins on lullabot.com. Thorough article focusing on plugins, by Mateu Aguiló Bosch. He calls the
create()
pattern a "Factory injection" style of dependency injection. I like that. - Dependency Injection in Drupal 8, an Introduction on lucius.digital. This seems like a good introduction.
- Safely extending Drupal 8 plugin classes without fear of constructor changes on previousnext.com.au
Other good reference:
- Services and dependency injection in Drupal 8 on drupal.org. Good overview.
- Services and Dependency Injection Container on api.drupal.org. Another good overview, especially on defining services.
- Structure of a service file on drupal.org. Lists all properties available in a services.yml file.
- Service Container on symfony.com. Detail on how the service container system works in symfony, on which Drupal's system is based.
- Dependency Injection on wikipedia.org. Very thorough information on exactly what DI is. Note the section on the three types of DI: Constructor, Setter, and Interface. The only type I've presented here is constructor injection. Using the
create()
method is a variation of constructor injection.
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!