Skip to main content

Lazy services

A lazy service costs nothing until it is used. The container hands out a lazy object of the class, and resolves the constructor's dependencies and calls the constructor the first time a property is read or a method is called. A request that never touches the database connection never opens it.

Make a class lazy​

#[Lazy] on a class, or lazy() for a class you cannot change, makes the container build it lazily:

use Dirthara\Container\Attribute\Lazy;

#[Lazy]
final class ReportGenerator
{
public function __construct(
private Connection $connection,
) {}
}

$container->lazy(PdoConnection::class);

$generator = $container->get(ReportGenerator::class); // nothing is built yet
$generator->generate(); // builds the Connection, then the ReportGenerator

The lazy object is an instance of the class itself, so it passes every type check. Everything else about the entry stays the same: a singleton is still built once, the parameters given to make() still reach the constructor, and contextual bindings still apply, when the object is initialized.

Parameter of lazy()TypeMeaning
classclass-stringAn instantiable class. Anything else throws an InvalidRegistrationException::notALazyClass().

When a class is lazy​

Laziness applies whenever the container builds the class by calling its constructor: when it is autowired, bound to itself, or the target of a binding such as bind(ConnectionInterface::class, PdoConnection::class). It does not apply to a class a factory builds, because the factory calls the constructor itself.

A class without instance properties is built right away. PHP considers a lazy object without properties initialized from the start, and would never call its constructor.

caution

Because the constructor runs later, so does everything that can go wrong in it. A dependency that cannot be resolved throws its ResolutionException where the object is first used, rather than where it was resolved.

Break a circular dependency​

A lazy class does not resolve its dependencies when it is built, so it breaks a cycle that would otherwise throw a CircularDependencyException:

#[Lazy]
final readonly class EventDispatcher
{
public function __construct(private ListenerProvider $listeners) {}
}

final readonly class ListenerProvider
{
public function __construct(private EventDispatcher $dispatcher) {}
}

$container->get(ListenerProvider::class); // builds, with a lazy EventDispatcher

After lock(), lazy() throws a ContainerLockedException::cannotConfigure().