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 HttpKernelSessionListener::getSession()
in HttpKernel alsoAbstractBaseFactory::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:
- Using inheritance makes them free from any coupling with any framework/DIC (as opposed to injecting the container);
- It makes injected dependencies immutable (as opposed to setter/property injection);
- 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);
- 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);
- 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);
- It makes dependencies explicit (as opposed to injecting the container);
- It allows to declare mandatory dependencies by using abstract getters;
- It allows to have optional dependencies by providing default getters that
return
null
or a default implementation (e.g. aNullLogger
); - 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;
- The proxies required to leverage the injection points are easy to write (see example) or generate;
- The proxies do not need to change when the injected dependencies change themselves (as opposed to using decorators when dealing with laziness);
- It adds no performance overhead when using the injected dependencies (as opposed to using decorators when dealing with laziness);
- 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
- It requires to write an inheritance proxy, thus adds more boilerplate than the other injection strategies when wiring or testing these classes;
- By design, it doesn't work with final classes nor with private methods;
- 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; - 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;
- It adds indirection, thus can make debugging harder (Why is this class throwing?, "Oh, initialization of this lazy dependency fails at that time");
- When the inheritance proxy provides laziness, it can create circular dependencies in the PHP object graph, making garbage collection required at some point;
- 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:
- That
$container
has aget
method, usually provided by your framework's DI component (that's coupling to it); - That the
get
method has a service namedfoo
(that's coupling to external configuration); - And that the
foo
service has adoFoo
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.
In Drupal 8, a web request can change an existing container so we had some situations in past where getter injection pattern was problematic and we came up with
A very good example is a Drupal testing system where we had a discussion about using
$this->container vs \Drupal::getContainer()
https://www.drupal.org/node/2066993.@Jibran thanks for the link, that looks related indeed. Using getter injection as implemented in Symfony 3.3 would mean using the not-global $this->container->get('string_translation') way, so that may be a good fit for Drupal at some point when it'll use 3.4LTS maybe?
Very nice for the Doctrine entities!
Hi ! I suppose it is possible to autowire dependencies by means of method's return type declaration in php7 or its dockblock in php5. You can consider any absctract getter function as a candidate to implement in the Proxy.
An another pros I think is that the getter injection allows circular dependencies. If service A depends on B and vice versa, then when A tries to obtain B, we can be sure that A exists.
@Vlad you got it right! Autowiring considers the php7 return type, abstract methods, and also a new
@required
annotation (see https://github.com/symfony/symfony/pull/21763). It does not parse the@return
annotation thought, that'd be too heavy to parse for a core component like DI.👍