Lazy Services
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.