Container & Services

The container is Fast's dependency injection system. It builds your services, wires their dependencies together automatically, and, crucially, manages the difference between things that live for the whole worker and things that live for a single request. Getting the two lifetimes right is the key to leak-free long-lived PHP.

Binding services

There are four ways to register something with the container, each with a different lifetime:

bind(Id::class, fn ($c) => new Id());          // transient: a fresh one every resolve
singleton(Db::class, fn ($c) => new Db());     // one shared instance for the worker
scoped('tx', fn ($c) => new Tx());             // one per request, then discarded
instance(Config::class, $config);              // a pre-built object

You then pull a service out with resolve() (or its alias app()):

$db = resolve(Db::class);
$config = app(Config::class);

If you ask for a class that was never bound, the container will still build it, autowiring its constructor dependencies by type-hint. So for plain classes you often do not need to register anything at all.

Singletons versus scoped: the important part

This distinction is what makes Fast safe to run as a persistent process, so it is worth internalizing.

A singleton is built once and reused across every request the worker handles. It is ideal for stateless or pooled things: a database handle, the view service, config. Because the worker is long-lived, a singleton is long-lived too.

A scoped service is built once per request and released the moment the response is sent. Use it for anything that holds request-specific state.

singleton('mailer', fn () => new Mailer($config));   // shared, stateless
scoped('cart', fn () => new Cart());                 // per-request state

The container enforces the boundary between them:

Resolving a scoped service outside of a request, or as a dependency of a singleton, throws. That is a feature, not a limitation: it makes it impossible for per-request state to accidentally leak into a worker-lived singleton and bleed across requests. If you hit that error, it means a long-lived object tried to grab a request-lived one, which is exactly the bug you want caught.

How request scopes work

Under the hood, the kernel creates a child scope of the container for each request and discards it afterward. Scoped services live in that child scope. This is the mechanism behind Fast's "boot once, no leaks" promise from the Application Lifecycle: the expensive graph is built once as singletons, and only the cheap per-request slice is rebuilt each time.

Domain helpers

The global helpers like db() and session() are just thin wrappers that resolve a service from the container. You can define your own the same way, which keeps your handlers readable and CGI-flat:

singleton('mailer', fn () => new Mailer(/* ... */));

function mailer(): Mailer
{
	return resolve('mailer');
}

post('/contact', function (): string {
	mailer()->send(/* ... */);
	return 'sent';
});

A caveat about spawned fibers

The request-aware helpers resolve against the current fiber's request context. If you manually spawn a child fiber with Io::spawn(), that child has no request context of its own, so request() returns null there and resolve() falls back to the root container. When you spawn a fiber, pass in whatever request data it needs explicitly rather than reaching for the request helpers inside it. Worker-scoped helpers like singletons and the response builders work everywhere.

The last utility topic is how to manage settings and secrets, in Configuration.