Service Container Lazy Loading
Rhapsody includes a powerful lazy‑loading system for the service container. Instead of instantiating every service eagerly at the start of a request, the framework can defer the creation of services until they are actually used. This improves performance, especially in applications with many dependencies or heavy services that are not always needed.
How It Works
The lazy‑loading system is implemented via a proxy pattern. When you request a service from the container, the ContainerDecorator may return a proxy object instead of the real service. The proxy has the same interface as the target class, but it does not instantiate the real object until one of its methods is called.
Lazy Loading Flow
- Your code requests a service via
$container->get(SomeService::class) - The
ContainerDecoratorchecks if lazy loading is enabled and if the service is not marked as "eager" - A dynamic proxy class is generated (if not already cached) by
ProxyGenerator - The proxy is instantiated with an initializer closure that knows how to resolve the real service
- The proxy is returned to your code – the real service is not created yet
- When you call a method on the proxy, the proxy triggers the initializer, instantiates the real service, and forwards the call
This means that heavy services (e.g., those that connect to external APIs, load large datasets, or have complex constructors) will only be built if they are actually used during the request.
Configuration
Lazy loading is controlled via the config.php file under the lazy key, or via environment variables.
Master Toggle
// config.php
'lazy' => [
'enabled' => filter_var($_ENV['LAZY_LOADING_ENABLED'] ?? true, FILTER_VALIDATE_BOOLEAN),
// ...
]
You can disable lazy loading entirely by setting LAZY_LOADING_ENABLED=false in your .env file.
Eager Services
Some services should never be lazy-loaded – for example, those that must be fully initialised early, or that have side effects in their constructor. You can list them in the eager array:
'lazy' => [
'enabled' => true,
'eager' => [
\App\Services\PaymentGateway::class,
\App\Services\EmailDispatcher::class,
],
]
Controllers are always resolved eagerly because they are typically needed immediately and their constructors should not be deferred.
Example: Lazy vs Eager
Suppose you have a heavy service ReportGenerator that generates complex PDF reports. It is rarely used – only on a specific admin page.
// Service with heavy constructor
class ReportGenerator
{
public function __construct(
private PdfEngine $engine,
private Database $db,
private Logger $logger
) {
// This can be slow: connecting to PDF service, loading fonts, etc.
$this->engine->loadFonts();
$this->logger->info('ReportGenerator initialized');
}
public function generate(): string { ... }
}
Without lazy loading, ReportGenerator would be built on every request where it is injected (even if not used). With lazy loading, the service is only created when generate() is called.
// Controller that uses the service only in one method
class AdminController extends BaseController
{
public function __construct(
private ReportGenerator $reportGenerator // <- this is a proxy, not the real object
) {}
public function dashboard(Request $request): Response
{
// If this method returns early, the report generator is never instantiated
if (! $request->hasParam('generate')) {
return $this->view('admin/dashboard');
}
// Only now does the real ReportGenerator get built
$pdf = $this->reportGenerator->generate();
return $this->fileResponse($pdf);
}
}
The first request (without generate) will not construct ReportGenerator, saving time and resources. Only when the method is actually called does the proxy trigger initialisation.
Performance Benefits
- Faster request startup – fewer services are instantiated upfront.
- Lower memory usage – unused services are never created.
- Selective heavy lifting – expensive operations happen only when needed.
Limitations
- Final classes – cannot be proxied because proxy classes extend the target;
ProxyGeneratorthrows an exception if the class is final. - Constructors are not lazy – the real object is only created when a method is called; constructor logic is deferred until that point.
- Private methods – are not proxied, but that's fine because proxy only forwards public/protected calls.
- Static methods – are not supported for lazy loading (they would bypass the proxy).
When to Use Eager Loading
- Services that must be fully initialised early for correct operation (e.g., database connection, session handler).
- Services with side effects in their constructor that must happen immediately.
- Services that are used in almost every request (e.g., logging, configuration).
Advanced: Customising the Proxy Generation
Proxies are generated on‑the‑fly by ProxyGenerator and cached in storage/cache/proxies/. You can pre‑build proxies for all services using the CLI command:
php rhapsody build:proxies
This can be useful in production to avoid the overhead of generating proxies on the first request.
Conclusion
Service container lazy loading is a powerful optimisation technique that can significantly improve performance by deferring the creation of heavy services until they are actually needed. Combined with the caching system, it provides a robust toolkit for building high‑performance applications.
Note: Lazy loading is enabled by default. You can disable it globally via LAZY_LOADING_ENABLED=false in your .env file.