Container interface — implementing PSR-11 ContainerInterface
Concept
PSR-11 is a PHP Standard Recommendation that defines a minimal interface for dependency injection containers. Published in 2017, it was created specifically to solve the ecosystem fragmentation problem: every framework had its own container with its own API, making it impossible to write truly interoperable packages that needed container access.
The PSR-11 ContainerInterface has exactly two methods: get(string $id): mixed and has(string $id): bool. That minimalism is intentional. The PSR authors deliberately left out bind(), singleton(), make(), and all the other rich APIs that real containers need, because those are implementation details. PSR-11 only standardises the read interface — resolving an already-registered service. The write API (registering bindings) is left to each implementation.
This design enables "container-agnostic" packages. A package that needs to resolve LoggerInterface can type-hint ContainerInterface and call $container->get(LoggerInterface::class), and it will work identically whether the consuming application uses Laravel's container, Symfony's, PHP-DI, or our custom one.
Our Container class will implement ContainerInterface. Beyond the PSR, we will add: bind() for factory bindings, singleton() for shared instances, instance() for pre-built objects, and make() as the primary resolution method with additional features like passing constructor parameters. This mirrors the public API of Illuminate\Container\Container.
The ContainerExceptionInterface and NotFoundException are the two PSR-11 exception interfaces. Our implementation will throw NotFoundException (which extends ContainerExceptionInterface) when get() is called with an identifier that cannot be resolved, matching the PSR requirement. The distinction: ContainerExceptionInterface is for errors during resolution (circular dependency, missing constructor argument); NotFoundException is specifically for when the ID does not exist in the container at all.
Code Example
<?php
declare(strict_types=1);
namespace Lumen\Container;
use Psr\Container\ContainerInterface;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
/**
* PSR-11 compliant exceptions for our container.
*
* NOTE: In production, add psr/container to composer.json:
* "psr/container": "^2.0"
* This provides the Psr\Container interfaces.
*
* For this standalone lesson, we define the interfaces inline.
*/
// If you do not install psr/container, define the interfaces manually:
// (In the real project, require psr/container and remove these.)
interface ContainerInterface
{
public function get(string $id): mixed;
public function has(string $id): bool;
}
/**
* Thrown when the container cannot resolve an ID for any reason.
*/
class ContainerException extends \RuntimeException implements ContainerExceptionInterface {}
/**
* Thrown when get() is called with an ID that has no binding.
*/
class NotFoundException extends ContainerException implements NotFoundExceptionInterface {}
// ------------------------------------------------------------------
// Container scaffold — implements PSR-11 ContainerInterface
// ------------------------------------------------------------------
/**
* The IoC Container — implements PSR-11 and adds framework-level features.
*
* This lesson: PSR-11 compliance scaffold.
* Subsequent lessons add: bind(), singleton(), make(), auto-wiring.
*
* Laravel equivalent: Illuminate\Container\Container
*/
class Container implements ContainerInterface
{
/**
* All registered bindings.
* Structure: ['abstract' => ['concrete' => callable|string, 'shared' => bool]]
*
* @var array<string, array{concrete: callable|string, shared: bool}>
*/
protected array $bindings = [];
/**
* Resolved shared instances (singletons).
*
* @var array<string, mixed>
*/
protected array $instances = [];
// ------------------------------------------------------------------
// PSR-11 interface
// ------------------------------------------------------------------
/**
* Finds an entry of the container by its identifier and returns it.
*
* @throws NotFoundException No entry was found for this identifier.
* @throws ContainerException Error while retrieving the entry.
*/
public function get(string $id): mixed
{
if (!$this->has($id)) {
throw new NotFoundException(
"No binding registered for [{$id}]."
);
}
return $this->make($id);
}
/**
* Returns true if the container can return an entry for the given identifier.
*/
public function has(string $id): bool
{
return isset($this->bindings[$id]) || isset($this->instances[$id]);
}
// ------------------------------------------------------------------
// Extended API (not PSR-11 — framework-specific)
// ------------------------------------------------------------------
/**
* Store a pre-built object instance in the container.
* The same instance is returned on every resolution.
*/
public function instance(string $abstract, mixed $instance): void
{
$this->instances[$abstract] = $instance;
}
/**
* Placeholder: bind() and make() are implemented in the next lessons.
*/
public function make(string $abstract, array $parameters = []): mixed
{
// Check for a pre-built instance first
if (isset($this->instances[$abstract])) {
return $this->instances[$abstract];
}
throw new NotFoundException("No binding registered for [{$abstract}].");
}
}Interview Q&A
Q: PSR-11 only defines get() and has(). Why doesn't it define bind() or make()?
PSR-11 was designed as a minimum viable interoperability standard, not a full container API. The PSR authors recognised that different containers have fundamentally different mental models: Laravel uses bind()/singleton(), Symfony uses XML/YAML configuration or attributes, PHP-DI uses reflection-based auto-wiring by default. Trying to standardise the write API would have made the PSR either too opinionated (excluding valid designs) or too abstract (unusable). By standardising only get() and has(), PSR-11 gives packages a stable way to read from any container without depending on the container's specific registration API. This is the Interface Segregation Principle in action at the ecosystem level.
Q: What is the difference between ContainerExceptionInterface and NotFoundExceptionInterface in PSR-11?
NotFoundExceptionInterface extends ContainerExceptionInterface. NotFoundExceptionInterface is thrown specifically when get() is called with an ID that has no entry — the identifier is unknown to the container. ContainerExceptionInterface covers all other resolution errors: the binding exists but throws during construction (circular dependency, missing constructor argument, a factory that throws). Code that calls get() can use this distinction: catch NotFoundExceptionInterface to detect "not registered" vs catch ContainerExceptionInterface to detect "registered but broken." Laravel throws \Illuminate\Contracts\Container\BindingResolutionException which implements ContainerExceptionInterface when resolution fails.
Q: Why does our has() check both $bindings and $instances?
The PSR-11 requirement for has() is: return true if calling get() with the same id would not throw NotFoundException. Our container has two ways a value can be resolvable: a binding (a factory registered with bind() or singleton()) and a pre-built instance (registered with instance()). If we only checked $bindings, calling has('app') after $container->instance('app', $appObject) would return false — incorrect and a PSR violation. Checking both arrays ensures has() accurately reflects what get() will succeed on.