Skip to main content

Deferred Resolution

Overview

The container does not build anything at binding time. Registering a service with bind(), singleton(), or array-access syntax only stores a factory — the concrete object is created the first time something actually asks for it, via make() or get(). This "lazy by default" behavior is what deferred resolution means in the DomainFlow Container: there is no separate service-provider or boot phase to opt into, because every binding already behaves this way.


How It Works

  • bind() stores a Closure (either the one you pass directly, or one the container generates for a class-string concrete). That closure is only invoked inside make(), when the abstract is actually requested.
  • singleton() behaves the same way, except the built instance is cached in $this->instances after the first resolution so every later call returns the same object without re-running the factory.
  • instance() is the one exception: it stores an already available value immediately, so there is nothing left to defer.

Because building a class-string concrete goes through reflection-based autowiring, even a deeply nested dependency graph is only walked when the outermost service is resolved — not when it's bound.


Deferring Expensive Work Explicitly

For a dependency that is expensive to construct (e.g. opens a network connection) but is only sometimes needed, bind a closure that wraps the expensive object behind a lazily-invoked factory instead of injecting the built object directly:

class ReportExporter {
public function __construct(
private Closure $connectionFactory
) {}

public function export(): void {
// The database connection is only opened here, the first time it's needed.
$connection = ($this->connectionFactory)();
$connection->query('...');
}
}

$container->bind(ReportExporter::class, function (Container $c) {
return new ReportExporter(fn () => $c->make(DatabaseConnection::class));
});

ReportExporter can now be resolved cheaply — DatabaseConnection is only built the moment export() actually calls the closure.


Example: Bindings Are Not Built Until Requested

class LazyService {
public function __construct() {
echo "LazyService constructed!\n";
}
}

$container = new Container();

// Registering the binding does not construct LazyService.
$container->bind(LazyService::class, LazyService::class);
echo "Binding registered.\n";

// The constructor only runs here, when the service is actually requested.
$lazyService = $container->get(LazyService::class);

Output:

Binding registered.
LazyService constructed!

Summary

Deferred resolution in the DomainFlow Container is a natural consequence of how bind() and singleton() work: they store a factory, and that factory only runs inside make()/get() when the service is actually requested. For dependencies that are expensive to construct and only conditionally needed, wrap them in a closure so their construction is deferred past the point where the consuming service itself is resolved.