Package · di

Scopes

A scope defines how long a resolved instance lives and whether it is shared. There are three: singleton, transient, and request. A class picks one via attribute or binding; the default is transient.

The three scopes

Scope Identity Cached Set by
singleton one per process yes, after first make() #[Singleton] · singleton()
transient new every resolution no #[Transient] · bind() · transient() · (default)
request one per request / coroutine per request #[Request] · request()

singleton

Created on the first make(), cached in the container, returned on every later call.

Use for stateless services — repositories, factories, connection pools, config readers. Avoid for classes holding per-request state; that leaks across requests under Swoole.

transient

No caching — every make() / injection returns a fresh object. This is the default when a class has no scope attribute and no manual binding.

Use for stateful, non-shared objects — query builders, form objects, DTOs, units of work.

request

One instance per HTTP request, keyed by runtime:

Runtime Behaviour
Swoole Stored in Coroutine::getContext()['__di'] — fully isolated per coroutine. Each concurrent request gets its own instance, cleaned up when the coroutine ends.
FPM Equivalent to singleton — one request = one process, so the process-level cache is correct.
CLI Equivalent to singleton — one command = one process.

Use for per-request state — auth context, current user, unit of work, request-bound counters.

The request scope is the one place ext-swoole matters; see Request scope & Swoole.

Precedence

A manual registration always overrides a class attribute — the last binding wins.

php
#[Singleton]
class UserService {}

// Override to transient for a specific context (e.g. tests)
$c->transient(UserService::class);
// → UserService is now transient regardless of #[Singleton]

The override applies even when the class has already been resolved: every registration (bind() / singleton() / transient() / request()) drops what the previous scope left behind — both the cached instance and its bookkeeping for flushRequestScope(). So a test can switch a scope mid-scenario and get an object built by the new rules rather than the old singleton out of the cache.

Overrides skip the cache entirely

make($abstract, $overrides) with a non-empty $overrides array always builds a fresh instance and neither reads nor writes the singleton/request cache — regardless of scope. Use make() with no overrides to get the shared instance.

Swoole safety matrix

Never place mutable per-request data in a singleton under Swoole — it leaks across concurrent requests sharing the worker’s memory.

Class type Recommended scope
DB connection pool singleton
Repository (stateless) singleton
Config reader singleton
Auth context request
Current user request
Unit of work request
Query builder transient
DTO / value object transient
Form object transient

Under load

When one Swoole worker serves many requests at once, the scope also decides what happens when the same class is resolved simultaneously: transient and request each build their own, while a singleton is built exactly once — the other coroutines wait for the first instead of building a copy. Details on Concurrent resolution.