How To: Symfony Autowire

This video is available to view for members only.

Click here to Join!

Already a member?

Login


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:

  1. There's a learning curve involved with this file
  2. 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.


Share This Episode

If you have found this video helpful, please consider sharing. I really appreciate it.