Lee RowlandsSenior Developer
Overriding services in Drupal 8 - advanced cases
Drupal 8 comes with a services based architecture allowing clean dependency injection, separation of concerns and another way to modify how Drupal works without hacking core
You've probably heard that Drupal 8 lets you swap out a core service for your own implementation, hey, I even said it myself here and here, but how do you achieve that?
Read on to find out how to manipulate Drupal 8 services at run-time and how this compares to other popular PHP Frameworks like Laravel, Silex and Symfony
Services based architecture
Drupal 8 comes with a service-based architecture. Put simply, services are objects that are responsible for representing operations that cannot be modelled as value-objects or entities. They are responsible for providing infrastructure concerns, operating on domain objects (value-objects and entities) and executing operations on domain objects.
Services in Drupal 8 are managed and instantiated using the dependency injection container, also known as the service container.
The service container is an implementation of the Inversion of Control design pattern. Its primary role is to build services on behalf of client services, to decouple the clients from the burden of knowing how the dependent service is constructed.
In Drupal 8, the primary registration of services is done via YAML based configuration, in the form of core and each enabled module's services.yml file.
An example if you will
Lets consider the forum index storage service as an example. This service is responsible for managing the association between forum posts (nodes) and forums (taxonomy terms) in an optimized format to allow forum module to operate in the most efficient manner. It implements \Drupal\forum\ForumIndexStorageInterface. Everywhere the forum index storage service is required, it is typehinted using the interface. This complies with the Liskov substitution principle and allows an alternate implementation of ForumIndexStorageInterface to be substitued. The default forum index storage service uses the database connection to read and write to the {forum_index} table. Lets assume you're working on a client-project that needs forum module, but for performance sake you want to store forum posts in a No-SQL database like MongoDB or perhaps the managed DynamoDb service from Amazon AWS.
Modifying services and the container
Tim Milwood gave a static example of how you might edit a service in the container, but there are advanced use cases that it does not handle - what if you want to change the arguments - or what if you need to perform dynamic modifications - i.e. only if a particular module is installed, or based on some other state of the container. Consider again the forum index storage example. The default service definition looks like this:
forum.index_storage:
class: Drupal\forum\ForumIndexStorage
arguments: ['@database', '@forum_manager']
tags:
- { name: backend_overridable }
But in our example, we don't want to inject the database. Lets assume we're using DynamoDb and we have a Dynamo client service which we need injected instead. The alias approach doesn't allow us to do this.
This is where \Drupal\Core\DependencyInjection\ServiceModifierInterface and the \Drupal\Core\DependencyInjection\ServiceProviderInterface come in. To implement this interface we need to first give our class a magic name. This is one of the few non-hook places in core where convention prevails over configuration, but for those familiar with hooks magic names are nothing new.
Start by taking your module name and converting it into camel case, then add the ServiceProvider suffix. For example if our module for the Dynamo Db implementation of forum index storage has a machine name of forum_dynamo, we'd need a fully qualified class name of \Drupal\forum_dynamo\ForumDynamoServiceProvider. You can also extend from \Drupal\Core\DepedencyInjection\ServiceProviderBase if you like, which already implements the interfaces for you. Lets look at the two possible interfaces in detail.
ServiceProviderInterface
This interface is for when your module needs to register services in the service-container in a dynamic fashion. YAML based service configuration is great, and very easy to parse - but what if you need to alter your service definition based on another condition in the environment - such as which modules are enabled. This is where ServiceProviderInterface comes in. It consists of a single method - register as follows:
interface ServiceProviderInterface {
/**
* Registers services to the container.
*
* @param ContainerBuilder $container
* The ContainerBuilder to register services to.
*/
public function register(ContainerBuilder $container);
}
The register method is called during container building, before dumping to disk. From here we can interact with other services. If you need to check which modules are enabled you can use the getParameter method on the container builder argument like so:
$modules = $container->getParameter('container.modules');
A great example of this in core is Drupal\language\LanguageServiceProvider, which is responsible for registering the language_request_subscriber and path_processor_language services, only if the site is multi-lingual. Clearly this couldn't be done in YAML alone.
ServiceModifierInterface
This interface is the one we want for modifying existing service definitions. Heading back to our example, we want to change the forum.index_storage service.
It consists of a single method as follows:
interface ServiceModifierInterface {
/**
* Modifies existing service definitions.
*
* @param ContainerBuilder $container
* The ContainerBuilder whose service definitions can be altered.
*/
public function alter(ContainerBuilder $container);
}
So we start by getting a reference to the forum.index_storage definition like so
$definition = $container->getDefinition('forum.index_storage');
Then we need to change the arguments like so - assuming that the dynamo client has machine name 'dynamo.client':
$definition->setArguments([
new Symfony\Component\DependencyInjection\Reference('dynamo.client'),
]);
Again Drupal\language\LanguageServiceProvider provides a great example of this in core, changing the default language manager provided in core to the configurable one provided by the language module, configuring the language config factory override service and setting a container parameter for the default languages.
Parallels with other PHP frameworks
Now what Drupal does in this space, registering and altering is not-dissimilar to comparable approaches in other PHP frameworks such as Symfony Full-stack framework, Laravel 5 and Silex.
Symfony
Symfony bundles can define an extension, in this case the extension is the same name as the bundle, except the 'Bundle' suffix is replaced with 'Extension'. The extension class needs to implement Symfony\Component\DependencyInjection\Extension\ExtensionInterface, but normally would just extend from Symfony\Component\DependencyInjection\Extension\Extension. This has a load method which is analagous to Drupals' register method.
Silex
In Silex, services are provided by service providers. Because Silex is geared towards bespoke applications rather than generic portable code each service provider is manually registered in the application using $app->register(). In this case a service provider is a class implementing Silex\ServiceProviderInterface which consists of a boot and a register method. The register method is analagous to Drupal's ServiceProviderInterface. The boot method serves the same use-case as the alter method in Drupal's ServiceModifierInterface.
Laravel
Like Silex, Laravel is also concerned with bespoke applications so service provider registration is done manually in your config/app.php. Service providers extend from Illuminate\Support\ServiceProvider. Just like Silex there are boot and register methods, and both serve the same use case as Drupal's alter and register methods respectively.
Conclusion
Being able to modify services adds a whole-new level of customisation to Drupal, but as seen by the similarities to other framworks, one which should feel familiar to developers coming from other PHP frameworks to Drupal. I think as time goes by modifying services will become one of the essential skills in a Drupal developer's bag of tricks.
If you want to hear more about how Drupal 8 will be game-changing, we're running a series of 'Get Ready for Drupal 8' seminars in many Australian capital cities on Thursday August 6th 2015. These are free to attend - but registration in advance is required. Hope to see you there.