Package · di

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

php
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

php
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

php
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

php
make(string $abstract, array $overrides = []): mixed
call(callable|array $callable, array $overrides = []): mixed
makeContextual(string $abstract, ?string $consumer): mixed

make()

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.

php
$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()

php
makeContextual(string $abstract, ?string $consumer): mixed

The 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()

php
inject(object $instance): object

Fills 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.

php
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()

php
flushRequestScope(): void

Ends 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:

php
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

php
register(string $providerClass): static

Instantiates $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.

php
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.

php
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.

php
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.

php
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.

php
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.

php
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.

php
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.