PSR-11元文档

容器元文档

1.简介

本文档描述了导致Container PSR的过程和讨论。其目标是解释每个决定背后的原因。

为什么要这么麻烦?

那里有许多依赖注入容器,这些DI容器具有非常不同的存储条目的方式。

  • 有些是基于回调(疙瘩,Laravel,......)
  • 其他基于配置(Symfony,ZF,...),具有各种格式(PHP数组,YAML文件,XML文件......)
  • 有些人可以利用工厂......
  • 有些人有PHP API来构建条目(PHP-DI,ZF,Symfony,Mouf ......)
  • 有些人可以进行自动布线(Laravel,PHP-DI,...)
  • 其他人可以根据注释连接条目(PHP-DI,JMS Bundle ...)
  • 有些人有图形用户界面(Mouf ...)
  • 有些人可以将配置文件编译为PHP类(Symfony,ZF ......)
  • 有些人可以做别名......
  • 有些人可以使用代理提供延迟加载依赖项...

因此,当您从大局出发时,可以通过大量方法解决DI问题,从而实现大量不同的实现。但是,所有的DI容器都满足了相同的需求:它们为应用程序提供了一种检索一组配置对象(通常是服务)的方法。

通过标准化从容器中获取条目的方式,使用Container PSR的框架和库可以与任何兼容的容器一起使用。这将允许最终用户根据自己的喜好选择自己的容器。

3.范围

3.1。目标

Container PSR设定的目标是标准化框架和库如何利用容器来获取对象和参数。

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

  • 配置条目
  • 获取条目

大多数时候,这两方并没有被同一方使用。虽然通常是最终用户倾向于配置条目,但通常是获取条目来构建应用程序的框架。

这就是为什么此接口仅关注如何从容器中获取条目的原因。

3.2。非目标

如何在容器中设置条目以及如何配置它们超出了此PSR的范围。这就是使容器实现独特的原因。一些容器根本没有配置(它们依赖于自动装配),其他容器依赖于通过回调定义的PHP代码,其他容器依赖于配置文件......此标准仅关注如何获取条目。

此外,用于条目的命名约定不是此PSR范围的一部分。实际上,当您查看命名约定时,有两种策略:

  • 标识符是类名称或接口名称(主要由具有自动装配功能的框架使用)
  • 标识符是一个通用名称(更接近变量名称),主要由依赖于配置的框架使用。

这两种策略都有其优点和缺点。这个PSR的目标不是选择一个约定而不是另一个约定。相反,用户可以简单地使用别名来弥合具有不同命名策略的2个容器之间的差距。

PSR声明:

“用户不应该将容器传递给对象,因此对象可以检索自己的依赖关系这样做的用户使用容器作为服务定位器。通常不鼓励使用服务定位器。“

// 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.

BadExample你不应该注入容器,因为:

  • 它使代码不易互操作:通过注入容器,您必须使用与Container PSR兼容的容器。使用另一个选项,您的代码可以使用任何容器。
  • 你迫使开发人员命名它的条目“db”。此命名可能与另一个对另一个服务具有相同期望的包冲突。
  • 它更难测试。
  • 从您的代码中不能直接清楚BadExample该类将需要“db”服务。依赖性是隐藏的。

通常,ContainerInterface其他包将使用它。作为使用框架的最终用户PHP开发人员,您不太可能需要直接使用容器或类型提示ContainerInterface

将Container PSR用于代码是否被认为是一种好的做法,或者归结为知道您检索的对象是否是引用容器的对象的依赖关系以下是一些例子:

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);
        // ...
    }
}

在此示例中,路由器正在将URL转换为控制器条目名称,然后从容器中提取控制器。控制器实际上不是路由器的依赖项。根据经验,如果您的对象正在计算 可以变化的条目列表中的条目名称,那么您的用例肯定是合法的。

作为例外,工厂对象的唯一目的是创建和返回新实例,可以使用服务定位器模式。然后工厂必须实现一个接口,以便它本身可以被另一个使用相同接口的工厂替换。

// 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'));
    }
}

5.历史

在将Container PSR提交给PHP-FIG之前,ContainerInterface首先在名为container-interop的项目中提出了这个问题该项目的目标是为实施ContainerInterface容器PSR 提供试验平台,并为其铺平道路。

在本元文档的其余部分中,您将看到频繁引用 container-interop.

6.接口名称

接口名称与讨论的名称相同container-interop (仅更改名称空间以匹配其他PSR)。它已经在container-interop [4]中得到了彻底的讨论,并通过投票决定[5]

与各自投票一起考虑的选项清单如下:

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

7.接口方法

在对现有容器进行统计分析之后,选择接口将包含哪些方法。[6]

分析摘要表明:

  • 所有容器都提供了一种通过其id获取条目的方法
  • 大多数人都说这种方法 get()
  • 对于所有容器,该get()方法有1个类型为string的必需参数
  • 某些容器有一个可选的附加参数get(),但它在容器之间没有相同的用途
  • 大多数容器提供了一种方法来测试它是否可以通过其id返回一个条目
  • 多数名称这种方法 has()
  • 对于所有提供has()容器,该方法只有1个字符串类型的参数
  • 大多数容器抛出异常而不是在找不到条目时返回null get()
  • 绝大多数容器都没有实现 ArrayAccess

在容器 - 互操作项目[4]的最开始讨论了是否包含定义条目的方法的问题已经判断出这些方法不属于此处描述的界面,因为它超出了其范围(参见“目标”部分)。

因此,ContainerInterface包含两种方法:

  • get(),使用一个必需的字符串参数返回任何内容。如果找不到条目,​​则应抛出异常。
  • has(),返回一个布尔值,带有一个必需的字符串参数。

7.1。get()方法中的参数数量

虽然ContainerInterface只定义了一个必需参数get(),但它与具有其他可选参数的现有容器不兼容。PHP允许实现提供更多参数,只要它们是可选的,因为实现确实满足接口。

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

虽然ContainerInterface只定义了一个必需参数get(),但实现可以接受其他可选参数。

这句话已从PSR-11中删除,因为:

  • 它源于PHP中的OO原则,因此这与PSR-11没有直接关系
  • 我们不希望鼓励实现者添加其他参数,因为我们建议对接口进行编码而不是实现

但是,某些实现具有额外的可选参数; 这在技术上是合法的。这些实现与PSR-11兼容。[11]

7.2。$ id参数的类型

的类型的$id参数get()has()已在容器互操作项目进行了讨论。

虽然string在所有分析的容器中使用,但有人建议允许任何东西(例如对象)允许容器提供更高级的查询API。

给出的示例是将容器用作对象构建器。$id参数将是描述如何创建实例的对象。

讨论[7]的结论是,这超出了从容器中获取条目而不知道容器如何提供它们的范围,并且它更适合工厂。

7.3。抛出异常

该PSR提供了2个接口,旨在通过容器异常实现。

7.3.1基本异常

Psr\Container\ContainerExceptionInterface是基础接口。它应该由容器直接抛出的自定义异常来实现。

预计作为容器域的一部分的任何异常都会实现ContainerExceptionInterface几个例子:

  • 如果容器依赖于配置文件,并且该配置文件存在缺陷,则容器可能会InvalidFileException执行该实现ContainerExceptionInterface
  • 如果在依赖关系之间检测到循环依赖关系,容器可能会抛出CyclicDependencyException实现ContainerExceptionInterface

但是,如果某些代码从容器的作用域中抛出异常(例如,在实例化条目时抛出异常),则不需要容器将此异常包装在实现该条目的自定义异常中ContainerExceptionInterface

基本异常接口的用处受到了质疑:它通常不会被捕获[8]

但是,大多数PHP-FIG成员认为这是最佳实践。基本异常接口在以前的PSR和几个成员项目中实现。因此保留了基本异常接口。

7.3.2未找到异常

get使用不存在的id 调用该方法必须抛出实现该异常的异常Psr\Container\NotFoundExceptionInterface

对于给定的标识符:

  • 如果has方法返回false,那么get方法必须抛出一个Psr\Container\NotFoundExceptionInterface
  • 如果has方法返回true,这并不意味着该get方法将成功并且不会抛出异常。Psr\Container\NotFoundExceptionInterface如果缺少所请求条目的一个依赖项,它甚至可以抛出一个

因此,当用户捕获时Psr\Container\NotFoundExceptionInterface,它有两种可能的含义[9]

  • 请求的条目不存在(错误的请求)
  • 或者所请求的条目的依赖项不存在(即容器配置错误)

然而,用户可以容易地区分呼叫has

在伪代码中:

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.
}

8.实施

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

实施者

中间件

消费者

此列表并不全面,仅应作为一个示例,表明对PSR有相当大的兴趣。

9.人

9.1编辑

9.2赞助商

9.3贡献者

这里列出了所有参与讨论或投票的人(在容器互操作和迁移到PSR-11期间),按字母顺序排列:

  1. 关于集装箱PSR和服务定位器的讨论
  2. 容器互操作的 ContainerInterface.php
  3. 所有问题清单
  4. 讨论接口名称和容器 - 互操作范围
  5. 投票给接口名称
  6. 现有容器方法名称的统计分析
  7. 讨论方法名称和参数
  8. 讨论基本异常的有用性
  9. 讨论 NotFoundExceptionInterface
  10. 讨论在container-interopPHP-FIG邮件列表中获取可选参数