At the Symfony Live Berlin 2012 last week I joined a discussion about handling dependencies in services. One person mentioned that he used to inject the whole dependecy injection container (DIC) to a service in some cases. The service itself requests one or more services from the injected container and use it then for internal stuff.
The service pulls his dependencies here. However dependencies should always be pushed and injected directly in the constructor or with a setter if possible.
by Frank Stelzer*
In this article I first explain why in some cases it is needed to pull dependencies in Symfony2 and later why you should avoid this in all other cases. I joined the great workshop “testable code” Kore and Toby hold at the Symfony Live and I got motivated writing this article furthermore. So, thank you for that
Cases a class needs the DIC to be injected
When you have read the cookbook entry “How to work with Scopes” you might remember that there is described passing the DIC to a service when it needs internally some other services within a narrower scope. For Symfony2 it is the request instance in the most cases, as it is defined in a seperate scope for good reason.
If you do not read the cookbook entry yet, just let me quote its most important fact here:
If your service depends on a scoped service, the best solution is to put it in the same scope (or a narrower one). Usually, this means putting your new service in the request scope.
But this is not always possible (for instance, a twig extension must be in the container scope as the Twig environment needs it as a dependency). In these cases, you should pass the entire container into your service and retrieve your dependency from the container each time you need it to be sure you have the right instance.
In other words that means – especially for Symfony2:
Do never ever inject the complete DIC to a service when you do not need the request there!
- When you need the logger, just inject the logger service from Symfony.
- When you need the mailer, just inject the mailer service. It’s that easy!
- When you are writing a twig extension and need the request, just inject the request….. Ah, stop! That won’t work. In this very special case this is not possible. As you can read in the cookbook entry, any twig extension has to be in the container scope and it is not allowed to define such service in the request scope. So the only possibility is to inject the DIC here.
So, I think you got the idea. When you are in the need of injecting the DIC and when not. Statements exist, which claim that one could solve the request injection with the help of proxy classes. However I never have seen code about that. When you know how to do it or if there is another blog post out there, please drop me some hint in the comments.
In the next section I explain why it is not a good idea pulling your dependencies in any other case.
Cases pulling dependencies should be avoided
Let’s imagine some simple class which needs a logger and mailer somewhere. The creation of the mailer message is directly put there for simplicitiy, please ignore it.
Those few lines look not very bad on the first view. Logging and mailer logic is de-coupled and their services are used. The class uses dependency injection -somehow- and creates no new instances on its own or even worse calls some static helpers (Logger::debug()…).
But, and please read the next sentence very carefully:
As the class uses the DIC you create implicit outgoing dependencies to all services and classes defined in the container!
There is no official metric for this implicit dependencies. Outgoing dependencies however are known as Efferent Coupling and its value is the count of all those dependencies. You should be aware of it!
Do not mix this metric up with the almost equal pronounced Afferent Coupling, which represents the count of all incoming dependencies of a class.
When you use a class which gets the DIC injected, you never know which dependencies are used internally without checking the code. It could use templating, translation, the session any other defined service or even nothing. You just do not know. If you want to provide a robust API for this class this is consequently a no-go.
Let’s write some test for making this problem more obvious. Actually this is no real test, I only want to demonstrate the usage problem of the class.
Regaring the API of the class this test should be fine and work. But ah …
It won’t as the created container does not know the needed logger service.
So, you have to define the logger service first in your test container and try it again.
Damn it, it won’t work either, as the mailer service is undefined in the test container too. Just another dependency the class did not supposed to have.
After modifying the test the second time and adding the definition of the mailer service the test will pass.
Yeah, we did it! But was it abvious how the test has to be written from be beginning? No! It is no big deal implementing this test on its own, but in the case the test is written by some other developer the one or the other WTF moment will certainly arise. Using a new service in your class will break your test again as your injected container knows nothing about this third service. Running the risk of creating regressions is very high so.
This simple demonstration has shown that the class has more dependencies as it originally seemed or worse you don’t even know the exact dependencies.
The solution for this problem is quite easy: All dependencies which are pulled within the class have to be injected directly in the constructor (or provide some setter for them) and the dependency to the container has to be removed completely. Now for every usage all dependencies have to be pushed to the class. With the help of the DIC this is done within seconds. Seconds which later on could save minutes or hours when the project grows up in size.
Speaking about time, another developer told me this method might be too much time consuming especially during debugging as for example for every class the logger service or any other new created service has to be injected additionally if needed. But I think this no real argument as those changes do not waste that much time and could be done very easily.
One side note about the former mentioned Efferent Coupling:
After modifcation the class has certainly a higher Efferent Coupling value. Here it raises from 2 to 3. In the original class there existed only dependencies to the ContainerInterface and the Swift_Message class. The cleaned version points to LoggerInterface, Swift_Mailer and the Swift_Message. This change of the Efferent Coupling is ok though. It is better to raise this value a bit and having no implicit dependencies than otherwise. If you want to check up this metric on your own you may have a look at PHP Depend.
Do not hesitate to drop me some comments why and if you are in the need of pulling dependencies or to inject the DIC in your services furthermore. Though pushing dependencies should always be the way to go!
* Frank Stelzer (@frastel) has been working with PHP since 2001 and fell in love with Symfony during his university studies more than 6 years ago.
After his degree he started to work in the eSports industry and developed for a high load platform. Focusing on performance, clean code, unit testing or other aspects of code quality he loves to review and discuss about code.
For increasing his support in the Symfony world Frank joined the SensioLabs Deutschland Team in 2011 where he is working as a software architect.
Feel free to contact if you’re interested to write your own guest post!