Extra Block Types (EBT) - New Layout Builder experience❗

Extra Block Types (EBT) - styled, customizable block types: Slideshows, Tabs, Cards, Accordions and many others. Built-in settings for background, DOM Box, javascript plugins. Experience the future of layout building today.

Demo EBT modules Download EBT modules

❗Extra Paragraph Types (EPT) - New Paragraphs experience

Extra Paragraph Types (EPT) - analogical paragraph based set of modules.

Demo EPT modules Download EPT modules

Scroll

9.15. Services and Dependency Injection.

08/03/2021, by Ivan

In Drupal we use hooks and services in custom modules from core and contrib modules, when we need to extend site functionality. We already used hooks in this article:

9.11.3. Entity hooks

Let's work with services now. Service is a special PHP object. When you create a new PHP class in your custom module, it will be better to define it as a service, so your code could be easily usable and overwritten in other modules.

Drupal collects all services in PHP object called Service Container, that Drupal stores information about all accessible and used services in one place. You can call this object and see all used services on your site:

<?php
$container = \Drupal::getContainer();
?>

https://api.drupal.org/api/drupal/core!lib!Drupal.php/function/Drupal%3A%3AgetContainer/9.2.x

Get container

Service container

You also can work with $container object with has/get/set methods, but usually we will add services in Services container using *.services.yml files in our modules.

Let's have a look on getContainer() method implementation:

<?php
public static function getContainer() {
  if (static::$container === NULL) {
    throw new ContainerNotInitializedException('\\Drupal::$container is not initialized yet. \\Drupal::setContainer() must be called with a real container.');
  }
  return static::$container;
}
?>

Variable Service Container is defined as static one, it means after index.php call and to the end of Drupal response we can get this variable $container in different places. It could be any PHP class, hook in module or even *.theme file.

How to use services in Drupal?

Let's start to use Service Container in Drupal. $container object stores services objects, it allows to execute all needed actions for service objects in constructor and pass ready to use object in our custom module. For example, we need to write an SQL query to database and we just call an object from Service Container to work with database. This object will use credentials for database connection from our settings.php file and will connect to MySQL for execution our SQL query:

$query = \Drupal::database()->select('node_field_data', 'n');
$query->addField('n', 'nid');
$query->condition('n.title', 'About Us');
$query->range(0, 1);
$nid = $query->execute()->fetchField();

If you open implementation of database() method, then you will see, object of database service is being called from Service Container:

https://api.drupal.org/api/drupal/core%21lib%21Drupal.php/function/Drupal%3A%3Adatabase/9.2.x

<?php
public static function database() {
  return static::getContainer()
    ->get('database');
}
?>

Thereby we include only needed classes for our custom code, which we will use in this moment. For this we are using unified, single storage of objects which called Service Container.

How to add service in Service Container?

When we create file *.services.yml, Drupal loads services from these files, initiates objects for these services from the files, and saves objects in Services Container.

https://api.drupal.org/api/drupal/core%21modules%21syslog%21syslog.services.yml/9.2.x 

core/modules/syslog/syslog.services.yml:

services:
  logger.syslog:
    class: Drupal\syslog\Logger\SysLog
    arguments: ['@config.factory', '@logger.log_message_parser']
    tags:
      - { name: logger }

In variable $container it's also possible to add services object with method set(), but usually this approach is using to mock dependencies in tests:

https://www.drupal.org/docs/automated-testing/phpunit-in-drupal/mocking-entities-and-services-with-phpunit-and-mocks

What is Dependency Injection?

If you run Code Sniffer, then it will return an errors,  Drupal::database() needs to be rewritten and call database in class constructor, in which we use object database from Service Container. When you call an object from Service Container in class constructor it's named Dependency Injection (DI), for example:

<?php

namespace Drupal\wisenet_connect\Form;

use Drupal\Core\Database\Connection;

/**
 * Implements the WisenetConfigurationForm form controller.
 *
 * This example demonstrates a simple form with a singe text input element. We
 * extend FormBase which is the simplest form base class used in Drupal.
 *
 * @see \Drupal\Core\Form\FormBase
 */
class WisenetGetCourseForm extends FormBase {

  /**
   * Active database connection.
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected $database;


  /**
   * Constructs a WisenetGetCourseForm object.
   *
   * @param \Drupal\Core\Database\Connection $database
   *   The database connection to be used.
   */
  public function __construct(Connection $database) {
    $this->database = $database;
  }

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

 ...

  /**
   * Implements course save handler.
   *
   * Function for save course data in course content type.
   */
  public function saveCourse($courses) {
     ...
      $query = $this->database->select('node__field_course_wisenet_id', 'nc');
      $query->addField('n', 'nid');
      $query->join('node_field_data', 'n', 'nc.entity_id = n.nid');
      $query->condition('nc.field_course_wisenet_id_value', $course['CourseOfferId']);
      $query->range(0, 1);

      $nid = $query->execute()->fetchField();
     ...
  }

In this example query to database is needed for the form, so we added method create(), which used for creating instance of class. create() method can be in different classes and interfaces, but it always has parameter with variable $container of ContainerInterface type. If method create() calls objects from Service Container $container->get('myservice.name'), than returned object will be passed in constructor __contruct() as and argument of the method ( in our case $container->get('database') and argument Connection $database).

About how to call object from Service Container in constructor of  Controller, Block, BaseForm, ConfigForm and custom class we will have a look in the next articles.

After that we will figure out how to include and use objects from  Service Container properly, we have a look how to create own services.

We will also look at how to override classes for services in order to use a class from a custom module instead of a contrib module class.

Why do we need Service container and Dependency Injection?

We can use namespaces and connect the code from modules directly, call the creation of objects for third-party classes in the place where we need without Service Container. But this causes problems with updating the code. For example, we have to replace the class for sending email messages and this class is called in 200 different places. For the convenience of updating the code, we create a Service, and not connect files directly. Now, when we want to send emails via smtp, and not via PHP mail (), then we will simply change the class for the service, and will not change the path to the new class in 200 places.

Dependency Injection solves the problem of double calling a service in the same class. We don't need to call the Service Container twice if we use the service in different methods of the same class. We simply write the service object into a property of our class and use the service already from the class property using $this->serviceName.

Of course, we can do without Service Container and Dependency Injection, but these patterns unify our code and allow us to simplify it and the ability to update it.

Where I can see name of service?

In our example we have 'database' service:

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      // Here we add service name.
      $container->get('database'),
    );
  }

but if you add a service from contrib/custom module it could have this name:

module_name.service_name

You can check service name in *.services.yml file, it's not required, that service ID will start with module_name.*, but usually it is. For example:

/**
 * {@inheritdoc}
 */
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
  return new static(
    $configuration,
    $plugin_id,
    $plugin_definition,
    $container->get('commerce_cart.cart_provider'),
    $container->get('entity_type.manager')
  );
}