matthiasnoback/lazy-services-bundle
A Symfony bundle for declaring lazy services that are only instantiated when first used. Lightweight proxies stand in for real services, cutting boot-time overhead and memory use in apps with heavy dependencies, without changing how services are wired or consumed.
Reality check first. This bundle predates — and largely duplicates — native Symfony lazy services. Before installing anything, try lazy: true on the service definition (Symfony ≥2.3) or #[Autowire(lazy: true)] on a constructor argument (Symfony ≥6.2). Install this bundle only if you are on an old Symfony (2.x/3.x) codebase and specifically need to make constructor arguments of services you don't own lazy. That is the one gap the bundle fills.
Minimal steps (legacy Symfony 2/3 project):
composer require matthiasnoback/lazy-services-bundle
// app/AppKernel.php
$bundles = [
// ...
new MatthiasNoback\LazyServicesBundle\LazyServicesBundle(),
];
# app/config/config.yml (key names per the bundle README)
lazy_services:
services:
- app.mailer # whole service becomes a proxy
arguments:
app.report_generator: # only this service's argument is proxied
- pdf_renderer
Then clear and warm the cache — proxies are generated at container compile time:
php app/console cache:clear --env=prod
Where to look first:
DependencyInjection/CompilerPass — it is the whole product; it rewrites Definition classes and/or wraps referenced arguments with generated ProxyManager value holders.app/cache/.../lazy_services or similar) — this tells you what actually got proxied.php app/console container:debug <service_id> to confirm the service exists and what its class originally was.First use case to prove it out: a controller or hot listener that injects a pdf_renderer/mailer/http_client which boots a subprocess or opens a socket in its constructor, but which is used on maybe 1 in 20 requests. Proxy it, hit a route that doesn't use it, and confirm (via XHProf/Blackfire or a var_dump in the real service's constructor) that the constructor no longer runs.
Proxy the heavy collaborator, not the hot service. The classic win is a cheap service (ReportGenerator, NotificationListener) that is instantiated on every request and holds an expensive dependency (PdfRenderer, Swift_Mailer, Doctrine\DBAL\Connection). Mark the dependency lazy; the cheap service still gets built normally.
Lazy arguments are the escape hatch for third-party definitions. You cannot edit Definition objects registered by another bundle, but the bundle's compiler pass runs on the finished container, so you can proxy an argument of a vendor service from your own config. This is also the cleanest fix for a circular dependency that would otherwise require setter injection:
lazy_services:
arguments:
app.a:
- app.b
Whole-service proxying for listeners and subscribers. Event listeners are constructed eagerly by EventDispatcher even if the event never fires. Proxying the listener class defers its constructor (and its own dependency graph) until handle() is actually called.
Console commands. Long-running commands that build a full stack of services on bin/console startup see a real wall-clock improvement from proxying the DB/HTTP layer.
Testing laziness explicitly. Don't trust the config. Add a test that asserts on construction:
class ReportGeneratorTest extends \PHPUnit_Framework_TestCase
{
public function testPdfRendererIsNotInstantiatedEagerly()
{
$kernel = new \AppKernel('test', true);
$kernel->boot();
$container = $kernel->getContainer();
// container must not have already built it
$this->assertFalse($container->get('app.pdf_renderer', Container::NULL_ON_INVALID_REFERENCE) instanceof PdfRenderer
&& PdfRenderer::$constructed);
}
}
A simpler production-side check: give the real service a public static $constructed = false; set to true in its constructor, and log it.
Migration posture. Treat this bundle as a stopgap. Every pattern above has a native equivalent today:
| Bundle pattern | Modern replacement |
|---|---|
lazy_services.services: [...] |
lazy: true on the definition |
lazy_services.arguments: [...] |
#[Autowire(lazy: true)] / lazy argument in services.yaml |
| ProxyManager value holders | Symfony's LazyServiceInstantiator + ProxyManagerBridge |
| Circular dep via lazy | Setter injection, service locator, or ServiceSubscriber |
If you inherit a project using it, write an ADR and schedule removal.
ProxyManager generates a subclass; final kills it. The same applies to classes with private constructors or static-only APIs.get_class() lies. Once proxied, get_class($service) returns a generated ProxyManagerGeneratedProxy\__PM__\... name. Code doing get_class($x) === 'App\Service\Foo' breaks. instanceof and type hints are fine because the proxy extends the real class.cache:clear (or cache:warmup on deploy). Symptoms of forgetting: your newly-marked service is still eager, or after a class rename you get "class not found" from a stale proxy file.beforeOptimization vs optimization).synthetic (e.g. request, doctrine.orm.entity_manager in some setups), proxying either silently no-ops or blows up at instantiation. Verify with container:debug --show-private.var_export'd config) will break on the proxy class.container:debug shows the original class, not the proxy. That mismatch is the number one source of "why isn't it lazy?" confusion. Cross-reference against the generated files in the cache directory.ContainerBuilder API; the generated proxies use ProxyManager patterns that are deprecation-noisy on PHP 8 (dynamic properties, return types) and will not run unmodified on Symfony 5+. Pin your PHP/Symfony versions or vendor a fork.ocramius/proxy-manager than trying to configure this bundle.How can I help you explore Laravel packages today?