API Reference
Complete reference for the public surface of Winter DI. This is the authoritative source for signatures and defaults; the guides and deep-dive pages link here.
Container
final class Container implements Psr\Container\ContainerInterface. The registry and resolver.
It self-registers, so Container and ContainerInterface can be injected as dependencies.
Initialisation
Container::init(): static
Container::getInstance(): static
Container::isInitialized(): bool| Method | Description |
|---|---|
init() |
Creates a new container and stores it as the process-wide singleton. Call once at bootstrap. Returns the instance for fluent chaining. |
getInstance() |
Returns the initialised container. Throws ContainerException if init() was never called. |
isInitialized() |
Whether a container exists yet. For code that works with or without one — a library reachable both from a booted application and from a bare script. Asking is cheaper than catching what getInstance() throws, because a missing container is a legitimate state there rather than an error. |
PSR-11
get(string $id): mixed
has(string $id): bool| Method | Description |
|---|---|
get($id) |
Alias for make($id). Throws NotFoundException if $id has no binding and is not an instantiable class. |
has($id) |
true if $id has a binding, a resolved/set() value, or matches an existing class name (class_exists). |
Registration
bind(string $abstract, string|callable $concrete): static
singleton(string $abstract, string|callable|null $concrete = null): static
transient(string $abstract, string|callable|null $concrete = null): static
request(string $abstract, string|callable|null $concrete = null): static
contextual(string $abstract, callable $factory): static
set(string $id, mixed $value): static| Method | Scope | Description |
|---|---|---|
bind() |
transient |
Maps $abstract to a concrete class or factory closure. A new instance on every resolution. |
singleton() |
singleton |
One shared instance per process. $concrete defaults to $abstract (self-bind). |
transient() |
transient |
Like bind() but allows self-binding when $concrete is null. |
request() |
request |
One instance per HTTP request / coroutine. $concrete defaults to $abstract. |
contextual() |
— | Consumer-aware factory fn(Container $c, ?string $consumer). An injection-time overlay; result never cached. See makeContextual(). |
set() |
— | Stores a scalar or pre-built instance under a named key. Retrieve via make($id) or #[Inject('id')]. |
A factory closure passed to bind() / singleton() / transient() / request() receives the
container: fn(Container $c) => new Service($c->make(Dep::class)).
Manual registration overrides attributes
A bind() / singleton() / transient() / request() call always takes priority over a
class’s scope attribute. Re-registering the same abstract replaces the previous binding and
drops everything the old scope left behind — the cached instance and its bookkeeping for
flushRequestScope() — so an override also applies to a class that was already resolved.
Resolution
make(string $abstract, array $overrides = []): mixed
call(callable|array $callable, array $overrides = []): mixed
makeContextual(string $abstract, ?string $consumer): mixedmake()
Resolves an abstract — a class, interface, or named value.
| Parameter | Type | Default | Description |
|---|---|---|---|
$abstract |
string |
— | Class/interface name or named-value key. |
$overrides |
array |
[] |
Named parameter overrides, keyed by parameter name. Bypasses autowiring for those parameters. |
Resolution order: (1) already-resolved singleton / set() value, (2) request-scope coroutine
cache, (3) cycle check against the current unit of work’s stack, (4) waiting for a singleton
another coroutine is building, (5) manual binding, (6) autowire by class name. Passing
$overrides always builds a fresh instance and is never cached. Throws
NotFoundException if unresolvable, ContainerException on a circular dependency — with the
full chain, [A] → [B] → [A]. Steps 3 and 4 are covered in
Concurrent resolution.
call()
Invokes a method or closure with its parameters resolved from the container.
$c->call([UserController::class, 'index']); // resolve class, then call
$c->call([$controller, 'store']); // existing instance
$c->call(fn(UserService $s) => $s->all()); // closure
$c->call([ImportJob::class, 'run'], ['chunkSize' => 100]); // with overrides| Parameter | Type | Default | Description |
|---|---|---|---|
$callable |
callable|array |
— | [class-string, method], [object, method], or any callable/closure. |
$overrides |
array |
[] |
Named parameter overrides, keyed by parameter name. |
makeContextual()
makeContextual(string $abstract, ?string $consumer): mixedThe resolver’s entry point for contextual injection. If a contextual() factory is registered
for $abstract, it is invoked with $consumer (result not cached); otherwise delegates to
make($abstract). Application code normally uses make() — this exists for the injection
machinery. The contextual overlay applies to injection only (constructor / method /
property); a direct make()/get() uses the regular binding.
inject()
inject(object $instance): objectFills the #[Autowired] / #[Inject] properties of an object the caller built, and returns
that same instance. It is the second half of make(), exposed on its own: make() builds and
injects, which is right whenever the container owns construction and wrong when the caller must
own the object’s identity.
public static function instance(?string $alias = null): static
{
$repository = new static(); // identity stays with the caller
Container::getInstance()->inject($repository); // dependencies still arrive
return $alias === null ? $repository : $repository->as($alias);
}The case this exists for is a repository handle carrying a query alias: the alias lives in
per-object state, so two aliases of one table need two distinct objects, and resolving them
through make() on a shared binding would collapse both into one and lose an alias. The
instance is never swapped, its constructor state is left alone, and injecting twice is harmless.
flushRequestScope()
flushRequestScope(): voidEnds the current request scope: the next resolution of a #[Request] binding builds a fresh
instance. Singletons and transients are untouched — this ends a scope, it does not reset the
container.
Over HTTP the scope ends by itself, because a request is a coroutine and its context dies with it. Nothing else has that boundary. A long-lived worker runs its whole body inside one coroutine, so a request-scoped bean resolved in the loop lives for the entire run and hands each job the previous job’s state:
while ($this->isRunning()) {
$job = $queue->pop();
$container->flushRequestScope(); // ← a new unit of work starts here
$ctx = $container->make(JobContext::class); // fresh, every time
// ... work ...
}Only the code driving the loop knows where one unit of work ends, which is why the call is explicit.
Providers
register(string $providerClass): staticInstantiates $providerClass, verifies it extends ServiceProvider, and runs its register()
immediately. Throws ContainerException if the class does not extend ServiceProvider.
Scanner
Walks a project tree once and dispatches every discovered class to all registered collectors.
Scanner::run(string $rootDir, ?string $cache = null): static
->collect(CollectorInterface $collector): static
->exclude(array $dirs): static
->execute(): void| Method | Description |
|---|---|
run($rootDir, $cache) |
Creates a scanner for $rootDir. When $cache is a path: cache hit loads the FQCN list and skips the FS walk; cache miss walks and writes the list. null (default) always walks, never caches. |
collect($collector) |
Registers a collector that receives every discovered class. Called in registration order. |
exclude($dirs) |
Adds absolute directory paths to exclude. vendor/ is always excluded. |
execute() |
Runs the scan. Abstract classes, interfaces, and traits are skipped before collectors run. |
The cache file is a plain PHP file returning a string[] of FQCNs. Delete it to force a
rescan. See Scanning & autodiscovery.
DICollector
final readonly class DICollector implements CollectorInterface. The built-in collector that
registers classes carrying a scope attribute.
new DICollector(Container $container)
collect(string $class, ReflectionClass $ref): void| Class attribute | Registration |
|---|---|
#[Singleton] |
$container->singleton($class) |
#[Request] |
$container->request($class) |
#[Transient] |
$container->transient($class) |
Classes with no scope attribute are ignored (they remain autowirable as transient).
CollectorInterface
Implement to plug custom logic into a Scanner pass.
interface CollectorInterface
{
// @param class-string $class FQCN
// @param ReflectionClass $ref reflection instance
public function collect(string $class, ReflectionClass $ref): void;
}collect() is called once per instantiable class, in a tight loop — keep it lightweight.
ServiceProvider
Abstract base for grouping bindings.
abstract class ServiceProvider
{
abstract public function register(Container $c): void;
}Register via $container->register(MyProvider::class). See
Service providers.
ProxyInterface
Implement this when you generate a subclass that has to resolve under the name of the class it stands in for.
interface ProxyInterface
{
/** @return class-string the class this proxy stands for */
public static function proxyTarget(): string;
}A proxy is a generated class that extends the original and adds behaviour around its
methods: run it in the background, wrap it in a transaction, cache its result. Winter’s
#[Async] attribute works exactly this way. The catch is that once the container hands out
UserService__Async, $instance::class no longer reports what the application wrote — and the
resolver uses that name to decide for whom a dependency is being built, so a
contextual() logger factory would name the channel
after the generated class.
Implementing the interface puts the identity back: the resolver plans injection from the target class and passes that same class to factories as the consumer.
final class UserServiceProxy extends UserService implements ProxyInterface
{
public static function proxyTarget(): string
{
return UserService::class;
}
}
$container->singleton(UserService::class, UserServiceProxy::class);The method is static on purpose: the resolver needs the identity before an instance exists,
while building constructor arguments. $instance::class keeps reporting the generated name —
the interface changes how the container reasons about identity, not what PHP reports.
Injection into the target’s private properties still works, because the resolver walks the class
hierarchy rather than trusting ReflectionClass::getProperties().
ReflectionCache
Per-process cache for reflection objects — built once, reused for the process lifetime.
ReflectionCache::classOf(string $class): ReflectionClass
ReflectionCache::enumOf(string $enum): ReflectionEnum
ReflectionCache::method(string $class, string $method): ReflectionMethod
ReflectionCache::parameters(string $class, string $method): ReflectionParameter[]| Method | Returns | Notes |
|---|---|---|
classOf($class) |
ReflectionClass |
Cached per class. |
enumOf($enum) |
ReflectionEnum |
Cached per enum. |
method($class, $method) |
ReflectionMethod |
Cached per class::method. |
parameters($class, $method) |
ReflectionParameter[] |
Delegates to method(); shares its cache entry. |
A public utility — see Reflection cache.
Exceptions
Both implement the PSR-11 exception interfaces and extend \RuntimeException.
| Exception | Implements | Thrown when |
|---|---|---|
NotFoundException |
Psr\Container\NotFoundExceptionInterface |
No binding for an id and it is not an instantiable class. |
ContainerException |
Psr\Container\ContainerExceptionInterface |
Circular dependency, unresolvable parameter, uninitialised container, invalid provider, invalid #[Lazy] target. |
Attributes
Reference tables for #[Singleton], #[Request], #[Transient], #[Autowired], #[Inject],
and #[Lazy] live on the Attributes page.