This document describes the process and discussions that led to the Container PSR. Its goal is to explain the reasons behind each decision.
There are dozens of dependency injection containers out there, and these DI containers have very different ways to store entries.
So when you look at the big picture, there is a very large number of ways in which the DI problem can be tackled, and therefore a big number of different implementations. However, all the DI containers out there are answering the same need: they offer a way for the application to retrieve a set of configured objects (usually services).
By standardizing the way entries are fetched from a container, frameworks and libraries using the Container PSR could work with any compatible container. That would allow end users to choose their own container based on their own preferences.
The goal set by the Container PSR is to standardize how frameworks and libraries make use of a container to obtain objects and parameters.
It is important to distinguish the two usages of a container:
Most of the time, those two sides are not used by the same party. While it is often end users who tend to configure entries, it is generally the framework that fetches entries to build the application.
This is why this interface focuses only on how entries can be fetched from a container.
How entries are set in the container and how they are configured is out of the scope of this PSR. This is what makes a container implementation unique. Some containers have no configuration at all (they rely on autowiring), others rely on PHP code defined via callback, others on configuration files... This standard only focuses on how entries are fetched.
Also, naming conventions used for entries are not part of the scope of this PSR. Indeed, when you look at naming conventions, there are 2 strategies:
Both strategies have their strengths and weaknesses. The goal of this PSR is not to choose one convention over the other. Instead, the user can simply use aliasing to bridge the gap between 2 containers with different naming strategies.
The PSR states that:
"users SHOULD NOT pass a container into an object, so the object can retrieve its own dependencies. Users doing so are using the container as a Service Locator. Service Locator usage is generally discouraged."
// This is not OK, you are using the container as a service locator
class BadExample
{
public function __construct(ContainerInterface $container)
{
$this->db = $container->get('db');
}
}
// Instead, please consider injecting directly the dependencies
class GoodExample
{
public function __construct($db)
{
$this->db = $db;
}
}
// You can then use the container to inject the $db object into your $goodExample object.
In the BadExample
you should not inject the container because:
BadExample
class will need
the "db" service. Dependencies are hidden.Very often, the ContainerInterface
will be used by other packages. As a end-user
PHP developer using a framework, it is unlikely you will ever need to use containers
or type-hint on the ContainerInterface
directly.
Whether using the Container PSR into your code is considered a good practice or not boils down to knowing if the objects you are retrieving are dependencies of the object referencing the container or not. Here are a few more examples:
class RouterExample
{
// ...
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function getRoute($request)
{
$controllerName = $this->getContainerEntry($request->getUrl());
// This is OK, the router is finding the matching controller entry, the controller is
// not a dependency of the router
$controller = $this->container->get($controllerName);
// ...
}
}
In this example, the router is transforming the URL into a controller entry name, then fetches the controller from the container. A controller is not really a dependency of the router. As a rule of thumb, if your object is computing the entry name among a list of entries that can vary, your use case is certainly legitimate.
As an exception, factory objects whose only purpose is to create and return new instances may use the service locator pattern. The factory must then implement an interface so that it can itself be replaced by another factory using the same interface.
// ok: a factory interface + implementation to create an object
interface FactoryInterface
{
public function newInstance();
}
class ExampleFactory implements FactoryInterface
{
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function newInstance()
{
return new Example($this->container->get('db'));
}
}
Before submitting the Container PSR to the PHP-FIG, the ContainerInterface
was
first proposed in a project named container-interop.
The goal of the project was to provide a test-bed for implementing the ContainerInterface
,
and to pave the way for the Container PSR.
In the rest of this meta document, you will see frequent references to
container-interop.
The interface name is the same as the one discussed for container-interop
(only the namespace is changed to match the other PSRs).
It has been thoroughly discussed on container-interop
[4] and was decided by a vote [5].
The list of options considered with their respective votes are:
ContainerInterface
: +8ProviderInterface
: +2LocatorInterface
: 0ReadableContainerInterface
: -5ServiceLocatorInterface
: -6ObjectFactory
: -6ObjectStore
: -8ConsumerInterface
: -9The choice of which methods the interface would contain was made after a statistical analysis of existing containers. [6].
The summary of the analysis showed that:
get()
get()
method has 1 mandatory parameter of type stringget()
, but it doesn't have the same purpose between containershas()
has()
, the method has exactly 1 parameter of type stringget()
ArrayAccess
The question of whether to include methods to define entries has been discussed at the very start of the container-interop project [4]. It has been judged that such methods do not belong in the interface described here because it is out of its scope (see the "Goal" section).
As a result, the ContainerInterface
contains two methods:
get()
, returning anything, with one mandatory string parameter. Should throw an exception if the entry is not found.has()
, returning a boolean, with one mandatory string parameter.get()
methodWhile ContainerInterface
only defines one mandatory parameter in get()
, it is not incompatible with
existing containers that have additional optional parameters. PHP allows an implementation to offer more parameters
as long as they are optional, because the implementation does satisfy the interface.
Difference with container-interop: The container-interop spec stated that:
While
ContainerInterface
only defines one mandatory parameter inget()
, implementations MAY accept additional optional parameters.
This sentence was removed from PSR-11 because:
However, some implementations have extra optional parameters; that's technically legal. Such implementations are compatible with PSR-11. [11]
$id
parameterThe type of the $id
parameter in get()
and has()
has been discussed in the container-interop project.
While string
is used in all the containers that were analyzed, it was suggested that allowing
anything (such as objects) could allow containers to offer a more advanced query API.
An example given was to use the container as an object builder. The $id
parameter would then be an
object that would describe how to create an instance.
The conclusion of the discussion [7] was that this was beyond the scope of getting entries from a container without knowing how the container provided them, and it was more fit for a factory.
This PSR provides 2 interfaces meant to be implemented by container exceptions.
The Psr\Container\ContainerExceptionInterface
is the base interface. It SHOULD be implemented by custom exceptions thrown directly by the container.
It is expected that any exception that is part of the domain of the container implements the ContainerExceptionInterface
. A few examples:
InvalidFileException
implementing the ContainerExceptionInterface
.CyclicDependencyException
implementing the ContainerExceptionInterface
.However, if the exception is thrown by some code out of the container's scope (for instance an exception thrown while instantiating an entry), the container is not required to wrap this exception in a custom exception implementing the ContainerExceptionInterface
.
The usefulness of the base exception interface was questioned: it is not an exception one would typically catch [8].
However, most PHP-FIG members considered it to be a best practice. Base exception interface are implemented in previous PSRs and several member projects. The base exception interface was therefore kept.
A call to the get
method with a non-existing id must throw an exception implementing the Psr\Container\NotFoundExceptionInterface
.
For a given identifier:
has
method returns false
, then the get
method MUST throw a Psr\Container\NotFoundExceptionInterface
.has
method returns true
, this does not mean that the get
method will succeed and throw no exception. It can even throw a Psr\Container\NotFoundExceptionInterface
if one of the dependencies of the requested entry is missing.Therefore, when a user catches the Psr\Container\NotFoundExceptionInterface
, it has 2 possible meanings [9]:
The user can however easily make a distinction with a call to has
.
In pseudo-code:
if (!$container->has($id)) {
// The requested instance does not exist
return;
}
try {
$entry = $container->get($id);
} catch (NotFoundExceptionInterface $e) {
// Since the requested entry DOES exist, a NotFoundExceptionInterface means that the container is misconfigured and a dependency is missing.
}
At the time of writing, the following projects already implement and/or consume the container-interop
version of the interface.
This list is not comprehensive and should be only taken as an example showing that there is considerable interest in the PSR.
Are listed here all people that contributed in the discussions or votes (on container-interop and during migration to PSR-11), by alphabetical order:
ContainerInterface.php
NotFoundExceptionInterface
The 1.1 release of the psr/container
package includes scalar parameter types. The
2.0 release of the package includes return types. This structure leverages PHP
7.2 covariance support to allow for a gradual upgrade process.
Implementers MAY add return types to their own packages at their discretion, provided that:
Implementers MAY add parameter types to their own packages in a new major release, either at the same time as adding return types or in a subsequent release, provided that:
"psr/container": "^1.1 || ^2.0"
so as to exclude
the untyped 1.0 version.Implementers are encouraged but not required to transition their packages toward the 2.0 version of the package at their earliest convenience.