illuminate/container
The Illuminate Container is Laravel's powerful dependency injection container. It manages class dependencies, binds and resolves services, supports contextual and method injection, and provides a simple, flexible foundation for building decoupled PHP applications.
Install via Composer (composer require illuminate/container) if you're outside Laravel; inside Laravel it's already there and you resolve through app(), facades, and constructor injection without touching the container directly. First thing to learn is the binding API — bind(), singleton(), instance(), and make(). Start with one interface and one concrete class:
$container = new Illuminate\Container\Container();
$container->bind(
\App\Contracts\PaymentGateway::class,
\App\Payments\StripeGateway::class
);
$gateway = $container->make(\App\Contracts\PaymentGateway::class);
Read the source for Container::make(), build(), and resolveDependencies() first — they're compact and explain 90% of the behavior you'll see when debugging. Your first real use case should be replacing new Foo() calls in one service class with constructor-injected dependencies, then binding the class in a service provider. That single change demonstrates the payoff (testability, swappable implementations) before you reach for tagging or contextual binding.
Interface-to-concrete for every external service. The everyday pattern is bind(Contract::class, Implementation::class) in a service provider, and type-hinting the contract in constructors. This is what makes swap()-style mocking in tests trivial and lets you defer "which payment provider / storage driver / mailer" to config.
singleton() for anything stateful or expensive. Database connections, HTTP clients, and caches should be singletons; stateless value objects and controllers should stay transient. If a class holds a connection, a lock, or a pool, bind it as a singleton. Otherwise leave it auto-resolved.
Contextual binding when the same contract needs different implementations. when(UploadController::class)->needs(StorageInterface::class)->give(S3Storage::class) and when(ReportController::class)->needs(StorageInterface::class)->give(LocalStorage::class). Use this instead of branching on instanceof inside the resolved class.
Tagging for plugin/list patterns. Collect related implementations under a tag ($container->tag(Formatter::class, [JsonFormatter::class, XmlFormatter::class])) and pull them all with $container->tagged(). This is the clean way to add a new event handler, exporter, or validator without editing a registry class — the registry just iterates tagged().
call() for method injection in controllers and commands. Route actions, event listeners, and Artisan commands go through $container->call([$handler, 'handle']), which resolves typed method parameters alongside route/event arguments. This is how you accept a Request plus typed service parameters in one method signature.
extend() to decorate post-resolution. Wrap a resolved service with logging, metrics, or retries without touching the class:
$container->extend(ApiClient::class, fn ($client) => new RetryDecorator($client, 3));
Use it sparingly — extension order depends on registration order, and stacking decorators gets confusing fast.
makeWith() for runtime parameters. When a constructor takes untyped scalars (an API key, a tenant id), use $container->makeWith(Foo::class, ['apiKey' => $key]) rather than fighting contextual bindings.
Lazy resolution inside long classes. If a class has a heavy dependency it only uses on some code paths, type-hint a closure or use app() inside the method — but prefer constructor injection and split the class if the dependency really is conditional.
Don't touch the global instance in long-running processes. Container::setInstance() mutates process-wide state. Under Octane, Horizon, or plain queue workers, this leaks between jobs. Inject dependencies or pass the container explicitly; if you must reset, do it in a clear lifecycle hook and be deliberate.
Circular dependencies surface at resolution time, not registration. The container will throw CircularDependencyException only when you call make(). Catch this early by adding a resolve-smoke test for each of your top-level services. Real fixes: inject an interface with a lazy proxy, split the class, or defer the dependency with a closure.
Auto-wiring only works on type-hinted, resolvable parameters. Any untyped or scalar constructor parameter will fail with BindingResolutionException unless you bind it, give it a default, or use makeWith(). When you see that error, check the constructor signature before assuming the binding is wrong.
bind() vs singleton() is a real behavioral difference in tests. A bind() returns a fresh instance every make() call — usually what you want. A singleton() will hand the same instance to every consumer in a test, which can cause subtle state bleed between assertions. Reset the container or use forgetInstance() between tests.
Use resolved() and build() to debug. resolved(Foo::class) tells you if an instance already exists (helpful when you suspect singleton state), and build(Foo::class) shows how a class would be constructed without caching it. Both are cheap and far more useful than dumping the whole container.
PSR-11 compatibility does not mean feature parity. get() is the PSR-11 method and works everywhere; tag(), tagged(), extend(), when(), and call() are Laravel extensions. If you plan to run the same code in a non-Laravel PSR-11 container later, keep those out of library code.
Watch for instance() sharing surprises. Registering a pre-built object with instance() makes it shared forever in that container — fine for config or a single logger, dangerous for anything request-scoped.
Resolve config-dependent scalars through bindings, not hardcoded values. Bind configuration values as named strings ($container->bind('app.timezone', fn ($c) => $c->make('config')->get('app.timezone'))) rather than closing over env() or config() in the class constructor — it keeps classes testable and container-swappable.
Performance: resolution is fast, but reflection-based auto-wiring on hot paths (per-request loops) adds up. Cache resolved singletons for anything used in a tight loop, and avoid calling make() inside large iterations where the instance doesn't change.
How can I help you explore Laravel packages today?