Skip to main content

Decorated services in Drupal 8

One of the aspects of the new object oriented architecture in Drupal 8 is the introduction of services. Services can be provided by any module and provide a mechanism for exposing reusable functionality by way of interfaces and classes. All services are instantiated via the container which is responsible for injecting a service’s dependencies.

Since services implement interfaces and are always instantiated via the container, we have the opportunity to alter what the container returns, ultimately allowing us to swap any existing service with a new one.

by sam.becker /

We have previously blogged about how we might go about about replacing and altering services. In a nut shell, it boils down to implementing ServiceModifierInterface. Here is an example about a fictional logger service, where instead of logging to a database we're required to send an email. Replacing the logger might look something like:

// Someone told me all the logs had to be emailed directly to the developer...
$definition = $container->getDefinition('logger');
$definition->setClass('Drupal\annoying_logger\DeveloperEmailLogger');

In this instance, the entire class has been replaced, leaving no trace of the original logger service to be found. The problem arises when someone finds out DeveloperEmailLogger has become ineffective because those pesky developers have been deleting their log emails as fast as they were landing in their inbox. The solution? Log to the database as well as sending an email. One approach to this problem might be to simply make DeveloperEmailLogger extend DatabaseLogger:

class DeveloperEmailLogger extends DatabaseLogger {
    public function log($message) {
        parent::log($message);
        mail('sam@pnx.me', 'Fix this!!!! ' . $message);
    }
}

In this instance the new logger is now building upon the functionality of the old logger and the brief has been met. The container now returns an instance of DeveloperEmailLogger, which in turn ensures DatabaseLogger has a chance to act.

There are a few problems and assumptions with this approach however. The first one is that DeveloperEmailLogger assumes that no other modules have decided they were also going to modify logger. By the time the annoying_logger module's ServiceModifier class is called it could be a completely different implementation provided by a different module which wanted to add it's own spin on database logging. Perhaps another module is collecting extra information about each log and compliments our own DeveloperEmailLogger nicely.

There are a few ways to deal with this situation. The first involves not extending DatabaseLogger and instead altering your ServiceModifier class to look something like:

$existing_logger = $container->getDefinition('logger');
$container->setDefinition('logger.existing', $existing_logger);

$email_logger = new Definition('Drupal\annoying_logger\DeveloperEmailLogger');
$email_logger->addArgument(new Reference('logger.existing'));

$container->setDefinition('logger', $email_logger);

Here we are instructing the container to decorate our new logger with the existing logger, so we aren’t making any assumptions about what might already be in the container. This is a helpful pattern because it allows any number of modules implementing ServiceModifierInterface to build upon each other's implementations.

As you may have noticed, there is a bit of setup required to implement this pattern. You need a service modifier and you have to manually shuffle around the arguments and the definition in the container. One feature in Drupal 8 (provided by symfony) you can use to simplify all of this is by using "decorates" in your service definition. For our logging example, it might look something like:

services:
  email_logger:
    class: Drupal\annoying_logger\DeveloperEmailLogger
    decorates: logger
    arguments: ['@email_logger.inner']

In this instance, email_logger.inner refers to the service which is being decorated and can be injected as an argument into our new email logger. When replacing services, it's important to implement the original interface and ensure method calls are also passed on to the original class, to ensure they have a chance to act. Currently this functionality is currently blocked behind a core issue, however manually decorating services using ServiceModifierInterface is still possible.

If you are providing some functionality which you suspect a lot of other modules will also need to act upon, you may consider looking at tagged services as an alternative to decorated services. Keep your eyes peeled for a follow-up blog post.