Symfony2 lazy dependencies

Service dependencies injection

Symfony2 service container handles services in a kind of lazyness. A service is never registered until it is invoked. But what about service dependencies? Well, when a service is invoked for the first time, Symfony2 registers its service dependencies first (if they aren't yet) to inject them to the service itself (container service) and then register it.

Let's see an example:

Guess we have the following services defined in services.yml

services:  
    dependency:
        class: SaveTheCode\LazyDependenciesBundle\Service\DependencyService

    main:
        class: SaveTheCode\LazyDependenciesBundle\Service\MainService
        arguments:
            - "@dependency"

There are 2 different services, having dependency service (dependency) as an argument for main service (container). So the process to register main service when invoked is:

  1. Create and register main service dependencies
    • Service dependency in our case. So DependencyService class instance is created and registered in service container as a service called dependency
  2. Create main service and inject dependencies
    • Dependencies defined as service arguments are injected in service class constructor as parameters. In our case MainService constructor will have a parameter that is the service called dependency (DependencyService class instance)
  3. Register main service in service container having an instance of MainService class

Whenever a service is invoked for the first time, the service class instance is created and registered as well as its service dependencies

Let's take a look to service classes code

class MainService {  
    private $dependency;

    public function __construct(DependencyService $dependency) {
        $this->dependency = $dependency;
    }

    public function doSomething() { ... }

    public function doSomethingWithDependency() {
        ...
        $this->dependency->doWhatever();
        ...
    }
}

class DependencyService {  
    public function doWhatever() { ... }
}

As you can see, MainService class constructor has an argument type hinted as DependencyService (line 4), that is actually the service class defined as its argument in services.yml, and it's assigned to a private class property called dependency. MainService class has 2 more methods:

  • doSomething
  • doSomethingWithDependency

First method doSomething() doesn't call dependency service, but second method doSomethingWithDependency() calls dependency service method doWhatever() (line 12)

Whenever main service is invoked, Symfony2 will provide an instance of MainService class. As we said before, a dependency service must be registered (having an instance of DependencyService class) to be able to register main service in service container.

This doesn't look wrong and actually it isn't. Let's see a simple example in which main service is invoked:

class DefaultController extends Controller {  
    public function indexAction() {
        ...
        $this->container->get('main')->doSomething();
        ...
    }
}

Hey! wait a sec. indexAction() calls main service method doSomething() (line 4). But if you go back to MainService class, this method doesn't access its injected service dependency, so we don't need a DependencyService instance to run this code, however, it will exist because Symfony2 needs it to create MainService class instance and register main service.

Well, this is not wrong, remember that service dependencies must be registered in service container before registering a new service, but what happens if DependencyService instance creation takes a lot of time/resources and it's not invoked? Let's make the following assumption to clear up

class DependencyService {

    public function __construct() {
        /* guess low performance code here */
    }

    public function doWhatever() { ... }
}

Calling indexAction() we are wasting time/resources in DependencyService instance creation and we don't really need to because it's never invoked.

Lazy services

To let our service dependencies classes be created really when they are needed and not when registered in Symfony2 service container when injected to other services, we have lazy services.

To enable this feature, you have to follow 2 steps:

Include proxy-manager dependency

composer require ocramius/proxy-manager:~1.0  

Declare service dependency as lazy

services:  
    dependency:
        class: SaveTheCode\LazyDependenciesBundle\Service\DependencyService
        lazy: true

    main:
        class: SaveTheCode\LazyDependenciesBundle\Service\MainService
        arguments:
            - "@dependency"

Notice that dependency service is defined as lazy bacause this is the service dependency that we need to be registered delaying contructor call until needed

It's pointless to define a service lazy if it is never injected into other service, since a service class instance will be created in the very moment of invocation.

What happens in previous example when indexAction() is executed and main service method doSomething() is called?

Actually dependency service is registered in Symfony2 service container since it 's a dependency for main service, but dependency service has not an instance of DependencyService class but a proxy object pretending to be an instance of real class bypassing its creation (constructor call) until it is really needed.

Thus, lazy dependency service class constructor will not be called until any of its methods is called

Sample Application

Guess we have this service definition

services:  
    lazy_dependency:
        class: SaveTheCode\LazyDependenciesBundle\Service\DependencyService
        lazy: true

    main_with_lazy_dependency:
        class: SaveTheCode\LazyDependenciesBundle\Service\MainService
        arguments:
            - "@lazy_dependency"

    dependency:
        class: SaveTheCode\LazyDependenciesBundle\Service\DependencyService

    main:
        class: SaveTheCode\LazyDependenciesBundle\Service\MainService
        arguments:
            - "@dependency"

Guess we have this controller

class DefaultController extends Controller {  
    public function doSomethingLazyAction() {
        $this->container->get('main_with_lazy_dependency')
            ->doSomething();
    }

    public function doSomethingWithDependencyLazyAction() {
        $this->container->get('main_with_lazy_dependency')
            ->doSomethingWithDependency();
    }

    public function doSomethingAction() {
        $this->container->get('main')
            ->doSomething();
    }

    public function doSomethingWithDependencyAction() {
        $this->container->get('main')
            ->doSomethingWithDependency();
    }
}

Let's say that DependencyService instance creation takes a lot of memory and time. In terms of performace there would be 2 groups.

Best performance

DependencyService class instance is never created

  • doSomethingLazyAction

Worst performance

DependencyService class instance is always created. It's needed in doSomethingWithOtherServiceLazyAction and doSomethingWithOtherServiceAction, but not needed in doSomethingAction and still created

  • doSomethingAction
  • doSomethingWithDependencyLazyAction
  • doSomethingWithDependencyAction

Source Code

You can find source code related to this article in GitHub
Source Code

You can find tests here:
Tests Source Code

Run tests:

phpunit -c app  

*Symfony2 takes time to generate cache files when running first time, so run tests at least 2 times to see real results