Skip to content

Lazy Services

Edit this page

In some cases, you may need to inject a service that is heavy to instantiate but is not always used. For example, imagine a NewsletterSender service that injects a mailer service. Only a few methods of the NewsletterSender actually use the mailer, but the mailer service is always instantiated in order to construct the NewsletterSender.

Lazy services solve this problem. When a service is marked as lazy, the container injects a proxy of the service instead of the real service. The proxy looks and acts like the real service, except that the service is not instantiated until you interact with the proxy in some way.

The service container provides other features to delay the instantiation of services. Depending on your needs, one of them can be a better fit than lazy services:

  • Service closures inject a closure that creates and returns the service when calling it. Prefer them when your own code must control explicitly when (and whether) the service is created;
  • Service locators inject a set of services, and each of them is only created when you fetch it from the locator. Prefer them when your class needs occasional access to several services instead of one.

Note

When using PHP 8.4 or later, lazy services rely on native lazy objects, so final and readonly classes are fully supported.

On older PHP versions, lazy services do not support final or readonly classes, but you can restrict the proxy to an interface to work around this limitation.

Making a Service Lazy

Use the lazy option to mark a service as lazy:

1
2
3
4
5
6
7
8
9
10
11
// src/Twig/AppExtension.php
namespace App\Twig;

use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use Twig\Extension\ExtensionInterface;

#[Autoconfigure(lazy: true)]
class AppExtension implements ExtensionInterface
{
    // ...
}

In PHP classes, you can also use the #[Lazy] attribute, which is a shorter equivalent of #[Autoconfigure(lazy: true)]:

1
2
3
4
5
6
7
8
9
10
11
// src/Twig/AppExtension.php
namespace App\Twig;

use Symfony\Component\DependencyInjection\Attribute\Lazy;
use Twig\Extension\ExtensionInterface;

#[Lazy]
class AppExtension implements ExtensionInterface
{
    // ...
}

From now on, when injecting this service into other services or when getting it directly from the container, a lazy ghost object with the same signature as the service class is injected instead. A lazy ghost object is an object created empty which initializes itself (i.e. instantiates the real service) when you access it for the first time.

Making Only Specific Injections Lazy

Marking a service as lazy makes it lazy everywhere it's injected. If you prefer to keep it as a regular service, you can make it lazy only for some specific injections. To do so, add the #[Lazy] attribute to the argument where the service is injected:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/Service/MessageGenerator.php
namespace App\Service;

use Symfony\Component\DependencyInjection\Attribute\Lazy;
use Twig\Extension\ExtensionInterface;

class MessageGenerator
{
    public function __construct(
        #[Lazy]
        ExtensionInterface $extension,
    ) {
        // ...
    }
}

The lazy argument of the #[Autowire] attribute produces the same result. Use that attribute instead when you also need some of its other features, such as selecting the service to inject:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/Service/MessageGenerator.php
namespace App\Service;

use App\Twig\AppExtension;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Twig\Extension\ExtensionInterface;

class MessageGenerator
{
    public function __construct(
        #[Autowire(service: AppExtension::class, lazy: true)]
        ExtensionInterface $extension,
    ) {
        // ...
    }
}

If the argument uses a union or intersection type, pass the interface that the proxy must implement as the value of the attribute:

1
2
3
4
5
6
7
8
9
10
11
12
13
class MessageGenerator
{
    public function __construct(
        #[Lazy(FooInterface::class)]
        FooInterface|BarInterface $foo,

        // when using the #[Autowire] attribute, pass the interface in the lazy argument:
        // #[Autowire(service: 'foo', lazy: FooInterface::class)]
        // FooInterface|BarInterface $foo,
    ) {
        // ...
    }
}

Restricting the Proxy to an Interface

Note

This technique is rarely needed nowadays. If you are using PHP 8.4 or later, final and readonly classes are supported natively, so the only remaining use case is to restrict the methods that can be called on the proxy.

Internally, the proxies generated to lazily load services inherit from the class used by the service. However, sometimes this is not possible at all (e.g. because the class is final and can not be extended) or not convenient.

To work around this limitation, you can configure the proxy to only implement specific interfaces. In the following example, the AppExtension class implements two interfaces, but the proxy generated for it only implements ExtensionInterface:

1
2
3
4
5
6
7
8
9
10
11
12
// src/Twig/AppExtension.php
namespace App\Twig;

use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use Twig\Extension\ExtensionInterface;
use Twig\Extension\GlobalsInterface;

#[Autoconfigure(lazy: ExtensionInterface::class)]
class AppExtension implements ExtensionInterface, GlobalsInterface
{
    // ...
}

The virtual proxy injected into other services will only implement the specified interfaces and will not extend the original service class, allowing you to lazy load services using final classes. In the previous example, other services can call the ExtensionInterface methods on the proxy, but not the GlobalsInterface methods or any other public method of the AppExtension class. You can configure the proxy to implement multiple interfaces by adding new "proxy" tags.

Tip

This feature can also act as a safe guard: given that the proxy does not extend the original class, only the methods defined by the interface can be called, which prevents calling implementation-specific methods. It also prevents injecting the dependency at all if you type-hinted a concrete implementation instead of the interface.

This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.
TOC
    Version