Container

Three service lifetimes, autowiring, publishing per-request values, and deterministic teardown.

There are two objects. Container holds process-lifetime definitions and is frozen when boot ends. RequestScope holds everything belonging to one request and is thrown away when it finishes.

Registering services

PHP
<?php
Application::boot(static function (Blueprint $app): void {
    // Once per process, shared by every request.
    $app->container->singleton(
        Connection::class,
        static fn (): Connection => Connection::sqlite($path),
    );

    // Once per request.
    $app->container->scoped(
        ShoppingCart::class,
        static fn (RequestScope $scope): ShoppingCart => new ShoppingCart(
            $scope->get(Session::class),
        ),
    );
});
A singleton must be stateless

It is shared by every request the worker ever serves. Configuration, connections and compiled tables are fine. Anything that remembers this request — the current user, a cart, a request id — must be scoped() or autowired, or it becomes one user's data shown to the next.

orbit make:class Name --singleton or --scoped writes the class with that constraint as its comment and prints the registration line above, so the choice is made while the file is being created rather than after a leak.

Scoped factories receive the scope

Note the parameter type: scoped() hands your factory the RequestScope, not the Container. That is deliberate — resolving a dependency through the scope keeps you in the same request. Handing it the container would tempt a factory into opening a second scope and quietly missing everything middleware published into the real one.

Autowiring

An unregistered class is built from its constructor signature:

PHP
<?php
final class ArticleRepository
{
    public function __construct(private readonly Connection $database)
    {
    }
}

final class ShowArticle implements Handler
{
    // Nothing registered. Both are built per request.
    public function __construct(private readonly ArticleRepository $articles)
    {
    }
}
Autowiring is scope-only, on purpose

Container::get() refuses anything unregistered. Only RequestScope::get() will autowire, and what it builds dies with the request. If the container cached an autowired instance, that instance would be shared by every later request in the worker — the exact leak the two-phase split exists to prevent.

Failures are explicit, and name the parameter that blocked resolution:

Output
CannotAutowire: Cannot resolve "$timeout" of App\Mailer::__construct(): it has no
  class type and no default. Register "App\Mailer" with an explicit factory.

CannotAutowire: Cannot resolve "App\Clock": it is an interface or abstract class,
  so it must be registered with an explicit factory.

CannotAutowire: Circular dependency while resolving "App\A": App\A -> App\B -> App\A

Interfaces

Bind the interface, depend on the interface:

PHP
<?php
$app->container->singleton(
    Clock::class,
    static fn (): Clock => new SystemClock(),
);

final class ExpiryCheck
{
    public function __construct(private readonly Clock $clock)
    {
    }
}

Swapping in a FrozenClock for tests is then a one-line change at boot.

Publishing per-request values

Middleware uses provide() to hand something to everything further in. This is how the session and the matched route reach your controller:

PHP
<?php
public function process(ServerRequest $request, RequestScope $scope, Closure $next): Response
{
    $tenant = $this->tenants->forHost($request->uri->host);

    $scope->provide(Tenant::class, $tenant);

    return $next($request);
}
PHP
<?php
// Anywhere downstream, including autowired constructors
final class Dashboard implements Handler
{
    public function __construct(private readonly Tenant $tenant)
    {
    }
}

provided() asks whether an optional collaborator exists, which is how a layer stays useful when something upstream did not run:

PHP
<?php
if (!$scope->provided(Session::class)) {
    return Response::text('SessionMiddleware must run first.', Status::InternalServerError);
}

What the framework publishes for you:

TypePublished byAvailable when
RequestScopeKernelAlways
RouteKernelA route matched
ServerRequestKernelAt handler dispatch
SessionSessionMiddlewareThat middleware is registered
AuthenticatorRegistered as scoped() in bootstrapAlways

Teardown

onClose() registers work to run when the request ends — including when the handler throws:

PHP
<?php
$scope->onClose(static function () use ($handle): void {
    fclose($handle);
});

Callbacks run in reverse order, and every one runs even if an earlier throws, so a single failing release cannot strand the rest. The scope then marks itself closed:

PHP
<?php
$stale = $scope;
// ... request ends ...
$stale->get(Session::class);
// ScopeClosed: This request scope has been closed. Holding a reference to it
//   beyond the request it belongs to would expose one request's state to another.

An exception is far kinder than silently serving the previous visitor's session.

Resolution order

For $scope->get(Foo::class):

  1. Already built or provided this request? Return it.
  2. A scoped() definition? Build it, cache for this request.
  3. A singleton() definition? Delegate to the container.
  4. Otherwise autowire it, per request.

The freeze

PHP
<?php
$app->container()->isFrozen();   // true, once boot() has returned

$app->container()->singleton(Foo::class, $factory);
// ContainerFrozen: Cannot register "Foo": the container was frozen when the
//   application finished booting. Registering during a request would leak the
//   definition into every later request served by this worker process.

This is the structural half of worker safety: not a convention to remember, but an exception you cannot get past.