league/container
League/Container is a fast, lightweight, PSR-11 compliant dependency injection container for PHP. It offers constructor autowiring, service providers, inflectors, and lazy loading out of the box, requires no configuration, and supports PHP 8.3, 8.4, and 8.5.
Build vs. buy: don't build a DI container. league/container is a ~small, MIT-licensed, PSR-11–compliant implementation that covers the 90% case (definitions, shared services, service providers, delegates, tags, reflection-based autowiring, attribute injection, lifecycle events). Building and maintaining this in-house is pure undifferentiated cost.
Standardize on PSR-11 as an architectural contract. Because the container implements Psr\Container\ContainerInterface, it can be treated as an implementation detail behind our own boundary. This directly supports the decision to not couple domain/application code to a specific framework's container — the same decision that lets us swap the underlying container later without a rewrite.
Framework-agnostic library and SDK strategy. If we ship reusable PHP packages (Composer libraries, internal SDKs, CLI tooling, queue workers, or a service that isn't Laravel/Symfony), league/container is a credible default. It gives explicit wiring (add, addShared, addShared overrides) plus ServiceProviderInterface for packaging module wiring alongside the code it configures.
Roadmap: incremental modularization. Service providers, delegates, and tags map cleanly onto a phased extraction of a monolith: each bounded context ships a service provider, cross-cutting concerns move to afterResolve()/events, and tagged definition sets replace hand-maintained factory lists.
Cross-cutting concerns without constructor pollution. The v5.2 event system (OnDefineEvent, BeforeResolveEvent, DefinitionResolvedEvent, ServiceResolvedEvent with forType() / forTag() / forId() filtering) supports roadmap items like per-service metrics, logging decorators, and lazy proxies without touching domain constructors — and without paying event-creation cost when no listener is registered.
Testability and seams. A container-agnostic boundary makes it trivial to substitute test doubles in integration tests, and Container::call() allows invoking controllers/handlers with resolved arguments — useful when evaluating whether to keep or retire a custom dispatcher.
Version/PHP floor is itself a decision input. This release line targets PHP 8.3–8.5. If our fleet is on 8.1/8.2, we'd be pinning to 5.0/5.1 — that pin should be recorded as an explicit platform decision, not discovered in CI.
Adopt when:
ReflectionContainer) rather than a magic default.#[Inject]/#[Resolve] attributes — without adopting a full framework.Look elsewhere when:
Illuminate\Container is already PSR-11 compliant, deeply integrated (facades, contextual binding, package discovery), and adding a second container only creates two sources of truth. Same logic for Symfony (symfony/dependency-injection gives compiler passes, config-driven wiring, and dump-time optimization).Mitigation for all of the above: wrap it. Depend on Psr\Container\ContainerInterface in application code, keep league-specific wiring confined to a bootstrap/composition-root module. Swapping to PHP-DI or Symfony DI then becomes a composition-root change, not a refactor.
To executives (one paragraph): We're proposing a small, MIT-licensed, standards-based dependency injection container for our non-Laravel PHP surface area. It's free, actively released (latest 5.2.x, March 2026), and replaces either a bespoke wiring layer we'd have to build and maintain or a heavyweight framework container we don't need. The strategic value is reduced lock-in: it implements the PSR-11 industry interface, so our code depends on a standard, not a vendor — which keeps future migration and acquisition/technical-diligence conversations simpler. Cost is near zero in licensing and roughly a one-to-two day spike in engineering time to validate on a representative module. Primary risk is maintainer concentration in a small open-source project; we mitigate by depending only on the PSR-11 interface in application code, keeping the package confined to the composition root.
To engineering leadership: PSR-11 compliant, psr/container ^2, explicit definitions by default with opt-in reflection autowiring, service providers for modular wiring, tags and delegates for plugin-style extension, and a v5.2 event system for lifecycle hooks (afterResolve() replaces the now-deprecated inflector()). Modern codebase: PHP 8.3–8.5, PER-CS/PSR-4, Pest tests, PHPStan level 8, documented changelog and upgrade paths. Migration surface is small because definitions are explicit; failure modes are actionable (ContainerException with guidance on unsatisfied constructor deps as of 5.2).
To the delivery team (the ask): A one-day spike — port one real module (or one shared library) behind a ContainerInterface boundary, confirm PHP version compatibility, and write a two-page adoption note covering the version pin, the composition-root convention, and the swap-out path. Decision gate: if the spike shows resolution ergonomics or cold-start cost are worse than what we have, we stop and evaluate PHP-DI instead; the wrapper keeps that exit cheap.
How can I help you explore Laravel packages today?