Contributed by
Nicolas Grekas
in #20973.

作为我们 功能体验计划 的一部分,在 Symfony 3.3 中我们添加了一个全新的功能,叫作 getter injection。它向依赖注入添加了不常见的架构,但并不改变任何内容。而是提供一种特别的方式来满足某些特定用例。

Getter注入允许DI容器利用类来提供“基于继承”(inheritance-based)的扩展特点(extension points),以匹配下列需求:不带参的私有或受保护方法且无副作用。

通过用grep检索Symfony及其vendor,找到了一些例子:

  • Kernel::getRootDir/CacheDir/LogDir() ,位于 HttpKernel
  • SessionListener::getSession() 同样位于 HttpKernel
  • AbstractBaseFactory::getGenerator() 位于 ProxyManager

在Symfony核心和其他地方的所有类中,它们是仅有的应用了这一 “open/closed原则” 特性的小子集。如例程所示,这既应用了对象注入(服务/service)又应用了值注入(参数/parameter)

Getter注入,是一种通过简单的DI配置,来把这些类给转换成DI候选者(candidates)的方式。在Yaml中,以 SessionListener::getSession() 为例的话,应该是下面这样:

1
2
3
4
services:
  SessionListener:
    getters:
      getSession: '@session'

实践中,这会告诉Symfony的 Dependency Injection Container 去创建一个匿名的“继承代理”(inheritance-proxy )类,像下面这种:

1
2
3
4
5
6
7
8
9
10
11
12
13
$sessionListener = new class ($container) extends SessionListener {
    private $container;
 
    function __construct(ContainerInterface $container)
    {
        $this->container = $container;
    }
 
    public function getSession()
    {
        return $this->container->get('session');
    }
};

超高难文档 Symfony身为PHP框架之王,其霸气侧漏再次得到证明,参考 匿名类

Getter注入的优点

执意设计为getter注入的类,相对于其他IoC策略,有若干优势:

  1. 使用继承,令它们从任意框架/DIC(对比注入容器)的任意耦合中解脱出来;

  2. 它令被注入的依赖immutable(对比setter/property注入);

  3. 在内部总是需要使用getter来取出某些依赖,进而允许对提及的依赖进行lazy实例化(对比constructor/setter/property注入);

  4. 它不会污染构造器,这对那些“为追求扩展性”而设计的类来说是一大优势(对比setter/property注入 - 同时对比构造器注入);

  5. 它可以和 “基于traits” 的组合相处得很好,每个trait提供了一个新的open getters组合(有和setter/property注入相同的优点 - 对比构造器注入);

  6. 它令依赖显式化(对比注入容器)

  7. 它允许使用abstracdt getter来声明任意依赖;

  8. 它允许提供默认getters来拥有可选依赖,默认getter返回 null 或 一个默认实现(如, 一个NullLogger);

  9. 它允许拥有和报告丢失的依赖,这些依赖在使用“由基类(一般是控制器或命令)提供的某些功能子集”时,作为某种条件下的需求 - 出现这种情况时可以抛出有用的异常信息。

  10. 用来影响注入特性的代理(proxies),很容易编写(见示例)或生成;

  11. 当注入的依赖自身改变时,代理不需要改变(对比当需要懒加载时使用dcorators);

  12. 在使用被注入的依赖时,没有性能损失(对比当需要懒加载时使用dcorators)

  13. 在使用匿名或已生成的proxy classes时,它不创建任何新的类型提示(new type to hint for),因此会从继承地狱中解脱。

Getter注入的缺点

  1. 它需要编写一个继承代理(inheritance proxy),在对这些类进行测试或关联时,相对于其他的注入策略,这便增加了更多的公式化代码(boilerplate);

  2. 根据设计,它不能工作在final类中,也不可以和private方法一起工作;

  3. 由于PHP(它不像其他语言)不提供“实时创建代理”的任何方式,而是需要,要么手写代码以及一个剥离出来的容器,或是使用 eval() 来支持基于实(runtime-based)时的DICs;

  4. 当懒加载(laziness)是一个目标时,它把“懒责任”给了依赖的调用一方(对比当使用decoration时,责任在被调用一方) - 这便令它只能在 “懒加载是[向getter注入开放的类] 的核心业务逻辑的一部分” 时才可以使用;

  5. 增加了间接性,这便令debugging变困难(为何这是类抛出来的?,“噢,这个懒依赖的初始化在那时失败了”);

  6. 当inheritance proxy提供了laziness时,它可能在PHP对象关系图中创建出“循环依赖”(circular dependencies),这会令“垃圾收集”在某些情况下变成刚需;

  7. 如果尚未遇到 “不能有副作用” 的需求,它会导致bugs - 当不使用abstract getters时,此一需求从技术角度来说很难执行(需要静态代码分析/static code analyzes)。

讨论

“getter注入” 在介绍中是矛盾的,它有正面和反面的反馈。我已尽量在上面列表中总结出全部合理参数和优缺点。

统计之后,我希望“令人惊喜的效果”会随着时间消失,并且仅对getter注入的技术价值进行评估。我个人对它的观点是,它通过自己独有的优缺点,填补了一些空白,并以此为资格,在Symfony容器的支持下,成为开发者工具链的一环。

在Symfony内核中使用之展望

本功能首先锁定的是用在特定框架的需求之中。现时点的目标是提供解耦的组合式基类trait(s)。参见 pull request 18193 以了解此主题的下一步是什么。

问答

使用常规DI时,如果需要,我可以手动关联依赖。使用这种class时我如何做同样的事?

例如在使用匿名类时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$myDep = new DepClass();
$myConsumer = new class ($myDep) extends ConsumerClass {
    private $dep;
 
    public function __construct(DepClassInterface $dep)
    {
        $this->dep = $dep;
    }
 
    protected function getDep(): DepClassInterface
    {
        return $this->dep;
    }
};

但那样的话有太多公式化代码!

是的,这已经列在“缺点”里了。我们使用DICs来完成烦人的关联(wiring) - 这里也一样。注意由于(PHP)语言的帮助,(代码)也可减少为下面这样(在PHP内核层面,关于此点有相关讨论,请去支持它):

1
2
3
4
5
6
7
8
9
$myDep = new DepClass();
$myConsumer = new class () use ($myDep) extends ConsumerClass {
    private $dep = $myDep;
 
    protected function getDep(): DepClassInterface
    {
        return $this->dep;
    }
};

我该如何测试这样一个类呢?

使用诸如 anonymous classes (见上例) - 或是一个 mock framework,比如 PHPUnit的:

1
2
$consumer = $this->createMock('ConsumerClass')->setMethods(['getDep'])->getMock();
$consumer->expects($this->any())->method('getDep')->will($this->returnValue($myDep));

让人们开启他们的类去迎合getter注入,难道不是在提倡坏实践吗?

在以不好的方式来做事时,人们往往更有创造力。getter injection也不例外。

由于这样会在底层注入容器,我们能否直接把容器注入?

当容器把 “你的类正在使用的依赖”给隐藏起来时,注入容器 (或是 "service locator" 设计模式) 是个坏实践: 在代码中 $container->get('foo')->doFoo() 这样写的话,会依赖几个执行乏力的假设(loosely-enforced assumptions),换句话说就是:

  1. 那个 $container 有一个 get 方法,通常由你框架的DI组件提供(已经与容器耦合了);
  2. 那个 get 方法有一个名为 foo 的服务 (这已经与外部配置信息耦合了);
  3. 同时那个 foo 服务有一个 doFoo 方法 (这同样与外部配置信息耦合)。

如果上述任意假设不再成立,则这样的代码便很脆弱:代码实际运行之前,错误难以发现(重构的噩梦)。

Getter注入(对比 constructor/setter injection)让你的类从类似问题中解脱出来:这个扩展的特点是,原生就是显式,并具备返回类型的提示(需要PHP7)。通过使用兼容的DIC,基于你的代码中的那些显式声明,代码可以自动生成。

我还是觉得它太糟糕

没问题!这个功能是可选的,你的Symfony程序可以不使用它。