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.
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.
Note
On PHP versions older than 8.4, lazy services don't support final or
readonly classes unless you restrict the proxy to an interface.
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,
) {
// ...
}
}
Restricting the Proxy to an Interface
By default, the lazy object injected into other services has the same type as the service class. You can instead configure a proxy that only implements some specific interfaces. This is useful to:
- Prevent calling methods that don't belong to those interfaces;
- Make lazy an argument that uses a union type;
- Lazy load final or
readonlyclasses on PHP versions older than 8.4.
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 safeguard: 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.
To restrict the proxy only for some specific injections, pass the interface as
the value of the #[Lazy] attribute (or of the lazy argument of the
#[Autowire] attribute). This is required when the argument uses a union
type:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// src/Service/MessageGenerator.php
namespace App\Service;
use App\Formatter\HtmlFormatterInterface;
use App\Formatter\TextFormatterInterface;
use Symfony\Component\DependencyInjection\Attribute\Lazy;
class MessageGenerator
{
public function __construct(
// to autowire a union type, all its types must be aliases of the
// same service; the proxy only implements TextFormatterInterface
#[Lazy(TextFormatterInterface::class)]
TextFormatterInterface|HtmlFormatterInterface $formatter,
) {
// ...
}
}