PSR 11 容器接口 - 说明文档
1. 介绍
本文档描述了创建 「 容器 」PSR 的过程和讨论。目标是解释每个决定背后的原因
2. 何必?
已经存在了几十个依赖注入容器,且这些依赖注入容器存储实体的方法各不相同
- 有些是基于回调的 ( Pimple, Laravel, ... )
- 另一些则基于配置 ( Symfony, ZF, ...),使用各种格式 ( PHP arrays, YAML files, XML files... )
- 有些使用了工厂方法...
- 有些提供了 PHP API 来创建实体 (PHP-DI, ZF, Symfony, Mouf... )
- 一些可以自动装配 ( Laravel, PHP-DI, ... )
- 其它的则通过注解来连接到实体 ( PHP-DI, JMS Bundle... )
- 有些具有用户图形界面 ( Mouf...)
- 一些可以将配置文件编译为 PHP 类 ( Symfony, ZF... )
- 一些可以做别名...
- 有些则使用代理来延迟加载依赖项...
当你看到了全部,那么解决依赖注入的问题的方法则有很多种,因此也就有很多中实现方式。 但是,所有的这些依赖注入容器都在实现相同的功能:它们为应用程序提供了一种检索一组配置对象 ( 通常是服务 ) 的方法
将从容器中获取实体的方式标准化,为了实现此容器 PSR 的框架或库可以与任何兼容的容器一起使用,允许消费者 ( 最终用户 ) 可以根据自己的喜好自由的选择自己容器
为啥省略了最后的那几个字....比如,这就是此 PSR 的宏伟愿景 ?
3. 正文
3.1. 目标
此容器 PSR 的目标是:将框架和库如何使用容器来获取对象和参数的方法标准化
区分容器的两种用法是很重要的:
- 配置实体
- 获取实体
大多时候,双方并不会被同一方使用。 消费者 ( 最终用户 ) 通常倾向于配置实体,框架则通常获取实体来创建应用程序
这也解释了为什么此规范中的接口只关注如何从容器中获取实体
3.2. 非目标
容器中的实体是如何设置的和如何配置的不在此 PSR 的范畴之内。这也使得实现了此接口的容器独一无二。
也就是说如何配置和设置实体由实现者自由选择,自由选择吗? 当然就是独一无二啦。
一些容器完全没有任何配置 ( 它们依赖于自动装配 ) ,一些容器则依赖于 PHP 代码中的回调机制 ,一些则使用配置文件....此标准仅关注如何获取实体
此外,实体的命名约定并不是此 PRR 范畴内的一份。实际上,当你查找命名约定时,有两种策略:
- 该标识符是类名称或接口名称( 主要是具有自动装配能力的框架在使用 )
- 该标识符是一个通用名称 ( 更接近变量名称 ) ,主要是依赖于配置的框架在使用
两种策略都有其优点和缺点。此 PSR 的目标不是选择一个约定而放弃另一个约定。相反,用户可以简单地使用别名来弥补两个使用不同的命名策略的容器之间的差异
4. 推荐用法: 容器 PSR 和 服务器定位模式
此 PSR 指出:
「 用户 不应该 把容器传递给一个对象,因此对象可以检查 自己的依赖关系 。 这种使用容器的方式叫做服务器定位模式。服务器定位模式通常是不被鼓励使用的 」
<?php // 这是不好的,因为使用了服务器定位模式 class BadExample { public function __construct(ContainerInterface $container) { $this->db = $container->get('db'); } } // 替代方法,使用直接依赖注入 class GoodExample { public function __construct($db) { $this->db = $db; } } // 然后你可以使用一个容器将 $db 对象注入到 $goodExample 对象中
在 BadExample
范例中,你不应该注入容器,因为:
-
它将使代码 更少的互操作性 : 使用注入容器的方式,你不得不使用兼容容器 PSR 规范的容器。而使用另一种方式,你的代码可以使用任何容器
-
这是在迫使开发者命名它的实体为 「 db 」,这个命名可能与另一个对某个服务具有相同命名要求的包相相冲突
-
很难进行测试
-
隐藏了依赖关系。很难从
BadExample
范例中的代码中看到需要依赖 「 db 」 服务
ContainerInterface
通常会被其它包使用。作为框架的最终使用者的 PHP 开发人员,不会直接需要在 ContainerInterface
上使用容器或类型提示
无论你的代码是否使用了此容器 PSR ,都是一种很好的实战。不能归结为知道你检索的对象是否 依赖 引用了容器的对象
以下是几个例子
<?php 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 转换为一个控制器实体的名称,然后从容器中获取该控制器。实际上, 控制器并不是路由器的依赖项。 一个经验,如果你的对象可以从一组可变的实体中计算出实体名称,那么你的用例肯定是合法的。
作为一个例外,只有创建和返回新实例的工厂对象可以使用服务定位器模式。 这个工厂也 必须 实现一个接口,以便它可以被另一个实现了相同的接口的工厂替换。
<?php // 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')); } }
5. 历史
将这份容器 PSR 提交给 PHP-FIG 之前,ContainerInterface
的概念由名为 「 互操作容器 」( container-interop
) 的项目首先提出,该项目的目标是为实现 ContainerInterface
的项目或库提供测试平台,并为 容器 PSR 铺平道路
本说明文档的剩余部分,你将会频繁的看到 「 互操作容器 」( container-interop
)
6. 接口名称
接口名称和关于「 互操作容器 」的一个讨论中的接口的名称相同,只更改了命名空间来匹配其它的 PSR
接口的名称,其实已经在 「 互操作容器 」[4] 中彻底讨论过了,并且通过投票决定 [5]
投票表决的结果如下:
ContainerInterface
: +8ProviderInterface
: +2LocatorInterface
: 0ReadableContainerInterface
: -5ServiceLocatorInterface
: -6ObjectFactory
: -6ObjectStore
: -8ConsumerInterface
: -9
7. 接口方法
我们在对现有容器进行统计分析之后才确定了接口将包含哪些方法 [6]
统计分析的结果表明:
- 所有的容器都提供了一个通过 id 来获取实体的方法
- 大多数都将该方法命名为
get()
- 所有的容器中,
get()
方法都有一个字符串类型的必传参数 - 一些容器的
get()
方法有着附加的可选的参数,但用途都各不相同 - 大部分的容器都提供了一个方法来检查否可以根据 id 返回一个实体
- 大多数的容器都将该方法命名为
has()
- 提供了
has()
方法都所有容器中,has()
方法都有一个明确的字符串类型的参数 - 绝大多数容器中的
get()
方法在没有找到实体时都会抛出一个异常而不是返回一个null
值 - 绝大多数容器都没有实现
ArrayAccess
是否应该包含一个定义实体的方法的问题 [4] ,在互操作容器项目的早期阶段就已经被讨论过了。结论就是这些方法不属于这里定义的接口,因为它已经超出了此规范的范畴 ( 参见 「 目标 」 部分 )
因此,ContainerInterface
接口包含两个方法:
get()
,返回任何东西,只有一个且必传的参数,如果实体没有找到则应该抛出一个异常has()
,返回布尔值,只有一个字符串类型的必传的参数
7.1. 「 get() 」 方法的参数数量
ContainerInterface
的 get()
方法只定义了一个必须参数,
这看起来与已经存在的容器中有着更多可选参数的 get()
方法不兼容。
PHP 允许一个实现可以提供更多可选的参数,因为这种实现也确实满足了接口的要求
对于 PHP 来说,没有重载的概念..
与互操作容器的区别:互操作容器规范 中讲到:
虽然
ContainerInterface
中的get()
方法只有一个且还是必选的参数,但实现类库 可以 接受更多的可选参数。
但这个段语句已经从 PSR-11 中移除,因为:
-
这是 PHP 面向对象 ( OO ) 的规则,这与 PSR-11 没有任何直接关系
-
我们并不想鼓励实现者添加更多的参数,我们建议面向接口编程而不是面向实现编程
但是,仍然有一些实现库添加了附加的可选的参数,这在技术上是合规的,这种实现也兼容了 PSR-11. [11]
7.2. 「 $id 」参数的类型
get()
和 has()
方法中的 $id
参数的类型在 「 交互性容器 」项目中已经被讨论过了。
尽管所有分析过的容器都使用 string
类型,还是建议允许任何类型 ( 例如对象 ),这样容器就可以提供一个更高级别的检索 API
一个例子就是使用容器作为对象构造器,$id
参数就可以作为一个描述如何创建实例的对象
那次讨论 [7] 的结论,这超出了从容器中获取实体而不用知道容器如何提供它们的范畴,并且它更适合于工厂方法
7.3. 抛出异常
此 PSR 提供了两个需要被实现的容器异常
7.3.1 基础异常
Psr\Container\ContainerExceptionInterface
是一个基础的接口,容器直接抛出的自定义的异常都 应该 实现此接口
容器领域内的任何异常都应该实现 ContainerExceptionInterface
接口,几个范例:
- 如果容器依赖一个配置文件,而配置文件正好又有缺陷,则容器可以抛出一个实现了
ContainerExceptionInterface
接口的InvalidFileException
异常。 - 如果依赖项之间出现了循环依赖,容器可以抛出一个实现了
ContainerExceptionInterface
接口的CyclicDependencyException
异常
但是,如果超出了容器范畴的代码引发了异常 ( 例如在实例化实体时引发的异常 ),则不要求容器将这个异常封装在实现 ContainerExceptionInterface
接口的自定义异常中
基础异常接口的用途被质疑:通常都不会去捕获这个异常 [8]
但是,大多数 PHP-FIG 成员认为这是最佳做法。 因为之前的 PSR 规范或成员项目中已经实现基础异常接口,因此保留了基础异常接口。
7.3.2 未找到异常
使用一个不存在的 标识符 ( id ) 调用 get
方法必须抛出一个实现了 Psr\Container\NotFoundExceptionInterface
接口的异常
对于给定的标识符:
- 如果
has
方法返回false
, 那么get
方法 必须 抛出Psr\Container\NotFoundExceptionInterface
异常 - 如果
has
方法返回true
, 并不意味着get
方法一定会成功且不抛出任何异常,它也有可能抛出Psr\Container\NotFoundExceptionInterface
异常,如果请求的实体的某个依赖项丢失。
因此,如果使用者捕获到了 Psr\Container\NotFoundExceptionInterface
异常,那么就有两种可能性 [9]:
- 请求的实体不存在 ( 错误请求 )
- 请求的实体缺少某个依赖项 ( 例如,没有配置容器 )
使用者可以轻松的通过 has
方法来区分它们,例如 ( 伪代码 )
<?php if (!$container->has($id)) { // 请求的实例不存在 return; } try { $entry = $container->get($id); } catch (NotFoundExceptionInterface $e) { // 因为请求的实例存在,所以 NotFoundExceptionInterface 异常意味着容器配置错误或缺少依赖 }
8. 实现
在撰写此规范时,下面的项目已经实现和/或使用容器互操作 ( container-interop
) 版本的接口
实现类库
- Acclimate
- Aura.DI
- dcp-di
- League Container
- Mouf
- Njasm Container
- PHP-DI
- PimpleInterop
- XStatic
- Zend ServiceManager
中间件
消费着 ( 使用者 )
- Behat
- interop.silex.di
- mindplay/middleman
- PHP-DI Invoker
- Prophiler
- Silly
- Slim
- Splash
- Zend Expressive
这份清单并不全面,仅仅是对此 PSR 有极大兴趣的一个例子
9. 参与人员
9.1 编撰者
9.2 发起人
- Matthew Weier O'Phinney (Coordinator)
- Korvin Szanto
9.3 贡献者
下面列出的是所有参与讨论或投票的人员 ( 在 容器互操作提案 和迁移到 PSR-11 期间 ) ,按字母顺序排列:
- Alexandru Pătrănescu
- Amy Stephen
- Ben Peachey
- David Négrier
- Don Gilbert
- Jason Judge
- Jeremy Lindblom
- Larry Garfield
- Marco Pivetta
- Matthieu Napoli
- Nelson J Morais
- Paul M. Jones
- Phil Sturgeon
- Stephan Hochdörfer
- Taylor Otwell
10. 相关文章
- Discussion about the container PSR and the service locator
- Container-interop's
ContainerInterface.php
- List of all issues
- Discussion about the interface name and container-interop scope
- Vote for the interface name
- Statistical analysis of existing containers method names
- Discussion about the method names and parameters
- Discussion about the usefulness of the base exception
- Discussion about the
NotFoundExceptionInterface
- Discussion about get optional parameters in container-interop and on the PHP-FIG mailing list