Need some help with your project? Contact me

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.

Comments

Hi -- Great article. But what "example" service is this providing? I haven't done much D8 work but is this like making a set of routes available to a limited set of users by Role, or is the Service a particular implementation of (I dunno) a data repository that should be only be visible to certain roles ?

Can you give a slightly more concrete example of what the possible Service could be, either in your business paradigm or a layer of your application ...

Hey,

Sorry but this is a bit of a more advanced topic on services in Drupal 8 (Symfony). So if you don't yet have experience with basic service definition and implementation, feel free to read some more about that.

As for an example of service here, the article describes it (if not actually shows you all the code). Based on the explanation it should be pretty straightforward to write the factory and user context implementations.

Thanks for the article. I'm not sure, if I got your example right, because it seems to be inconsistent with example given in https://api.drupal.org/api/drupal/core%21core.api.php/group/container/8.2.x. Shouldn't your example be:

my_module.user_context_factory:
    class: Drupal\my_module\UserContextFactory
    arguments: ['@current_user']
my_module.user_context:
    class:  Drupal\my_module\UserContextInterface
    factory: 'my_module.user_context_factory:getUserContext'

In my understanding, this should create an object, which is an instance of UserContextInterface.

The only difference I see is that for the my_module.user_context service you used UserContextInterface as the class. Which is fine. However, it is not mandatory. I used the factory class again, but it doesn't change anything. The service will return different things, can even return a string (if we let it) as the container cannot enforce anything. If it works the way you suggest, all the better.

'The rest is boilerplate and we won't go into it' lol.

This reminds of the how to draw an owl instructable.
1. Draw two circles
2. Draw the rest of the owl.

Add new comment

You can post comments in Markdown and basic HTML tags.
For code blocks, wrap your code within '~~~'. For example:
~~~
$var = 'my variable';
~~~