Services and Dependency Injection

While learning Drupal 8, I usually use Google to find tips and snippets of code to help my brain work out what the hell it's doing. The problem is, a lot of code posted around the interweb is usually out-of-date, depreciated, or just the wrong way of doing things. I keep hearing things like 'services', 'service container', and 'dependency injection' and you apparently you need to have those in mind when coding or you're just doing it wrong. So I explore what those are and what are the best Drupal coding practices when writing coding within classes.

Services

Services is the concept that keeps code decoupled and easy to read. A service is an object that performs a global task, by grouping related functionality, and is used globally by the application. It is a class that provides a single specific functionality that can be accessed when you need it. In Drupal 8, everything is done with services. So if you were to write a whole bunch of related functionality, you would do so in a class, then register that class with service container.

Service Container

Service Container is an object that contains references to all the services in an application and manages the instantiation of services. The Drupal 8 service container is a class containing an array of all service references. You can find a list of services via the Drupal Console:

drupal container:debug

The service container keeps track of what a service needs before getting instantiated. The service container is used to inject services into Drupal 8 classes.

Dependency Injection

So services are objects that that perform tasks in Drupal and the service container references all those available services, so what is dependency injection? Dependency Injection is a way of providing these services to a class or controller in Drupal 8, for example passing an object to the class constructor, otherwise if you instantiate an object or service directly within your class or controller, your code then depends on that object or service being available at the time of execution - resulting in the code becoming tightly coupled. Also, it can make it harder for other coders to extend the class and unit testing becomes more difficult. I found this video explains dependency injection quite well.

Drupal global class

The global Drupal class provides static methods to access some of the most common services but the best practice is to use injected services instead. I repeat, DO NOT get a service using the static Drupal class within your classes - if you do, kittens all over the world will die. Apparently, this is there to be used in the procedural code that still exists in Drupal such as the code we put into the .module file.

The wrong way

So with the above in mind, I want to show an example of the way of using a service in Drupal, but in a way that does not use dependency injection and therefore is the wrong way of doing it. Then I will show an example that does use dependency injection and is the correct way of using a service within a class. So here we are trying to get the path alias of a node and display it on a page:

<?php

namespace Drupal\module_name\Controller;

use Drupal\Core\Controller\ControllerBase;

/**
 * Class DummyController.
 *
 * @package Drupalmodule_name\Controller
 */
class DummyController extends ControllerBase {

  /**
   * Constructs front page content.
   */
  public function dummy() {
    $path_alias = \Drupal::service('path.alias_manager')->getAliasByPath('/node/1');
    return [
      '#type' => 'markup',
      '#markup' => $path_alias,
    ];
  }

}

This is incorrect because doing \Drupal::service('path.alias_manager')->getAliasByPath('node/1')is the static way of using a service. Not only does your class depend on the service, it needs to know the name of the service.

The correct way

<?php

namespace Drupal\module_name\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Path\AliasManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Class DummyController.
 *
 * @package Drupal\module_name\Controller
 */
class DummyController extends ControllerBase {

  /**
   * The path alias manager.
   *
   * @var \Drupal\Core\Path\AliasManagerInterface
   */
  protected $aliasManager;

  /**
   * Class constructor.
   */
  public function __construct(AliasManagerInterface $alias_manager) {
    $this->aliasManager = $alias_manager;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('path.alias_manager')
    );
  }

  /**
   * Constructs front page content.
   */
  public function dummy() {
    $path_alias = $this->aliasManager->getAliasByPath('/node/1');
    return [
      '#type' => 'markup',
      '#markup' => $path_alias,
    ];
  }

}

When we create a custom page in Drupal 8, we create a class that extends ControllerBase, and ControllerBase is a class that implements ContainerInjectionInterface. If you take a look at ContainerInjectionInterface.php, it contains a method called create with the comment:

This is a factory method that returns a new instance of this class. The factory should pass any needed dependencies into the constructor of this class, but not the container itself.

ContainerInjectionInterface is used as a part of a static factory method pattern. Doing it this way, a class's constructor can specify all of the services it needs, and its factory method, in this case create(), can satisfy the constructor by retrieving services from the container.

So in our correct example above, this is what's happening. We have a create method that is injecting the service we need into our controller class constructor method. So now we can use the service like this $this->aliasManager->getAliasByPath('/node/1') and this way is using dependency injection.

Tip

A good way of finding the service you may need for dependency injection is via Drupal Console on the command line. Just do the following:

drupal container:debug

This outputs a list of Drupal service ID's and its corresponding namespace & class. You can search for a specific class name or service ID by doing the following:

drupal container:debug | ag 'DatabaseStorage'