欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

[译文]PSR-11-容器规范说明文档 container-meta

程序员文章站 2022-04-13 08:48:34
...

容器PSR规范说明文档 Container Meta Document

本文翻译自 PSR-11-container-meta, 限于翻译水平, 部分地方文字表达地不够清晰, 还请见谅, 如有更好的意见欢迎在评论区里提出.

配合 PSR-11-容器 食用更为美味.
阅读本文档能够让你更加深入理解 PSR-11-容器 规范.


1. 简介 Introduction

本文档描述了PSR容器形成的过程和期间的讨论, 目的在于解释每个决定背后的原因.


条目: 指通过容器可以获取的对应的 变量或实例.
条目标识符: 指存储在容器中的条目对应的id, 可通过该id从容器中获取对应的条目.


2. 为什么 Why bother?


目前市面上流行有数十个依赖注入容器, 而这些DI(Dependency Injection)容器有着大相径庭的方式来存储条目.

  • 有一些基于回调(callbacks) (Pimple, Laravel, …)
  • 其他的基于配置(Symfony, ZendFramework, …), 配置有多种格式(PHP数组, YAML文件, XML文件)
  • 有一些可以 利用工厂(leverage factories)
  • 有一些使用PHP API来创建条目(PHP-DI, ZF, Symfony, Mouf, …)
  • 有一些可以自动装配(Laravel, PHP-DI)

    即自动处理依赖

  • 其他的一些能根据注释来解决依赖(PHP-DI, JMS Bundle…)
  • 有一些提供图形用户界面(Mouf)
  • 有一些可以使用配置文件编译成PHP类(Symfony, ZF)
  • 有一些支持别名
  • 有一些可以使用代理以提供依赖关系的延迟加载(lazy loading of dependencies)

局势就是如此, 当前有着非常多种方式来解决依赖注入问题, 因此有很多种DI容器的实现方式.
然而, 所有的DI容器都在满足同样的需求: 它们为应用程序提供一种检索一组配置对象(通常是服务)的方法.

通过标准化从容器中获取条目的方式, 使用 容器PSR规范 的框架和库可以与与任何兼容的容器.
这更便于用户(指使用容器的开发者)根据各自的喜好来选择容器.

3. 范围 Scope


3.1. 目的 Goals

容器标准规范设立的目的在于标准化框架和库如何使用容器来获取对象和参数.

区分容器的以下两种用法很重要:

  • 配置条目
  • 获取条目

大多数时候, 这两种用法不会被同一方使用.
因为通常用户(开发者)倾向于配置条目, 而框架主要是获取条目来构建整个应用程序.

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.

3.2 非目的 Non-goals


条目如何在容器中保存, 以及如何配置它们这并不在PSR的范围内, 因为这是不同容器实现的独特之处.
一些容器完全没有配置(它们依赖于自动装配(自动处理依赖关系)), 其他有基于PHP硬编码的回调, 有基于配置文件…本标准仅关注如何获取条目.

此外, 条目的命名约定不是本PSR的范围. 事实上, 当你查看命名约定时会发现, 主要有2种策略:

  • 标识符使用 类名(class)接口名(interface), 主要由具有自动装配能力的框架使用.
  • 标识符使用 通用名(更接近于变量名称), 主要由依赖配置的框架使用.

两种策略都有其优点和缺点.
PSR的目的并非选用其中一种同时弃用另外一种. 相反的, 用户可以简单地使用别名来桥接具有不同命名策略的两种容器之间的间隙.

4. 推荐用法 PSR容器和服务定位器 Container PSR and the Service Locator


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 中你不应直接注入容器, 这是因为:

  • 它导致代码可操作性(less interoperable)更少: 通过注入容器到对象, 你不得不使用兼容PSR规范的容器. 使用直接注入对象的依赖的方式, 你的代码可以使用任意容器.
  • 它迫使开发者使用”db”来命名数据库条目. 该命名可能与其他包(使用同一条目但期望获得其他服务)产生冲突.
  • 不方便测试
  • 从代码中无法明显看出 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'));
    }
}

5. 历史 History


在将容器标准规范提交到 PHP-FIG之前, ContainerInterface 首先在名为 container-interop 的项目中提出.
该项目的目的在于为实现 ContainerInterface 接口的容器提供测试平台, 并为 容器PSR 铺平道路.

在这个元文档的剩余部分, 你会看到对 container-interop 的频繁引用.

6. 接口名 Interface name


接口名称与在 container-interop 中所讨论的一样(只更改了其命名空间以匹配其他PSRs), 它已经在 container-interop 中进行了彻底的讨论并通过了投票决定.

所有候选的接口名选项的投票情况如下:

  • ContainerInterface +8
  • ProviderInterface +2
  • LocatorInterface 0
  • ReadableContainerInterface -5
  • ServiceLocatorInterface -6
  • ObjectFactory -6
  • ObjectStore -8
  • ConsumerInterface -9

7. 接口方法 Interface methods


接口将包含哪些方法是基于对现有容器进行统计分析后制定的.

译: 上述链接对应的统计结果

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

分析总结表明:

  • 所有的容器都提供一个通过条目id来获取对应条目的方法, 大多数使用方法名是 get()
  • 对于所有容器, get() 方法提供一个必须的参数(string类型).
  • 一些容器为get() 提供了一个可选的附加参数, 但它在不同容器中并没有相同的用途.
  • 大多数容器提供了一个方法来测试它是否可以通过特定 id 返回对应的条目, 大多数使用的方法名是 has()
  • 对于所有提供 has() 的容器, 该方法只有一个string类型的参数
  • 大部分容器会在 get() 方法无法返回对应条目时抛出一个异常, 而不是返回 null
  • 大部分容器没有实现 ArrayAccess 接口(将容器作为数组来使用)

container-interop 项目的最初阶段已经讨论过是否应包含定义条目的方法. 讨论结果是这些方法不属于 容器PSR的范畴, 因为它超出了 容器PSR 的范围.

作为结果, ContainerInterface 接口包含两个方法:

  • get() , 返回任何内容, 并带有一个必须的字符串参数. 如果没有对应的条目, 应抛出异常.
  • has() , 返回布尔值, 带有一个必须的字符串参数.

7.1 get() 方法中的参数数量 Number of parameters

ContainerInterface 接口仅在 get() 中定义了一个必须参数, 而这与现存的具有其他可选参数的容器不兼容.
PHP允许容器的实现提供更多的可选参数, 因为这确实符合接口的实现.

container-interop 的区别: container-interop 规范声明了:

尽管 ContainerInterfaceget() 只定义了一个必须参数, 但其实现可以接受额外的可选参数.

上述句子在 PSR-11 中被移除了, 因为:

  • 这源于PHP的面向对象原则, 因此 PSR-11 并没有直接相关
  • 我们不希望鼓励容器的实现者添加额外的参数, 因为我们建议针对接口编程而不是针对实现编程(译: 面向对象的设计原则之一).

然而有一些容器的实现具有额外的可选参数, 这在技术上是合法的. 这种实现也与 PSR-11 兼容.

7.2 $id参数的类型 Type of the $id parameter

get()has() 中的 $id 参数的类型已经在 container-interop 项目中讨论过了.

尽管在我们分析过的容器中$id 的参数类型都是string, 但有人建议应允许任何类型(例如对象)以便容器提供更高级的查询API.
给出的一个例子是将容器作为对象构造器来使用. $id 参数是一个对象并被用来描述如何创建对应的实例.

讨论的结论是: 在不清楚容器如何提供条目的情況下, 从容器中如何获取条目这超出了 PSR-11 的范围, 同时这种方式 (指将容器作为构造器使用) 更适合使用工厂.

7.3 抛出异常 Exceptions thrown

本PSR提供了2个接口以供容器的异常来实现.

7.3.1 异常基类 Base exception

Psr\Container\ContainerExceptionInterface 是容器异常的基础接口. 它应该被从容器中直接抛出的自定义异常所实现.

任何属于容器部分的异常都应实现 ContainerExceptionInterface. 以下是几个示例:

  • 如果某个容器依赖于文件配置, 并且该配置文件存在缺陷, 那么容器可能抛出一个实现了 ContainerExceptionInterface 接口的 InvalidFileException 异常.
  • 如果在依赖关系中检测到依赖循环, 那么容器可能会抛出一个实现了 ContainerExceptionInterface 接口的 CyclicDependencyException 异常.

但是如果某个异常是在容器范围外的代码抛出的(比如实例化一个条目时抛出的异常), 则不要求容器将该异常包装成实现了 ContainerExceptionInterface 接口的自定义异常.

基本异常接口的用处被质疑过: 它并非一个通常会特地捕获的异常.

但是大多数 PHP-FIG 认为这是最佳做法. 基础异常接口在之前的 PSRs规范 和一些成员的项目中都被实现了, 因此保留了基本的异常接口.

7.3.2 “未找到”异常 Not found exception

使用不存在的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` 异常的捕获意味着容器缺少配置, 其中有依赖项缺失.
}

8. 实现 Implementations


在撰写本文时, 以下项目已经实现 和/或 使用了container-interop 版本的接口.

容器实现 Implementors

中间件 Middleware

容器使用者 Consumers

这份清单并不全面, 仅用于表明 PSR 受到了广泛关注.

9. 人员 People


9.1 编者 Editors

9.2 赞助者 Sponsors

9.3 贡献者 Contributors

这里列出所有参与讨论或投票的人员(在 container-interop 项目中以及迁移至 PSR-11 期间), 按照字母顺序排列:

  1. 容器PSR和服务定位器的相关讨论
  2. Container-interop 项目的ContainerInterface.php
  3. 所有问题列表
  4. 关于接口名和 container-interop 的范围
  5. 接口名的投票
  6. 现有容器方法名的统计分析
  7. 关于方法名和参数的讨论
  8. 关于异常基类的讨论
  9. 关于NotFoundExceptionInterface 的讨论
  10. 关于 container-interopPHP-FIG邮件列表中 关于 get可选参数的讨论