Drupal 8: Properly Injecting Dependencies Using DI

As I am sure you know by now, dependency injection (DI) and the Symfony service container are important new development features of Drupal 8. However, even though they are starting to be better understood in the Drupal development community, there is still some lack of clarity about how exactly to inject services into Drupal 8 classes.

Many examples talk about services, but most cover only the static way of loading them:

This is understandable as the proper injection approach is more verbose, and if you know it already, rather boilerplate. However, the static approach in real life should only be used in two cases:

  • in the .module file (outside of a class context)
  • those rare occasions within a class context where the class is being loaded without service container awareness

Other than that, injecting services is the best practice as it ensures decoupled code and eases testing.

In Drupal 8 there are some specificities about dependency injection that you will not be able to understand solely from a pure Symfony approach. So in this article we are going to look at some examples of proper constructor injection in Drupal 8. To this end, but also to cover all the basics, we will look at three types of examples, in order of complexity:

  • injecting services into another of your own services
  • injecting services into non-service classes
  • injecting services into plugin classes

Going forward, the assumption is that you know already what DI is, what purpose it serves and how the service container supports it. If not, I recommend checking out this article first.

Services

Injecting services into your own service is very easy. Since you are the one defining the service, all you have to do is pass it as an argument to the service you want to inject. Imagine the following service definitions:

Here we define two services where the second one takes the first one as a constructor argument. So all we have to do now in the AnotherDemoService class is store it as a local variable:

And that is pretty much it. It's also important to mention that this approach is exactly the same as in Symfony, so no change here.

Non-Service Classes

Now let's take a look at classes that we often interact with but that are not our own services. To understand how this injection takes place, you need to understand how the classes are resolved and how they are instantiated. But we will see that in practice soon.

Controllers

Controller classes are mostly used for mapping routing paths to business logic. They are supposed to stay thin and delegate heavier business logic to services. Many extend the ControllerBase class and get some helper methods to retrieve common services from the container. However, these are returned statically.

When a controller object is being created (ControllerResolver::createController), the ClassResolver is used to get an instance of the controller class definition. The resolver is container aware and returns an instance of the controller if the container already has it. Conversely, it instantiates a new one and returns that. 

And here is where our injection takes place: if the class being resolved implements the ContainerAwareInterface, the instantiation takes place by using the static create() method on that class which receives the entire container. And our ControllerBase class also implements the ContainerAwareInterface.

So let's take a look at an example controller which properly injects services using this approach (instead of requesting them statically):

The EntityListController class doesn't do anything for our purposes here, so just imagine that BlockListController directly extends the ControllerBase class, which in turn implements the ContainerInjectionInterface.

As we said, when this controller is instantiated, the static create() method is called. Its purpose is to instantiate this class and pass whatever parameters it wants to the class constructor. And since the container is passed to create(), it can choose which services to request and pass along to the constructor. 

Then, the constructor simply has to receive the services and store them locally. Do keep in mind that it's bad practice to inject the entire container into your class, and you should always limit the services you inject to the ones you need. And if you need too many, you are likely doing something wrong.

We used this controller example to go a bit deeper into the Drupal dependency injection approach and understand how constructor injection works. There are also setter injection possibilities by making classes container aware, but we won't cover that here. Let's instead look at other examples of classes you may interact with and in which you should inject services.

Forms

Forms are another great example of classes where you need to inject services. Usually you either extend the FormBase or ConfigFormBase classes which already implement the ContainerInjectionInterface. In this case, if you override the create() and constructor methods, you can inject whatever you want. If you don't want to extend these classes, all you have to do is implement this interface yourself and follow the same steps we saw above with the controller.

As an example, let's take a look at the SiteInformationForm which extends the ConfigFormBase and see how it injects services on top of the config.factory its parent needs:

As before, the create() method is used for the instantiation, which passes to the constructor the service required by the parent class as well as some extra ones it needs on top.

And this is pretty much how the basic constructor injection works in Drupal 8. It's available in almost all class contexts, save for a few in which the instantiation part was not yet solved in this manner (e.g. FieldType plugins). Additionally, there is an important subsystem which has some differences but is crucially important to understand: plugins.

Plugins

The plugin system is a very important Drupal 8 component that powers a lot of functionality. So let's see how dependency injection works with plugin classes.

The most important difference in how injection is handled with plugins is the interface plugin classes need to implement: ContainerFactoryPluginInterface. The reason is that plugins are not resolved but are managed by a plugin manager. So when this manager needs to instantiate one of its plugins, it will do so using a factory. And usually, this factory is the ContainerFactory (or a similar variation of it). 

So if we look at ContainerFactory::createInstance(), we see that aside from the container being passed to the usual create() method, the $configuration, $plugin_id, and $plugin_definition variables are passed as well (which are the three basic parameters each plugin comes with).

So let's see two examples of such plugins that inject services. First, the core UserLoginBlock plugin (@Block):

As you can see, it implements the ContainerFactoryPluginInterface and the create() method receives those three extra parameters. These are then passed in the right order to the class constructor, and from the container a service is requested and passed as well. This is the most basic, yet commonly used, example of injecting services into plugin classes.

Another interesting example is the FileWidget plugin (@FieldWidget):

As you can see, the create() method receives the same parameters, but the class constructor expects extra ones that are specific to this plugin type. This is not a problem. They can usually be found inside the $configuration array of that particular plugin and passed from there.

So these are the main differences when it comes to injecting services into plugin classes. There's a different interface to implement and some extra parameters in the create() method.

Conclusion

As we've seen in this article, there are a number of ways we can get our hands on services in Drupal 8. Sometimes we have to statically request them. However, most of the time we shouldn't. And we've seen some typical examples of when and how we should inject them into our classes instead. We've also seen the two main interfaces the classes need to implement in order to be instantiated with the container and be ready for injection, as well as the difference between them.

If you are working in a class context and you are unsure of how to inject services, start looking at other classes of that type. If they are plugins, check if any of the parents implement the ContainerFactoryPluginInterface. If not, do it yourself for your class and make sure the constructor receives what it expects. Also check out the plugin manager class responsible and see what factory it uses. 

In other cases, such as with TypedData classes like the FieldType, take a look at other examples in core. If you see others using statically loaded services, it's most likely not yet ready for injection so you'll have to do the same. But keep an eye out, because this might change in the future.

Tags:

Comments

Related Articles