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.
Architecture fit. The central architectural fact is that Laravel already ships a first-class DI container (Illuminate\Container\Container), and it is PSR-11 compliant via Illuminate\Contracts\Container\Container extends Psr\Container\ContainerInterface. Adopting league/container therefore does not "add DI" — it adds a second container. That is defensible only in bounded contexts: framework-agnostic libraries you publish or share across services, extracted domain packages that must not depend on Illuminate, CLI/worker binaries outside the HTTP app, and test harnesses that need a clean PSR-11 graph without booting the framework. Wholesale replacement of the Laravel container is not an architecture fit; it invalidates facades, contextual binding, route-model binding, queue payload hydration, and Octane request scoping.
Integration feasibility. High mechanically, low semantically. The API surface is small and PSR-11 standard: add/addShared, get, has, delegates, ServiceProviderInterface with provides(), ReflectionContainer for auto-wiring, Container::call, plus v5.1 #[Inject]/#[Resolve] attributes and the v5.2 event system (OnDefineEvent, BeforeResolveEvent, DefinitionResolvedEvent, ServiceResolvedEvent). Because both sides speak PSR-11, a bidirectional delegate bridge is trivial to write. The hard part is governance: once two containers exist, "which container owns this service" becomes an unwritten convention that decays into a defect class.
Technical risk.
Container::inflector() in favour of afterResolve()/events, scheduled for removal in v6.0, and removes EventAwareContainerInterface from the contract surface (EventAwareTrait on the concrete class only). Any code typed against that interface is already broken; adopting the deprecated inflector now guarantees a v6 migration.ReflectionContainer caches resolutions; shared definitions and event listeners persist. Under Octane or queue workers this needs explicit reset per request/job — a real operational hazard, not a theoretical one.dependents: 0, which contradicts League's real-world usage and should be verified against Packagist before it informs any decision). MIT licence and a clean history mitigate this — forkability is the escape hatch.Key questions.
psr/container constraint resolve cleanly against each Laravel version in the matrix?Stack fit. Position league/container as an inner container for portable code, with Laravel's container as the outer composition root. Recommended pattern: league/container owns definitions inside extracted domain packages and shared libraries; Laravel's container owns the application, HTTP layer, and everything framework-coupled. The bridge is one adapter registered as a Laravel singleton, not a fleet of ad-hoc get() calls.
Migration path. Do not migrate the application container. Migrate packages, one at a time, each of which:
ServiceProviderInterface implementation with an explicit provides() list (mandatory since 4.0.0 — no class-property shortcut).ServiceProvider that registers those definitions into the bridge.Illuminate out of its composer.json beyond illuminate/support for the adapter.For existing Orno\Di-era or 3.x/4.x code, use the changelog as a migration ledger: 4.0.0 changed shared → addShared and dropped the third $shared argument from add; 5.0.0 made definition overwriting opt-in (disabled by default) and moved the floor to PHP 8.1.
Compatibility. Confirm the psr/container constraint resolves against your Laravel versions before anything else — history here is instructive: 3.3.5 added ^2.0.0, 3.4.0 removed it as irreconcilable, 4.0.0 re-adopted it. Also note auto-wiring moved out of the core Container into the ReflectionContainer delegate in 2.0.0 and is off by default; teams expecting Laravel-style implicit resolution will be surprised. Union types are explicitly unsupported for auto-wiring (5.1.0) — that constraint will surface in real domain code.
Sequencing.
get() interop, shared-instance identity, and Octane reset behaviour. Decide bridge direction explicitly and document it.provides() contract as team convention; add static analysis so missing definitions fail in CI, not at runtime.#[Inject]/#[Resolve] and the v5.2 event API are new surface area and the inflector is already deprecated.Maintenance. Every new service forces a routing decision between two containers; that decision cost is permanent and is the dominant long-run expense, dwarfing the package's own upgrade churn. Definitions live in PHP rather than Laravel's config, so they are greppable and reviewable — an advantage over service-provider sprawl. The changelog is unusually disciplined (per-release files back to 1.0.0), which makes upgrade forecasting cheap. Budget one minor upgrade per quarter and one major every ~18–24 months.
Support. Documented at container.thephpleague.com with a versioned 5.x doc tree, Jekyll-built from docs/. Test suite is Pest with PHPStan at level 8 — strong signal for a small package. PER-CS, PSR-4, PSR-11 compliance are stated and CI-enforced. Security contact is a personal email rather than a private disclosure channel; for an internal dependency that is acceptable, but it should be an explicit accepted risk. MIT licence means the ultimate support fallback is a fork.
Scaling. Resolution is cheap and ReflectionContainer caches, so throughput is not the concern. The scaling concern is process lifetime: under Octane, Swoole, or long-lived queue workers, shared definitions and registered event listeners accumulate across requests unless you reset the container (or scope it per-request/per-job). Treat "the container is request-scoped" as an invariant enforced by the bridge, not by convention. Tag-based resolution (addTag, plus the automatic 'shared' tag introduced in 5.2.0) is the lever for batch resolution without N lookups.
Failure modes.
NotFoundException / ContainerException from PSR-11 interfaces — good, typed, catchable. Since 5.2.0, unsatisfied constructor dependencies raise a ContainerException with actionable guidance instead of a raw ArgumentCountError, which materially shortens incident triage.provides() is generated from or tested against the actual definitions.Ramp-up. The API is small enough that a mid-level PHP developer is productive in one to two days; the organisational ramp-up is the real cost — the team must internalise PSR-11 semantics, the definition-vs-reflection distinction, and the two-container routing rule. Estimate ~1 engineer-week of documentation and pairing to make the pattern self-sustaining, and designate a single owner for the bridge. Because the supplied scores show low opportunity (53.56) against a modest composite (25.7), treat adoption as an enabler for portability, not as a modernization win in its own right: for a Laravel-only estate the correct recommendation is usually to decline, and to revisit only when a concrete framework-agnostic distribution requirement exists.
How can I help you explore Laravel packages today?