PSR-11 compliance — how Laravel's container implements ContainerInterface
Concept
PSR-11 is the PHP-FIG standard that defines the Container Interface (Psr\Container\ContainerInterface). It specifies exactly two methods: get(string $id): mixed and has(string $id): bool. The goal is framework interoperability — any library that needs a service locator can type-hint ContainerInterface instead of a Laravel-specific class, making the library usable with Symfony, PHP-DI, or any other PSR-11-compliant container.
Laravel's Illuminate\Container\Container implements ContainerInterface as of Laravel 5.5. The get($id) method delegates to make($id), and has($id) checks whether the abstract is bound ($this->bound($id)) or has already been resolved. The full chain: get() → make() → the same resolution path covered in the container internals lesson.
One important PSR-11 subtlety: has() is specified to return true only if get() will not throw — not merely if a binding exists. Laravel's bound() checks $bindings, $instances, and $aliases. If none of those contain the abstract but the class itself is auto-resolvable (no binding needed, just a plain class), has() returns false even though get() would succeed. This is a deliberate conservative interpretation: PSR-11 says "return false if you're not sure." This distinction matters when writing package code that relies on has() to check for optional integrations.
The Psr\Container\NotFoundExceptionInterface and Psr\Container\ContainerExceptionInterface are the two PSR-11 exception contracts. Laravel wraps its BindingResolutionException in Illuminate\Container\EntryNotFoundException (which implements NotFoundExceptionInterface) when get() is called for an unresolvable abstract that has no binding.
| PSR-11 method | Laravel implementation | Throws |
|---|---|---|
get(string $id) | Delegates to Container::make($id) | EntryNotFoundException (PSR-11 NotFoundExceptionInterface) |
has(string $id) | Calls Container::bound($id) | Never throws |
Implementing ContainerInterface in your own custom container (for the Framework track) means your container will work with any package that accepts ContainerInterface, including PSR-15 middleware dispatchers, PSR-14 event dispatchers, and Slim Framework.
Code Example
<?php
declare(strict_types=1);
// --- 1. ContainerInterface type hint in a library class ---
// This class works with Laravel, Symfony, PHP-DI, or any PSR-11 container.
namespace Acme\Library;
use Psr\Container\ContainerInterface;
class PluginManager
{
public function __construct(private readonly ContainerInterface $container) {}
public function get(string $pluginClass): object
{
if (!$this->container->has($pluginClass)) {
throw new \RuntimeException("Plugin {$pluginClass} is not registered.");
}
return $this->container->get($pluginClass);
}
}
// --- 2. Laravel satisfies ContainerInterface ---
// app() returns Illuminate\Foundation\Application which extends Container
// which implements Psr\Container\ContainerInterface.
use Illuminate\Container\Container;
use Psr\Container\ContainerInterface;
$container = app();
assert($container instanceof ContainerInterface); // true
// PSR-11 get() — identical to make() for registered bindings
$cache = $container->get(\Illuminate\Contracts\Cache\Repository::class);
// --- 3. has() vs bound() — the PSR-11 subtlety ---
class MyService {}
// Not explicitly bound, but auto-resolvable (plain concrete class):
$container->make(MyService::class); // works fine
$container->has(MyService::class); // false — no explicit binding
$container->bound(MyService::class); // false — same check
// After explicit binding:
$container->singleton(MyService::class, fn() => new MyService());
$container->has(MyService::class); // true — now explicitly bound
// --- 4. EntryNotFoundException for PSR-11 compliance ---
try {
$container->get('non.existent.binding');
} catch (\Psr\Container\NotFoundExceptionInterface $e) {
// Laravel throws Illuminate\Container\EntryNotFoundException
// which implements Psr\Container\NotFoundExceptionInterface
echo get_class($e); // Illuminate\Container\EntryNotFoundException
}
// --- 5. Using ContainerInterface in a service provider ---
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Psr\Container\ContainerInterface;
class ThirdPartyServiceProvider extends ServiceProvider
{
public function register(): void
{
// Bind the PSR-11 interface itself to Laravel's container —
// so packages that type-hint ContainerInterface get the Laravel container.
$this->app->bind(ContainerInterface::class, fn($app) => $app);
// Register the PluginManager with PSR-11 ContainerInterface injected
$this->app->singleton(\Acme\Library\PluginManager::class, function ($app) {
return new \Acme\Library\PluginManager($app);
});
}
}Interview Q&A
Q: What is PSR-11, and how does Laravel implement it?
PSR-11 is the PHP-FIG Container Interface standard, defining get(string $id): mixed and has(string $id): bool as the minimal surface area for a dependency injection container. Laravel's Illuminate\Container\Container implements Psr\Container\ContainerInterface by delegating get() to make() and has() to bound(). This makes the Laravel container usable anywhere a PSR-11 container is accepted — Slim, middleware dispatchers, test frameworks, and third-party packages. For has(), Laravel conservatively returns false for auto-resolvable classes that have no explicit binding, following the spec's rule that has() should only return true if get() is guaranteed not to throw. When get() is called for an unresolvable abstract, Laravel throws Illuminate\Container\EntryNotFoundException, which implements Psr\Container\NotFoundExceptionInterface.
Q: Why does ContainerInterface::has() return false for a class that make() can successfully resolve?
PSR-11 specifies that has($id) must return true only when get($id) will succeed without throwing. For auto-resolved classes (plain concrete classes with no explicit bind() or singleton() registration), the container can build them via Reflection, but it cannot guarantee this without actually attempting the build. The class might have an unresolvable constructor dependency that is only discovered at build time. Laravel's bound() check — which powers has() — only looks at explicit registrations in $bindings, $instances, and $aliases. Auto-resolution is attempted optimistically in make() but not guaranteed. Package authors who rely on has() to test for optional features should always explicitly bind those features rather than relying on auto-resolution.
Q: When would you type-hint ContainerInterface instead of Application in your own code?
You type-hint ContainerInterface when writing reusable library code that must not depend on Laravel specifically — a package that might be used with Symfony or PHP-DI, a PSR-15 middleware that needs to resolve handlers, or an event dispatcher that resolves listeners on demand. Inside a Laravel application you would not typically do this because Illuminate\Container\Container offers richer methods (make(), bind(), resolving(), etc.) that ContainerInterface does not expose. The rule is: libraries use ContainerInterface; applications use the concrete container. Binding Psr\Container\ContainerInterface::class to the application instance inside a service provider ensures any library that requests ContainerInterface via DI gets the Laravel container automatically.