Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Services and dependency injection

A service is an object that does one job and is available from anywhere on the site: the database connection, the current user, sending mail. Your own logic is written as a service too — then the same code is used by a controller, a form, a block and a hook.

The service class

A service is an ordinary class in the src folder of the module. Let us write a reading time calculator for an article. Create the file by hand:

// web/modules/custom/blog/src/ReadingTime.php
namespace Drupal\blog;
 
class ReadingTime
{
    public function minutes(string $text): int
    {
        $words = preg_split('/\s+/u', trim(strip_tags($text)), -1, PREG_SPLIT_NO_EMPTY);
 
        return max(1, (int) ceil(count($words) / 200));
    }
}

The method counts the words in the text and divides by 200 — the number of words a person reads per minute. There is nothing from Drupal in the class yet.

Registering

To turn the class into a service, describe it in the file blog.services.yml in the root of the module folder. Create it by hand:

# web/modules/custom/blog/blog.services.yml
services:
  blog.reading_time:
    class: Drupal\blog\ReadingTime

blog.reading_time is the service name used to get it. After drush cr Drupal knows about the service and creates the object on first use, one for the whole request.

Getting it in a controller

A class does not create a service itself; it receives a ready object through the constructor. This is what dependency injection means. In a controller the static create() method serves this purpose:

// web/modules/custom/blog/src/Controller/BlogController.php
namespace Drupal\blog\Controller;
 
use Drupal\blog\ReadingTime;
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
 
class BlogController extends ControllerBase
{
    public function __construct(private ReadingTime $readingTime) {}
 
    public static function create(ContainerInterface $container): static
    {
        return new static($container->get('blog.reading_time'));
    }
 
    public function about(): array
    {
        $minutes = $this->readingTime->minutes('A blog about Drupal development.');
 
        return ['#markup' => $this->t('Reading time: @min min.', ['@min' => $minutes])];
    }
}

Drupal calls create() and passes in the container — the storage of all services. The method takes the ones it needs by name and hands them to the constructor. The /blog/about page shows "Reading time: 1 min.". Forms and blocks work the same way.

Getting it in a hook

Functions in the blog.module file have no constructor. There a service is taken through the \Drupal class:

// web/modules/custom/blog/blog.module
$minutes = \Drupal::service('blog.reading_time')->minutes($text);

A service with dependencies

A service may need other services too. They are listed in arguments; the @ sign means "the service with this name":

# web/modules/custom/blog/blog.services.yml
services:
  blog.reading_time:
    class: Drupal\blog\ReadingTime
    arguments: ['@config.factory']
// web/modules/custom/blog/src/ReadingTime.php
namespace Drupal\blog;
 
use Drupal\Core\Config\ConfigFactoryInterface;
 
class ReadingTime
{
    public function __construct(private ConfigFactoryInterface $configFactory) {}
 
    public function minutes(string $text): int
    {
        $speed = $this->configFactory->get('blog.settings')->get('words_per_minute') ?? 200;
        $words = preg_split('/\s+/u', trim(strip_tags($text)), -1, PREG_SPLIT_NO_EMPTY);
 
        return max(1, (int) ceil(count($words) / $speed));
    }
}

Now the reading speed comes from the module settings, and until they exist it equals 200. Settings are covered in the lesson about configuration. The order of the arguments in arguments matches the order in the constructor.

Frequent core services

NameWhat it gives
entity_type.managerLoading and saving entities
current_userThe current user
config.factorySettings
databaseThe database connection
messengerMessages to the visitor
logger.factoryThe log
http_clientRequests to other sites

Common mistake

Calls to \Drupal::service() inside classes — controllers, forms, your own services — hide dependencies: the constructor does not show what the class needs, and it cannot be created in a test. Keep static access for hook functions, and pass services through the constructor in classes. And remember: blog.services.yml is read only on drush cr.

Thanks for reading!

Donate to the site — we work hard for you.