Symfony's autowiring

Posted on 19 Sep 2017
Tagged with: [ symfony ]  [ autowire ]  [ magic

When asking people if they use Symfony’s autowiring functionality, an often heard excuse why people don’t use it is because of all the magic that is happening during autowiring. But just like most impressive magic tricks, once explained, it all boils down to a few simple principles and Symfony’s autowiring is nothing different in that perspective. In this blogpost I will explain the new autowiring and autoconfiguration features, and why you should love them.

Defining services manually

You should already be familair with how we create services in Symfony: we must define the name of the service and the class to which the service point to. This is normally done in configuration files like services.yml. These files can often be found within Symfony bundles, and are nothing more than a (long) list of services and their corresponding classes.

Once Symfony collected all these services, it will compile a service container with all services which then be used within your Symfony project.

# services.yml
mailer:
    class: AppBundle\Service\MailerService

renderer:
    class: AppBundle\Service\RenderService

When calling a service, Symfony tries to find the corresponding name in this service container, instantiate the corresponding class if not done so already, and returns it for you to use.

Let’s suppose I have a service that uses another service: a mailer service for instance may want to use the template render engine service in order to generate nice mail layouts. Symfony allows us to “connect” this template renderer to the mailer service by “injecting” it inside the mailer service.

	class MailerService {
	  protected $renderer;
  	
	  function __construct(RenderService $renderer) {
	    $this->renderer = $renderer;
	  }
  	
	  function sendMailing(string $template, array $data) {
	    $mail = $this->renderer->render($template, $data);
	    $this->send($mail);
	  }
	}

In this example the render service is injected as a dependency into the mailer service. The mailer service itself isn’t aware of what kind of renderer this is: it might be a Twig template renderer, a smarty template engine, or even a plain PHP renderer. This allows for decoupling of services.

Defining these dependencies is done in the same services.yml files, but we explicitly add dependencies to the services that needs them:

# services.yml
mailer:
    class: AppBundle\Service\MailerService
    arguments: [ "@renderer" ]

renderer:
    class: AppBundle\Service\RenderService

The arguments option in the services.yml above defines which services (the renderer service in this case) must be injected in the class. Symfony takes care of the correct order of things. When calling the mailer service, it will first instantiate the renderer class before it instantiate the mailer class.

Automatic generation of services

Now that we know how services are used within Symfony, let’s talk about autowiring. There are a few “tricks” happening during the autowiring process. The first thing is that Symfony automatically creates services from classes automatically without specifing this inside your services.yml file.

Obviously, symfony cannot know what you like to call those services (like renderer or mailer), but instead it will register each class it finds as the fully qualified class name (FQCN). Our mailer service would automatically be registered as the service AppBundle\Service\MailerService.

Symfony doesn’t do this automatically for all your classes: you must tell Symfony which directory or classes you want to autowire. This can be done withing your services.yml:

services:

    AppBundle\Service\:
        resource: '../../src/AppBundle/Service'

With this in place, we tell Symfony that the all classes found inside the \AppBundle\Service. For instance, Symfony would automatically create the \AppBundle\Service\MailerService and the \AppBundle\Service\RenderService service if these two services were inside the Service directory.

If we need to inject arguments, we still must do this manually:

services:

    AppBundle\Service\:
        resource: '../../src/AppBundle/Service'
        
    mailer:
        class: AppBundle\Service\MailerService
        arguments: [ "@AppBundle\Service\RenderService" ]

Here we have defined a mailer service, which uses the RenderService as a dependency.

Autowiring dependencies

Symfony’s second trick is what actually makes the autowiring. If we take a look back to our mailer class, we notice that the constructor uses a dependency:

    function __construct(RenderService $renderer) {

Since we use a typehint to an object, PHP expects you to pass an \AppBundle\Service\RenderService instance to this constructor. During autowiring, Symfony will look in the service container to see if there is such a service defined. If so, it will automatically inject that specific service into the constructor, even though you haven’t configured this service and added any arguments to it.

This is what Symfony’s autowiring functionality is all about: Symfony automatically wires the dependencies based on their fully qualified class name.

In order for Symfony to autowire dependencies, you must explicitly tell this inside the service container:

    services:
        AppBundle\Service\:
            resource: '../../src/AppBundle/Service'
            autowire: true

In this example, all services found in the AppBundle\Service directory will automatically be autowired.

When autowiring doesn’t work

It’s possible that autowiring doesn’t always work. This can happen when you need to inject a class and you do not have that class as a service defined in your service container. Or, it might also be possible that multiple services of the needed type exist. This happens when you typehint on for instance interfaces, or when you extend a class into multiple subclasses.

Suppose I have an interface called ImageLoaderInterface, and three concrete classes: JpgLoader, GifLoader and PngLoader. Now we have a generic ImageProcessor class, that depends on one of the loaders:

    class ImageProcessor {
    	function __construct(ImageLoaderInterface $loader) {
	    	// ...
    	}    
    }

If we used autowiring, Symfony cannot define which of the loaders needs to be autowired and will ask you to explicitly define this inside your service configuration. You could do this manually, but you could also create an alias to one of the loaders:

services:
    AppBundle\Service\:
        resource: '../../src/AppBundle/Service'
        autowire: true
        
    AppBundle\Service\ImageLoaderInterface: '@AppBundle\Service\JpgLoader'

This way, whenever symfony finds the ImageLoaderInterface typehint, it knows that it’s connected to the JpgLoader.

When Symfony cannot autowire services, it will throw an exception and allows you to manually fix the issue inside your service definitions.

Autoconfiguration

Autoconfiguration is closely related to autowiring. Where autowiring will automatically connect any dependencies to services, autoconfiguration will actually configure those services automatically. This isn’t always needed, but you will see often that autoconfiguration is used for automatically adding tags to services.

Normally, when creating things like twig extensions, or custom form types, or even controllers as services, you’ll need to tag them with for instance twig.extension, form.type, kernel.subscriber etc. This allows the container to find services tagged with specific tags during compilation of the container. The twig service for instance tries to find all services tagged with twig.extension, and add those services as twig extensions so you can automatically use those extensions within Twig.

    services:
        my.twig.extension:
            class: AppBundle\Twig\Extension\MyExtension
            tags:
              - { name: twig.extension }

When using autowiring, many services are created automatically, so there is no way to add tags, unless you add all these extensions manually, which kind of ruins the whole point of autowiring.

With autoconfiguration, you can automatically, well, configure these kind of services. For each service that matches a certain class, parent class or interface (basically, if it matches the “instanceof” check), you can autoconfigure services.

For example, any twig extension you create must implement the Twig_ExtensionInterface. Inside the main twig service, we configure that any service we discover and implements this interface, will be automatically tagged with twig.extension. This is done in TwigExtension.php:

	$container
		->registerForAutoconfiguration(\Twig_ExtensionInterface::class)
		->addTag('twig.extension')
	;

This code autoconfigures services that are an instance of the Twig_Extension class and automatically tags them with twig.extension.

A lot of these autoconfigurations can be found within the FrameworkBundle:

        $container->registerForAutoconfiguration(Command::class)
            ->addTag('console.command');
        $container->registerForAutoconfiguration(ResourceCheckerInterface::class)
            ->addTag('config_cache.resource_checker');
        $container->registerForAutoconfiguration(ServiceSubscriberInterface::class)
            ->addTag('container.service_subscriber');
        $container->registerForAutoconfiguration(AbstractController::class)
            ->addTag('controller.service_arguments');
        $container->registerForAutoconfiguration(Controller::class)
            ->addTag('controller.service_arguments');
        $container->registerForAutoconfiguration(DataCollectorInterface::class)
            ->addTag('data_collector');
        $container->registerForAutoconfiguration(FormTypeInterface::class)
            ->addTag('form.type');
        $container->registerForAutoconfiguration(FormTypeGuesserInterface::class)
            ->addTag('form.type_guesser');

This automatically tags commands, controllers, form types, guessers and a whole lot more.

Autoconfiguration your own instances

Besides using the registerForAutoConfiguration method from the service container, it’s also possible to add your own autoconfiguration directly in your services.yml. For instance, let’s suppose you want to autoconfigure any instance of ImageLoaderInterface with the tag image.loader:

services:
    ....

     _instanceof:
        AppBundle\ImageLoaderInterface:
            tags: ['image.loader']
            public: false

Now, any service that is found and implements the ImageLoaderInterface, will be automatically tagged with the image.loader tag, AND will not be public services. There are more autoconfiguration options you can set, but often you will see this for tagging services mostly.

Note that you need to add these to the special _instanceof section in your services.yml.

Setting defaults

Sometimes you need to set certain defaults for each service you define. This can be done with the special _defaults section in your services.yml:

    services:
        _defaults:
            autowire: true
            autoconfigure: true
            public: false

This will automatically autowire and autoconfigure all services found in the current services.yml file, and will set them to private services.

Notes

There are a few things you need to consider when using autowiring:

  • They do not work on scalar typehints, only objects. Meaning you cannot autowire strings, ints or booleans for example. Symfony will automatically throw an exception in case it runs into this.

  • Contrary to many people’s believes, autowiring does not impose a performance hit since all services are eventually compiled into a cacheable container file. However, when in dev mode, it might recompile more often when you modify classes but unless you have very large projects, that should not be a problem.

  • There is absolutely no need to use autowiring. Everything with autowiring can be done with manually defining services too. Autowiring just removes the need to writing large service files with boilerplate. You are not missing anything out when you opt for not using autowiring.

  • Autowiring is not magic. It’s just Symfony trying to automatically inject services with other services based on their fully qualified class name.