Nicolas Grekas
Contributed by Nicolas Grekas in #20973

WARNING: This feature was reverted before the release of Symfony 3.3, so no stable Symfony version ever supported it.


As part of our experimental features program, in Symfony 3.3 we've added a new feature called getter injection. This adds up to the usual mechanisms used for dependency injection and doesn't replace any of them. Instead, it provides an additional way that fits some specific use cases.

Getter injection allows the dependency injection container to leverage classes that provide inheritance-based extension points that matches the following requirements: public or protected methods with zero arguments and free of side-effects.

Some examples found while grepping Symfony and its vendors:

  • Kernel::getRootDir/CacheDir/LogDir() in HttpKernel
  • SessionListener::getSession() in HttpKernel also
  • AbstractBaseFactory::getGenerator() in ProxyManager

This is only a small subset of all the classes that apply this flavor of the open/closed principle in Symfony core and elsewhere. As shown in the examples, this applies both to objects injection (services) and to values injection (parameters).

Getter injection is a way to turn these classes into DI candidates via simple DI configuration. In Yaml, taking the `SessionListener::getSession()` example, this could look like:

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

In practice, this tells the Symfony Dependency Injection Container to create an anonymous inheritance-proxy class like this one:

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

Pros of using getter injection

Classes designed with getter injection in mind have several advantages over the other IoC strategies:

  1. Using inheritance makes them free from any coupling with any framework/DIC (as opposed to injecting the container);
  2. It makes injected dependencies immutable (as opposed to setter/property injection);
  3. By requiring to always use the getter to fetch some dependencies internally, it allows for lazy instantiation of said dependencies (as opposed to constructor/setter/property injection);
  4. It doesn't pollute the constructor, which is an advantage for classes designed for extensibility (same as setter/property injection - and as opposed to constructor injection);
  5. It plays well with traits-based composition, each trait providing a new set of open getters (same pro for setter/property injection - and as opposed to constructor injection);
  6. It makes dependencies explicit (as opposed to injecting the container);
  7. It allows to declare mandatory dependencies by using abstract getters;
  8. It allows to have optional dependencies by providing default getters that return null or a default implementation (e.g. a NullLogger);
  9. It allows to have and report missing dependencies that are conditionally required when using some subset of the features provided by the base class (typically a controller or a command) - by throwing a useful exception message when that happens;
  10. The proxies required to leverage the injection points are easy to write (see example) or generate;
  11. The proxies do not need to change when the injected dependencies change themselves (as opposed to using decorators when dealing with laziness);
  12. It adds no performance overhead when using the injected dependencies (as opposed to using decorators when dealing with laziness);
  13. When using anonymous or generated proxy classes, it doesn't create any new type to hint for, thus is free from inheritance hell.

Cons of using getter injection

  1. It requires to write an inheritance proxy, thus adds more boilerplate than the other injection strategies when wiring or testing these classes;
  2. By design, it doesn't work with final classes nor with private methods;
  3. Since PHP (unlike other languages) doesn't provide any way to create proxies at runtime, it requires either hand written code, a dumped container, or using eval() for runtime-based DICs;
  4. When laziness is a target, it gives the laziness responsibility to the caller-side of dependencies (as opposed to callee-side when using decoration) - thus it should be used only when laziness is part of the core business of the class open to getter injection;
  5. It adds indirection, thus can make debugging harder (Why is this class throwing?, "Oh, initialization of this lazy dependency fails at that time");
  6. When the inheritance proxy provides laziness, it can create circular dependencies in the PHP object graph, making garbage collection required at some point;
  7. If the side-effect-free requirement is not met, it will lead to bugs - when not using abstract getters, this requirement is technically hard to enforce (requires static code analyzes).

Discussion

The introduction of "getter injection" was controversial and it generated both positive and negative feedbacks. I tried to sum up all rational arguments, pros and cons, in the lists above.

Having addressed them, I hope for the "surprise effect" to disappear over time and have getter injection be evaluated for its technical merits only. My personal point of view about it is that it fills a few holes with its unique pros/cons balance, and as such deserves being part of the developers' tool chain, with support from Symfony's container.

Envisioned use case in Symfony core

This feature is targeted at being used for framework-specific needs first. The target for now is to provide decoupled composable base controller trait(s). See pull request 18193 for what could be the next step on the topic.

FAQ

With typical DI, I can wire manually my dependencies if I need to. How can I do the same with such classes?

By using for example anonymous classes:

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

But that's a lot of boilerplate!

Yes, that's listed in the "cons". We use DICs to do the boring wiring - same here. Note also that with the help of the language, this could be reduced to the following (there is a discussion about that on php-internals, please support it):

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

How can I test such a class?

By using e.g. anonymous classes (see above) - or a mock framework, e.g. PHPUnit's:

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

Won't this encourage bad practice, by making people open their classes to fit getter injection?

People will always be creative to do things in bad ways. There is nothing specific about getter injection on that topic.

Since this injects the container behind the scene, can't we just inject the container directly?

Injecting the container (or the "service locator" design pattern) is bad practice when it hides the dependencies your classes are using: doing $container->get('foo')->doFoo() in your code relies on several loosely-enforced assumptions, namely:

  1. That $container has a get method, usually provided by your framework's DI component (that's coupling to it);
  2. That the get method has a service named foo (that's coupling to external configuration);
  3. And that the foo service has a doFoo method (that's coupling to external configuration also).

If any of those assumptions are not valid anymore, such code is fragile: any mistake can be undetected until the code is actually run (a refactoring's nightmare.)

Getter injection (as constructor/setter injection) makes your classes free from any such issues: the extension points are plain explicit and return-type hinted (you need PHP 7). By using a compatible DIC, the code is automatically generated, based on these explicit declarations in your code.

I still feel bad about it

No problem! Remember that this feature is optional and your Symfony applications don't have to use it.

Published in #Living on the edge