How To: Symfony Autowire
Symfony as a whole has a reputation for being verbose. It can certainly feel like there's a lot of 'stuff' that needs to be in place for things to work. And this can make the initial process of learning Symfony seem that much more difficult.
However, that verbosity in configuration translates to explicitness in exactly how your application works. It gives you the ability to open pretty much any Symfony project, open a few key files (config.yml
, services.yml
, routing.yml
, as examples), and gain a fairly decent high level overview of just what might be happening in this application's code.
I also fully understand that this setup doesn't work for everyone. There are plenty of frameworks in plenty of languages that I've looked at, and experimented with, that just simply never seem to 'gel' with me. I would hazard a guess it's the same for you?
Somewhere in the opposite direction of explicitness is "magic".
By following the given framework's conventions, you can achieve big thinks with little effort. Fantastic! Until things go wrong. And you suddenly need to understand the internals to figure out why your code isn't behaving.
Again, this is all opinion.
Why do I bring this up at all?
Because what if I could show you how to add a little magic to Symfony?
Intrigued?
Then let me show you one of the nicest features of Symfony that I never use.
Enter Autowiring
Let's start with an example:
# /app/config/services.yml
services:
first_service:
class: AppBundle\SomeDirectory\First
arguments:
- "@logger"
- "@some_custom_rep"
- "@event_dispatcher"
- "@second_service"
second_service:
class: AppBundle\DifferentDirectory\SecondService:
arguments:
- "@logger"
- "@api_client"
- "@third_service"
third_service:
class: AppBundle\AnotherDirectory\ThirdService:
arguments:
- "@logger"
# api_client:
# ...
# some_custom_repo:
# ...
This is fairly typical of any Symfony project.
Services are used in dare I say, every real world Symfony project in some form or other.
Having to type out all that config has two drawbacks:
- There's a learning curve involved with this file
- This file can become quite large
Let's address point #2 first.
Yes, it can. There are solutions to this. For example, you could split services into their own sub-directories, and files:
# /app/config/config.yml
imports:
- { resource: parameters.yml }
- { resource: security.yml }
- { resource: services.yml }
- { resource: services/converter.yml }
and then:
# /app/config/services/converter.yml
services:
convert_to_object:
class: AppBundle\Converter\ConvertToObject
convert_to_yaml:
class: AppBundle\Converter\ConvertToYaml
And that works just fine.
The downside now is that you've added another file to your project, and also - potentially - increased the mental burden of working with your project, just ever so slightly. However, of course these little mental burdens are cumulative.
Anyway, switching back to our original example: what if instead of all that code, you could have:
# /app/config/services.yml
services:
first_service:
class: AppBundle\SomeDirectory\First
autowire: true
This could be incredibly beneficial to you if working on a prototype, fleshing out an idea, moving fast and breaking things.
A Simple(?) Example
Let's look at a really basic way of using autowiring, and why even with this basic example, the trade offs of using autowiring become evident.
Imagine we have this service definition:
# /app/config/services.yml
services:
first_service:
class: AppBundle\Service\FirstService
arguments:
- "@logger"
And the associated class:
<?php
// /src/AppBundle/Service/FirstService.php
namespace AppBundle\Service;
use Psr\Log\LoggerInterface;
class FirstService
{
/**
* @var LoggerInterface
*/
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function doSomethingInteresting()
{
$this->logger->debug('hello from First Service');
return true;
}
}
Rather than explicitly specify the arguments
in first_service
's service definition, we could autowire
this service:
# /app/config/services.yml
services:
first_service:
class: AppBundle\Service\FirstService
autowire: true
We gain a shorter service definition, the benefits of which will be compounded as we inject more 'things' into FirstService
.
However, we have lost the explicitness. We have lost the documentation here that FirstService
depends on the @logger
service.
Now, if you think about it, the @logger
service is already a bit of a black box. If you haven't read the documentation, you could guess it's some form of logging utility. And hitting the docs would reveal that Symfony uses Monolog.
But here's the confusing part: We don't explicitly depend on Monolog
in FirstService
's contructor. We depend on PSR-3's LoggerInterface
. Monolog just happens to implement that interface.
Hmmm.
Clever? Yes.
Confusing? I'd say just as much.
But don't dwell on that for the moment, as we will cover why this works shortly.
Going A Level Deeper
Imagine our system is growing, and FirstService
now needs to split some of its work into sub-services. We can do that the explicit way:
# /app/config/services.yml
services:
first_service:
class: AppBundle\SomeDirectory\First
arguments:
- "@logger"
- "@second_service"
second_service:
class: AppBundle\DifferentDirectory\SecondService:
arguments:
- "@logger"
- "@event_dispatcher"
And we would need to update our constructor to accept an instance of SecondService
:
<?php
// /src/AppBundle/Service/FirstService.php
namespace AppBundle\Service;
use Psr\Log\LoggerInterface;
class FirstService
{
/**
* @var LoggerInterface
*/
private $logger;
/**
* @var SecondService
*/
private $secondService;
public function __construct(LoggerInterface $logger, SecondService $secondService)
{
$this->logger = $logger;
$this->secondService = $secondService
}
public function doSomethingInteresting()
{
$this->logger->debug('hello from First Service');
$this->secondService->doAnotherThing();
return true;
}
}
Ok, this works fine.
However, we could instead just continue with our existing autowire
enabled service definition instead:
# /app/config/services.yml
services:
first_service:
class: AppBundle\Service\FirstService
autowire: true
Yup, no need at all to even tell services.yml
about SecondService
.
This time though, not only are the two dependencies inferred from the constructor of FirstService
, the SecondService
is detected by association. Symfony will create a fully functioning private service definition for SecondService
, and inject as instance into FirstService
. This includes the dependencies of SecondService
.
And this can go on, and on. So long as the autowirer can guess what service you want, it will add it for you. It's a really, really smart concept.
The trade off - again - becomes hunting down exactly what is doing what. This time you have to open a bunch of files, following the constructor arguments through to gain an understanding of how each piece fits together.
Then there's the private services. You can see these easily enough by using the console command of:
php bin/console debug:container --show-private
Symfony Container Public and Private Services
=============================================
---------------------------------------------------------------------- --------------------------------------------------------------------------------------------
Service ID Class name
---------------------------------------------------------------------- --------------------------------------------------------------------------------------------
annotation_reader Doctrine\Common\Annotations\CachedReader
annotations.reader Doctrine\Common\Annotations\AnnotationReader
assets.context Symfony\Component\Asset\Context\RequestStackContext
assets.packages Symfony\Component\Asset\Packages
b9060dc118ca23048969c04f8e53884b8f741d18ed763ea6017d233d08ef00f3_1 AppBundle\Service\SecondService
... etc
And you can even inspect that private service definition more throughly:
php bin/console debug:container b9060dc118ca23048969c04f8e53884b8f741d18ed763ea6017d233d08ef00f3_1
Information for Service "b9060dc118ca23048969c04f8e53884b8f741d18ed763ea6017d233d08ef00f3_1"
============================================================================================
------------------ --------------------------------------------------------------------
Option Value
------------------ --------------------------------------------------------------------
Service ID b9060dc118ca23048969c04f8e53884b8f741d18ed763ea6017d233d08ef00f3_1
Class AppBundle\Service\SecondService
Tags -
Public no
Synthetic no
Lazy no
Shared yes
Abstract no
Autowired no
Autowiring Types -
------------------ --------------------------------------------------------------------
This again is fine, if you are comfortable with Symfony. But consider others who need to work on the project also. Will this save them as much time as it has done for you? More on this, shortly.
Is It Time To Transition?
The Symfony documentation is fairly clear that autowiring is best used for Rapid Application Development:
... which is useful in the field of Rapid Application Development, when designing prototypes in early stages of large projects. It makes it easy to register a service graph and eases refactoring.
Inevitably after the very busy / hectic first stages of a project, things begin to settle down. What was a little hacked together prototype starts to settle down and take a more refined definition. If you're really strict, you might throw away the prototype and re-write it all using TDD.
But many prototypes become the real code.
Anyway, let's look at an example of where you might want to start transitioning from the RAD / autowired approach, into the more traditional explicit service definitions.
Imagine our business is in the rapidly growing conversion sector.
Our core business model is to convert things. Currently, we only offer conversion of array to CSV, but our multimillionaire dragon investors expect us to up our offering to support 5 conversion options in total. And that's just the beginning! Arghh the perils of taking funding.
The investors meet with the CEO and CTO, who in turn meet with your boss, who in turn tells you that we must now support the following conversion options:
- YAML
- JSON
- XML
- PHP Objects
- and the original CSV.
Being good developers, we immediately think we better standardise our code on a definitive interface:
<?php
namespace AppBundle\Converter;
interface ConverterInterface
{
public function convert(array $data);
}
We've got our services autowired, so we can just update the constructor to depend on ConversionInterface
, and things should just still work:
# /app/config/services.yml
services:
first_service:
class: AppBundle\Service\FirstService
autowire: true
And FirstService
itself:
<?php
// /src/AppBundle/Service/FirstService.php
namespace AppBundle\Service;
use AppBundle\Converter\ConverterInterface;
use Psr\Log\LoggerInterface;
class FirstService
{
/**
* @var LoggerInterface
*/
private $logger;
/**
* @var ConverterInterface
*/
private $converter;
public function __construct(LoggerInterface $logger, ConverterInterface $converter)
{
$this->logger = $logger;
$this->converter = $converter;
}
public function doSomethingInteresting()
{
$this->logger->info('Interesting things are happening!');
return $this->converter->convert([
'a' => 'b',
'c' => 'd',
]);
}
}
We decide that we can add the JSON converter in this sprint, and if we stretch we might also get conversion to PHP Objects implemented too.
Development begins in earnest, and we end up with two new working and well-tested implementations. Hurrah.
Now then, how do we go about using these?
RuntimeException in AutowirePass.php line 256: Unable to autowire argument of type "AppBundle\Converter\ConverterInterface" for the service "first_service". Multiple services exist for this interface (convert_to_csv, convert_to_json, convert_to_object).
Yikes.
Ok, a dig around the manual reveals that when we have multiple services all implementing the same interface, we can specify a default service to use with the autowiring_types
option. But first, we will need to add in a service definition for each of the converters we have in our project:
# /app/config/services.yml
services:
first_service:
class: AppBundle\Service\FirstService
autowire: true
convert_to_csv:
class: AppBundle\Converter\ConvertToCsv
autowiring_types: AppBundle\Converter\ConverterInterface
convert_to_json:
class: AppBundle\Converter\ConvertToJson
convert_to_object:
class: AppBundle\Converter\ConvertToObject
It is in my opinion that at this stage, you should start seriously considering using the explicit service definitions. And I would strongly advise you to do so across your entire project. Don't have some services autowired, and some services explicit.
Now, if you think back to when we initially injected LoggerInterface
, and yet we didn't need to bother with autowiring_types
, how come that worked?
Well, that's because Monolog is the only class implementing that interface in our entire project. We likely never need an alternative logger, so it's a safe assumption to make.
Wrapping Up
Anyway, that's about it for autowiring.
It's available, and has been since Symfony 2.8.
It works, and is easy to enable and start using.
However, personally I do not use it as I prefer the explicit configuration that Symfony is somewhat known for. This is my opinion, and you are completely free to differ, of course.