本文翻译自 PSR-11-container-meta, 限于翻译水平, 部分地方文字表达地不够清晰, 还请见谅, 如有更好的意见欢迎在评论区里提出.
配合 PSR-11-容器 食用更为美味.
阅读本文档能够让你更加深入理解 PSR-11-容器 规范.
本文档描述了PSR容器形成的过程和期间的讨论, 目的在于解释每个决定背后的原因.
条目: 指通过容器可以获取的对应的 变量或实例.
条目标识符: 指存储在容器中的条目对应的id, 可通过该id从容器中获取对应的条目.
目前市面上流行有数十个依赖注入容器, 而这些DI(Dependency Injection)容器有着大相径庭的方式来存储条目.
有一些可以自动装配(Laravel, PHP-DI)
即自动处理依赖
局势就是如此, 当前有着非常多种方式来解决依赖注入问题, 因此有很多种DI容器的实现方式.
然而, 所有的DI容器都在满足同样的需求: 它们为应用程序提供一种检索一组配置对象(通常是服务)的方法.
通过标准化从容器中获取条目的方式, 使用 容器PSR规范 的框架和库可以与与任何兼容的容器.
这更便于用户(指使用容器的开发者)根据各自的喜好来选择容器.
容器标准规范设立的目的在于标准化框架和库如何使用容器来获取对象和参数.
区分容器的以下两种用法很重要:
大多数时候, 这两种用法不会被同一方使用.
因为通常用户(开发者)倾向于配置条目, 而框架主要是获取条目来构建整个应用程序.
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.
条目如何在容器中保存, 以及如何配置它们这并不在PSR的范围内, 因为这是不同容器实现的独特之处.
一些容器完全没有配置(它们依赖于自动装配(自动处理依赖关系)), 其他有基于PHP硬编码的回调, 有基于配置文件…本标准仅关注如何获取条目.
此外, 条目的命名约定不是本PSR的范围. 事实上, 当你查看命名约定时会发现, 主要有2种策略:
两种策略都有其优点和缺点.
PSR的目的并非选用其中一种同时弃用另外一种. 相反的, 用户可以简单地使用别名来桥接具有不同命名策略的两种容器之间的间隙.
PSR指出:
用户 不该 为了让对象能够检索自己的依赖而将容器传递给对象. 因为这意味着容器被作为 服务器定位器使用, 这是不被推荐使用的模式.
// 这是不恰当的, 因为你正将容器作为服务定位器来使用.
class BadExample
{
public function __construct(ContainerInterface $container)
{
$this->db = $container->get('db');
}
}
// 作为替代, 请考虑直接注入依赖
class GoodExample
{
public function __construct($db)
{
$this->db = $db;
}
}
// 你可以使用容器来将 $db 对象注入到你的 $goodExample 对象
object.
在 BadExample
中你不应直接注入容器, 这是因为:
BadExample
类 依赖 “db”服务, 依赖关系被隐藏了.ContainerInterface
通常会被其他包使用.
作为框架的最终使用者, 你不太可能需要使用容器或直接在 ContainerInterface
类型提示.(???)
原文: 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.
判断在代码中是否合理的使用 PSR容器, 归结于你 正要检索的对象 是否是 引用容器的对象 的依赖. 以下是几个例子:
class RouterExample
{
// ...
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function getRoute($request)
{
$controllerName = $this->getContainerEntry($request->getUrl());
// 这是正确的, 路由器使用容器获取对应的控制器条目, 而该控制器并不是路由器的依赖.
$controller = $this->container->get($controllerName);
// ...
}
}
上述示例中, 路由器将URL转换为控制体条目名, 然后从控制器中获取该控制体实例.
路由器实际上并不依赖控制器. 作为一条经验法则, 如果你的对象需要解析并从一系列条目(指容器中提供的同类型多个对象)中获取所需条目, 那么你的用例肯定是恰当的(指的是将容器注入到对象中).
原文: 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.
还有一个例外, 单纯的只为了创建并返回实例的工厂对象是可以使用服务定位器模式的.
这个工厂必须实现一个接口, 以便它能够被实现同样接口的其他工厂替代.
// ok: 一个工厂接口 + 实现 创建对象
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'));
}
}
在将容器标准规范提交到 PHP-FIG之前, ContainerInterface
首先在名为 container-interop
的项目中提出.
该项目的目的在于为实现 ContainerInterface
接口的容器提供测试平台, 并为 容器PSR 铺平道路.
在这个元文档的剩余部分, 你会看到对 container-interop
的频繁引用.
接口名称与在 container-interop
中所讨论的一样(只更改了其命名空间以匹配其他PSRs), 它已经在 container-interop
中进行了彻底的讨论并通过了投票决定.
所有候选的接口名选项的投票情况如下:
ContainerInterface
+8ProviderInterface
+2LocatorInterface
0ReadableContainerInterface
-5ServiceLocatorInterface
-6ObjectFactory
-6ObjectStore
-8ConsumerInterface
-9接口将包含哪些方法是基于对现有容器进行统计分析后制定的.
译: 上述链接对应的统计结果
Container | Read | Test | Not found behavior | ArrayAccess |
---|---|---|---|---|
Aura DI | get($key) | has($key) | Exception | No |
Auryn | make($key, [$params]) | Exception | No | |
AWS/Guzzle | get($key, [$throwAway]) | array access | Exception | Yes |
Laravel | make($key, [$params]) | bound($key) | Exception? | Yes |
League\Di | resolve($key) | bound($key) | Exception | No |
Mouf | get($key) | has($key) | Exception | No |
Orno\Di | resolve($key, [$args]) | ? | No | |
PHP-DI | get($key) | has($key) | Exception | No |
Pimple | array access | array access | Exception | Yes |
PPI | get($key, [$bool]) | hasOption($key) | Exception | Yes |
Symfony | get($id, [$invalidBehavior]) | has($key) | Null or Exception | No |
ZF2 | get($key, [$params]) | Null | No |
分析总结表明:
get()
get()
方法提供一个必须的参数(string类型).get()
提供了一个可选的附加参数, 但它在不同容器中并没有相同的用途.has()
has()
的容器, 该方法只有一个string类型的参数get()
方法无法返回对应条目时抛出一个异常, 而不是返回 null
ArrayAccess
接口(将容器作为数组来使用)在 container-interop
项目的最初阶段已经讨论过是否应包含定义条目的方法. 讨论结果是这些方法不属于 容器PSR的范畴, 因为它超出了 容器PSR 的范围.
作为结果, ContainerInterface
接口包含两个方法:
get()
, 返回任何内容, 并带有一个必须的字符串参数. 如果没有对应的条目, 应抛出异常.has()
, 返回布尔值, 带有一个必须的字符串参数.get()
方法中的参数数量 Number of parametersContainerInterface
接口仅在 get()
中定义了一个必须参数, 而这与现存的具有其他可选参数的容器不兼容.
PHP允许容器的实现提供更多的可选参数, 因为这确实符合接口的实现.
与 container-interop
的区别: container-interop
规范声明了:
尽管
ContainerInterface
在get()
只定义了一个必须参数, 但其实现可以接受额外的可选参数.
上述句子在 PSR-11 中被移除了, 因为:
然而有一些容器的实现具有额外的可选参数, 这在技术上是合法的. 这种实现也与 PSR-11 兼容.
$id
参数的类型 Type of the $id parameterget()
和 has()
中的 $id
参数的类型已经在 container-interop
项目中讨论过了.
尽管在我们分析过的容器中$id
的参数类型都是string
, 但有人建议应允许任何类型(例如对象)以便容器提供更高级的查询API.
给出的一个例子是将容器作为对象构造器来使用. $id
参数是一个对象并被用来描述如何创建对应的实例.
讨论的结论是: 在不清楚容器如何提供条目的情況下, 从容器中如何获取条目这超出了 PSR-11 的范围, 同时这种方式 (指将容器作为构造器使用) 更适合使用工厂.
本PSR提供了2个接口以供容器的异常来实现.
Psr\Container\ContainerExceptionInterface
是容器异常的基础接口. 它应该被从容器中直接抛出的自定义异常所实现.
任何属于容器部分的异常都应实现 ContainerExceptionInterface
. 以下是几个示例:
ContainerExceptionInterface
接口的 InvalidFileException
异常.ContainerExceptionInterface
接口的 CyclicDependencyException
异常.但是如果某个异常是在容器范围外的代码抛出的(比如实例化一个条目时抛出的异常), 则不要求容器将该异常包装成实现了 ContainerExceptionInterface
接口的自定义异常.
基本异常接口的用处被质疑过: 它并非一个通常会特地捕获的异常.
但是大多数 PHP-FIG 认为这是最佳做法. 基础异常接口在之前的 PSRs规范 和一些成员的项目中都被实现了, 因此保留了基本的异常接口.
使用不存在的id调用 get
方法 必须 抛出一个实现了 Psr\Container\NotFoundExceptionInterface
接口的异常.
对于给定的标识符:
has
方法返回了 false
, 那么 get
方法必须抛出一个 Psr\Container\NotFoundExceptionInterface
异常.has
方法返回了 true
, 这不意味着 get
方法会成功且不会引发异常. 如果所请求条目的某个依赖项丢失, 它也会抛出 Psr\Container\NotFoundExceptionInterface
异常.因此当用户捕捉到 Psr\Container\NotFoundExceptionInterface
异常时, 它意味着可能存在2种情况:
用户可以通过调用 has
方法很容易地区分上述的2种情况.
伪代码如下:
if (!$container->has($id)) {
// 所请求的实例不存在
return;
}
try {
$entry = $container->get($id);
} catch (NotFoundExceptionInterface $e) {
// 由于请求条目确定是存在的, `NotFoundExceptionInterface` 异常的捕获意味着容器缺少配置, 其中有依赖项缺失.
}
在撰写本文时, 以下项目已经实现 和/或 使用了container-interop
版本的接口.
这份清单并不全面, 仅用于表明 PSR 受到了广泛关注.
这里列出所有参与讨论或投票的人员(在 container-interop
项目中以及迁移至 PSR-11 期间), 按照字母顺序排列: