More Complex Services Using Factories in Drupal 8
The Symfony service container that Drupal 8 ships with allows us to define a large number of services (dependency objects) that we can inject in our controllers, forms, plugins, other services, etc. If you don't know about dependency injection yet, you can read more about it here. In this article we will look at how we can use our own factory class to instantiate a service via the Symfony - Drupal 8 service container.
The typical (barebones) service definition consists of a class name to be instantiated and an array of arguments to be passed to its constructor as it gets created (other service definitions or static parameters). For more information, check out the documentation on services.
In some cases, though, we would like our service to be built dynamically based on certain contextual conditions, such as the current user. The implication is also that we don’t rely on the service container for the actual object instantiation, but our own factory class. We do still want to benefit from most of what the container offers us, such as caching.
Let’s see a very simple example. Imagine a UserContextInterface
which can have multiple implementations. These implementations depend on some value on the current user account (such as role for instance). And we want to have a service we can inject into our other objects which implements this interface but which is also the representation of the current user. Meaning it is an implementation specific to it (not always the same class).
We can go about achieving this in two ways:
- We can have a Factory class we define as a simple service (with the current user as an argument), use this as our dependency and then always ask it to give us the correct
UserContextInterface
. - We can have a Factory class we define as a service (with the current user as an argument) but use it in the definition of another service as a factory and rely on the container for asking it for the
UserContextInterface
.
The first option is pretty self-explanatory and not necessary in our case. Why should we keep asking the user context at runtime (the process to determine the context can be quite complex) when we can have that cached for the duration of the request. So let’s instead see how the second option would work:
my_module.user_context_factory:
class: Drupal\my_module\UserContextFactory
arguments: ['@current_user']
my_module.user_context:
class: Drupal\my_module\UserContextFactory
factory: 'my_module.user_context_factory:getUserContext'
So these would be our service definitions. We have the factory which takes the current user as an argument, and the user context service which we will be injecting as our dependency wherever we need. The latter uses our factory’s getUserContext()
method to return the relevant UserContextInterface
implementation. It is not so important what class we set on this latter service because the resulting object will always be the result of the factory.
The rest is boilerplate and we won’t be going into it. However, what needs to happen next is create our UserContextFactory
class which takes in the AccountProxyInterface
representing the current user and which implements the getUserContext()
method tasked with building the UserContextInterface
implementation. The latter method is not bound to any return type by the service per se, however, we must ensure that we return a UserContextInterface
in every case to preserve the integrity of our application. One good practice to ensure this is creating a UserContextNone
implementation of UserContextInterface
which would be returned by the factory in those edge cases when the context cannot be determined or values are missing, etc.
So that is pretty much it on how and why you would or can use a factory instantiation of services from your container. There is nothing new here, in fact the Symfony documentation has an entry specifically about this. However, I believe it’s a neat little trick we should all be aware of.