When I first heard the term “Autowiring”, I thought it sounds exciting. But when I learned more or less what is it about, I got pretty sceptical of the idea. Too much magic, too much implied information... However, when I finally used it for the first time... Gosh I wish I could never define services manually again!
Dependency Injection Container is definitely my favourite design pattern – so elegant, simple and flexible!
It comes with one inconvenience though: you basically need to code the same thing twice. If your service requires some dependency, it cannot just take it or just create it. Instead, it should expect its dependencies, while the container provides them from the outside. When you want to add a new dependency, you need to both add it in the class and in some external service definition. This doesn’t look like much effort, but when you’re intensively playing around with your services, it all adds up. I didn’t even realise, how annoying that process is, until I no longer had to follow it.
When I started a fresh project recently, using the latest version of Symfony after over a year of working almost exclusively with Phalcon and Micrus, I had already forgotten about those two new features of Symfony: autowiring and autoconfiguration. I remembered by accident.
I needed a Twig extension, so I quickly created one: simple class, extending
\Twig_Extension, expecting some other service in its constructor, providing some filter in
getFilters(). I refreshed the tab in the browser and immediately realised I’m gonna see an exception – in the end, I forgot to tag this class as a
twig.extension, I forgot to provide the dependencies, it has to fail! But no, it just worked!
The idea behind autowiring is pretty simple: if a service requires an instance of
AppBundle\Api\CmsService, and your DI container happens to have a service called
AppBundle\Api\CmsService, which is of type
AppBundle\Api\CmsService, then it’s pretty safe to assume, that’s the one you want.
Ideally, you should depend on abstractions, interfaces. For instance when you have multiple cache adapters, all implementing
Psr\Cache\CacheItemPoolInterface, your services should depend on
Psr\Cache\CacheItemPoolInterface, not on any specific implementation. This way you can always switch for instance from
RedisAdapter without any trouble.
But if you have more than one implementation of an interface, how does the autowired container know, which one to use? You tell it, by creating an alias:
And what about the scalars and arrays? If my service expects
string $baseUrl, how does the container know that? Of course it doesn’t, I need to specify it myself:
AppBundle\Service\ExternalSerive: arguments: $baseUsr: 'https://api.external.org/'
You can do the same thing for services as well. For instance when all of them should use some generic logger, except for one, which requires something more specific, just overwrite its
arguments.$logger with the other one.
Pretty simple, right? The idea behind autoconfiguration is even simpler. You (or libraries, like Twig) can define that all the services which are an instance of
Foo should automatically receive some specific config, for instance a tag. That’s why I didn’t have to register my Twig extension.
Main concern one might have about autowiring is that, although pretty simple, it sure is time-consuming. Should I sacrifice my website’s performance for the developer’s convenience? Of course not! Symfony’s container is compiled, so autowiring has absolutely no overhead on production!
Also, Symfony Container has a concept of public and private services. Public ones you can just fetch from the container wherever you wish. Private ones can only be injected into other services – if they aren’t used, they get removed from the container completely.
Autowiring is amazing. Simple, but effective. It makes the development faster, especially in the early phase. It prevents you from repeating yourself and maintaining a lengthy
services.yml file full of information noise. And, although magical, it’s not magic!