Need some help with your project? Contact me

Lazy loaded services in Drupal 8

Inheriting from Symfony (in principle but not implementation), Drupal 8 allows us to define certain services as lazy. Why? Well why the hell not?!

Sometimes, our services get big. Not necessarily in the number of things they do (hopefully not) but in the time it takes for them to get instantiated. As you know, when we define a service and make it a dependency of something else, the service container will instantiate that service and inject it where it is needed. And this happens whether on that particular request that service is used or not.

For example, let’s imagine you have a Controller with 2 public methods used for 2 distinct routes. Most likely, when one method gets hit for the route, the logic of the second one doesn’t run. And even if only the second one depends on an injected service, the latter gets instantiated in both cases regardless.

Of course, for “popular” services like the EntityTypeManager or form builders this is not a big deal. For one, they are probably going to be instantiated anyway for other parts of the request. And second, they are not expensive to construct. Well, they probably are but anyway, see point 1. However, if we have our custom service as a dependency which is used only for that one route, it doesn’t make sense to have it instantiated for both routes. Especially if it is expensive to do so — heavy on resources. Enter lazy services.

Lazy services basically “tell” the container:

Ok, I need to be injected, sure, but unless I’m not used, please don’t construct me… mkay?

So how does this work in practice? Let’s see an example.

Assume this service:

namespace Drupal\module_name;

class MyHeavyService implements HeavyServiceInterface {

  /**
   * This be slow.
   */
  public function __construct() {
    sleep(4);
  }

  /**
   * Does something, doesn't matter what.
   */
  public function doSomething() {}
}

A few things to note here:

  • It’s important to have an interface. Without one, this won’t work. You’ll see in a moment why.
  • The constructor does, for some reason, take an expensive nap.
  • It’s not important what the API of the service does.

For such a service, the normal service definition would look like this:

module_name.heavy_service:
  class: Drupal\module_name\MyHeavyService

If we inject this into our Controller, any request which uses the latter will instantiate this service as well — which costs us 4 seconds a pop. So to make it lazy we just have this instead:

module_name.heavy_service:
  class: Drupal\module_name\MyHeavyService
  lazy: true

Lazy services work by way of proxy classes. Meaning that for each service that is declared lazy, the container expects a proxy class which is responsible for decorating the original one and only instantiate it if any of the public APIs are requested. But don’t worry, we don’t have to write another class. We have a PHP script provided by Drupal core that does this for us:

php core/scripts/generate-proxy-class.php 'Drupal\module_name\MyHeavyService' 'modules/custom/module_name/src'

The script takes two parameters:

  • The namespace of the service we want to create a proxy for
  • The location where it should be written

Do note that proxy classes are dumped automatically into a ProxyClass folder located at that specified path. So this is what gets generated for our service at modules/custom/module_name/src/ProxyClass/MyHeavyService.php:

// @codingStandardsIgnoreFile

/**
 * This file was generated via php core/scripts/generate-proxy-class.php 'Drupal\module_name\MyHeavyService' "modules/custom/module_name/src".
 */

namespace Drupal\module_name\ProxyClass {

    /**
     * Provides a proxy class for \Drupal\module_name\MyHeavyService.
     *
     * @see \Drupal\Component\ProxyBuilder
     */
    class MyHeavyService implements \Drupal\module_name\HeavyServiceInterface
    {

        use \Drupal\Core\DependencyInjection\DependencySerializationTrait;

        /**
         * The id of the original proxied service.
         *
         * @var string
         */
        protected $drupalProxyOriginalServiceId;

        /**
         * The real proxied service, after it was lazy loaded.
         *
         * @var \Drupal\module_name\MyHeavyService
         */
        protected $service;

        /**
         * The service container.
         *
         * @var \Symfony\Component\DependencyInjection\ContainerInterface
         */
        protected $container;

        /**
         * Constructs a ProxyClass Drupal proxy object.
         *
         * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
         *   The container.
         * @param string $drupal_proxy_original_service_id
         *   The service ID of the original service.
         */
        public function __construct(\Symfony\Component\DependencyInjection\ContainerInterface $container, $drupal_proxy_original_service_id)
        {
            $this->container = $container;
            $this->drupalProxyOriginalServiceId = $drupal_proxy_original_service_id;
        }

        /**
         * Lazy loads the real service from the container.
         *
         * @return object
         *   Returns the constructed real service.
         */
        protected function lazyLoadItself()
        {
            if (!isset($this->service)) {
                $this->service = $this->container->get($this->drupalProxyOriginalServiceId);
            }

            return $this->service;
        }

        /**
         * {@inheritdoc}
         */
        public function doSomething()
        {
            return $this->lazyLoadItself()->doSomething();
        }

    }

}

As you can see, we have a simple decorator. It implements the same interface and has the same public methods. The latter, however, are derived automatically from the service class and not the interface. And basically, the container is injected and used to instantiate the underlying service the first time any of the public methods are called. If none are called in that request, it won’t get instantiated.

I mentioned above that having an interface on the service is necessary. The reason is that when we inject it somewhere, we need to type hint the interface. Otherwise, the container would pass an instance of Drupal\module_name\ProxyClass\MyHeavyService which is not the same as the original Drupal\module_name\MyHeavyService.

So now, we can inject it, type hint it with the interface and it would only get instantiated if any of the public methods are called. Neat no?

The responsible for making all this happen is the Drupal\Core\DependencyInjection\Compiler\ProxyServicesPass compiler pass. Looking for service definitions that have been marked as lazy, it creates a new identical service definition (non-lazy) which uses the proxy class and adds that to the container instead. It’s actually not rocket science if you look at the code.

And like many things, just because we have this available, it doesn’t mean we should use it for every service we write. Remember, if you create services used all over the place, this is useless. The criteria for whether to make your service lazy should be:

  • Is it heavy to instantiate (depends on a bunch of other services which in turn are not super popular either)?
  • Is it ever instantiated for no reason?

Hope this helps.

Add new comment

You must have Javascript enabled to use this form.