Pre-requisites
Dependency injection inverts how you create helper objects
When we're writing some method on some object, and we need some helper object, to do work for the current object, the naive way of creating the helper is:
class MyClass { public function doSomething() { $helper = new Helper(); } }
However, it's now very hard to test doSomething()
without also having your test depend on the correct behaviour of Helper
: you can't drop a "pretend" or mock helper object in. This complicates tests in all sorts of ways. It also means that, if you were to come up with an AdvancedHelper
in the future, you couldn't make use of it in your code: Helper
is hardwired, leading to strong coupling between MyClass
and Helper
.
The general solution to this and other coupling problems is dependency injection:
Dependency Injection (DI) is a way of abolishing the
new
keyword from your code (in all except factory and repository objects, which are ntended for the creation of new objects.)
For more background, you can read my other blogposts on DI. For now, this tutorial assumes familiarity with the general concept.
How services are registered in Drupal
Services in Drupal are strongly Symphony, and work in almost the same way. Every module modulename
can have a modulename.services.yml
file in its top-level folder, and these define which service names map to which classes, and what other options the service might need to be successfully created.
A services can be an object of any class: it doesn't have to implement any interface, or extend any abstract class. For example, create the following file in the d8api
module at src/ExampleService.php
:
<?php namespace Drupal\d8api; class ExampleService { }
Then add the following to the d8api.services.yml
file in the top-level folder of the module:
services: d8api.example: class: 'Drupal\d8api\ExampleService'
If the services:
key already exists (you might have done these tutorials in a different order!) don't add another one: just add the d8api.example:
key and what follows. But make sure you get the indentation right: only services:
is unindented.
You could now access the service using:
$exampleService = \Drupal::service('d8api.example');
anywhere in your code. But DI is more subtle than that, as we'll see.
Injecting services
The method for injecting a service into something else depends on what you want the something else is.
Inject one service into another
Injecting services that Drupal knows about, into others Drupal knows about, couldn't be simpler! Alongside your existing ExampleService
class file, add the following as src/ExampleServiceInjected.php
:
<?php namespace Drupal\d8api; /** * An example service, with another service injected. */ class ExampleServiceInjected { /** * @var ExampleService */ protected $exampleService; /** * Implements __construct(). * * @param ExampleService $exampleService * The example service, injected. */ public function __construct(ExampleService $exampleService) { $this->exampleService = $exampleService; } /** * Retrieve the class name of the stored example service. * * @return string * Name of the class. */ public function retrieveExampleServiceClass() { return get_class($this->exampleService); } }
This might not look simple, but it's what you'd have to write anyway in order to bring in a helper service: the DI part is yet to come. You can see this class has an object __construct()
method, which takes an object of class ExampleService
and stores it in a protected object variable. The class name of this injected object can then be returned by another method, as a test that injection has worked.
How do we now perform good DI of ExampleService
into our new ExampleServiceInjected
? Well, we add another entry into d8api.services.yml
to instantiate this new service, but using arguments for its constructor:
services: # ... (unchanged) ... d8api.example_injected: class: 'Drupal\d8api\ExampleServiceInjected' arguments: ['@d8api.example']
The @
symbol in the argument indicates that it's not simply the plaintext string "d8api.example", but the actual service with that name should be provided instead. In this way, the ExampleService
is instantiated and injected into ExampleServiceInjected
, whenever the latter is itself retrieved. No more work is necessary.
Inject services into other code
There are two ways of injecting services into other code:
- "Most" objects, like forms and page callbacks, can declare they implement
ContainerInjectionInterface
. - Plugins (which we'll discuss later) need to declare a different interface,
ContainerFactoryPluginInterface
.
Whatever interface you declare your object implements, you must then add the class method create()
to your class, and implement a __construct()
which takes the new DI arguments you want. This differs slightly depending on the interface;
ContainerInjectionInterface
is very straightforward:
<?php use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Symfony\Component\DependencyInjection\ContainerInterface; class MyThing implements ContainerInjectionInterface { protected $exampleService; public static function create(ContainerInterface $container) { return new static($container->get('d8api.example')); } public function __construct(ExampleService $exampleService) { // If you're extending some other class, call its constructor. parent::__construct(); $this->exampleService = $exampleService; } }
ContainerFactoryPluginInterface
is more complex, because the plugin configuration needs to be passed along:
<?php use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Symfony\Component\DependencyInjection\ContainerInterface; class MyPlugin extends SomePluginClass implements ContainerFactoryPluginInterface { protected $exampleService; public static function create( ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition ) { return new static( $configuration, $plugin_id, $plugin_definition, $container->get('d8api.example') ); } public function __construct( array $configuration, $plugin_id, $plugin_definition, ExampleService $exampleService ) { // If you're extending a core plugin class, call its constructor. parent::__construct($configuration, $plugin_id, $plugin_definition); $this->exampleService = $exampleService; } }
Later, when we discuss plugins in more depth, we'll see examples of both of these.
Even though there's a lot of code here (and we've omitted method and variable comments for brevity, which you shouldn't ever do!) it's mostly boilerplate, and only ExampleService
, $exampleService
, d8api.example
etc. are the bits that matter. It's true that in theory, you are calling \Drupal::service(...)
here in your own code, through $container->get(...)
; in practice, the create()
code pattern strictly limits the possible side-effects of doing so. As a rule: class methods can safely access $container
; but they should never pass $container
to the object for its own use later on.
What you should see
If you've edited the d8api.services.yml
file, you should rebuild caches. In order to test this code, you should also install Drush. We don't cover either process here.
With Drush installed, open a terminal and change the directory so you're inside the codebase for your Drupal site. You can then type the following:
drush php-eval 'print get_class(\Drupal::service("d8api.example"));'
The response will be:
Drupal\d8api\ExampleService
In this example, Drupal-as-service-container looks up the service, finds and loads the class, instantiates the object and passes it to get_class()
. This then prints the class name to the terminal window.
To show that ExampleServiceInjected
has been registered correctly as a service, you can run the same code but with the key for the injected service:
drush php-eval 'print get_class(\Drupal::service("d8api.example_injected"));' # Output: Drupal\d8api\ExampleServiceInjected
However, you can go one stage further and run the following:
drush php-eval 'print \Drupal::service("d8api.example_injected")->retrieveExampleServiceClass();' # Output: Drupal\d8api\ExampleService
This output shows that ExampleService
has been successfully instantiated and injected into ExampleServiceInjected
, and the latter returned by the container.
If you can see all this then congratulations! you have registered two services with Drupal, and successfully injected one into the other.