Categories


Archives


Recent Posts


Categories


Symfony: Autowiring Services

astorm

Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

Updated for Magento 2! No Frills Magento Layout is the only Magento front end book you'll ever need. Get your copy today!

Last time we covered the basics of Symfony’s service container. The service container has been with Symfony since the early days of Symfony 2, and will likely always be at the heart of the Symfony system.

However — programming fashion changes. These days it’s hard to walk out your PHP door and not trip over the phrase dependency injection. If you’re not familiar with automatic dependency injection it can be intimidating. Your programming language will seem to behave in a way that’s impossible.

Symfony developers face an additional challenge with dependency injection. The service container system was never designed with automatic dependency injection in mind, and this can lead to some configuration paths that are, (initially), not intuitive.

Hopefully by the end of this article we’ll have you set on the right path, and you’ll see that dependency injection isn’t all that complicated, and you’ll understand how Symfony’s configured to allow this magic feature into its convention based culture.

Dependency Injection

So what is dependency injection? I’m not the first person to say this, but the idea of dependency injection is a lot simpler than its fancy pants name. Another way to describe dependency injection is passing values into functions.

Consider the following code

function someFunction($foo) {
    // some stuff happens
    // ...

    // an object is instantiated and used
    $object = new \Foo\Baz\Bar($foo, $otherParam, etc...);
    $value = $object->doSomeThing();

    // some other stuff happens
    // ...

    // we return
    return $someValue;
}

This code directly instantiates an object inside a function, between a lot of other code. This pattern of work ends up being hard to maintain and change over time. This function is dependent on the Foo\Baz\Bar object, and dependent on the values Foo\Baz\Bar is instantiated with.

The idea behind dependency injection is it’s better to write code like this

function someFunction($foo, \Foo\Baz\BarInterface $object) {
    // some stuff happens
    // ...

    // The object that's injected is used
    $value = $object->doSomeThing();

    // some other stuff happens
    // ...

    // we return
    return $someValue;
}

This code passes (or “injects”) the object instead of instantiating it directly in the function. This means we can pass this function whatever object we want, so long as it implements the Foo\Baz\BarInterface interface (either an explicit interface like the one in the above example, or an implicit interface in an object that implements all the methods that get called in the function).

This pattern gives you more flexible functions, and functions that are easier to test. They’re easier to test because we can control what’s returned by the doSomething method, and ensure our unit tests handle different results from this method.

Where dependency injection gets tricky is something actually needs to instantiate the objects. When a system like Laravel, Magento, or Symfony tout dependency injection as a feature, they’re usually referring to systems that automatically create objects for us and pass them into object constructors, or pass them into methods or functions that are called automatically, (like controller action methods).

So, now that we’re sorted as to what dependency injection is, lets take a look at a basic example in Symfony.

Starting from Zero

The first thing we’re going to do is temporarily remove the default service configurations that ship with a Sylius (or a Symfony) system. These defaults turn on certain service features that we’re not ready to have turned on yet.

First, make a backup copy of your service configuration

$ cp config/services.yaml config/services.yaml.bak

Then, remove everything in your config/services.yaml file except the parameters section.

#File: config/services.yaml
parameters:
    locale: 'en'

This locale parameter is required by the Symfony framework.

Now we’re ready to start.

Setting up The Services

Similar to last time, we’re going to setup a controller endpoint that, using the global kernel object’s container reference, will instantiate a service and call its sayHello method. This service will call a second service.

If that was confusing be sure to read our previous article in this series. Also — don’t worry, you’ll be able to follow this “cookbook style” and still get the gist of what we’re doing.

So let’s configure our route.

#File: config/routes.yaml
dependency_injection_playground:
    path: /pulsestorm_di
    methods: [GET]
    controller: App\Controller\Pulsestorm\Di::run

and create the controller it references

#File: src/Controller/Pulsestorm/Di.php
<?php
namespace App\Controller\Pulsestorm;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

class Di extends AbstractController {
    public function run() {
        global $kernel;
        $container = $kernel->getContainer();
        $service = $container->get('App\Services\ServiceA');
        echo $service->sayHello();
        exit;
    }
}

Then, we’ll configure two services

#File: config/services.yaml
services:
    App\Services\ServiceA:
        public: true
        arguments:
            - '@App\Services\ServiceB'
    App\Services\ServiceB:
        public: true

And create their class files

#File: src/Services/ServiceA.php
<?php
namespace App\Services;
class ServiceA {
    protected $service;
    public function __construct($service) {
        $this->service = $service;
    }

    public function sayHello() {
        return $this->service->getMessage();
    }
}


#File: src/Services/ServiceB.php
<?php
namespace App\Services;
class ServiceB {
    public function getMessage() {
        return 'Hello Soon to Be Di Injected Services';
    }
}

and finally, we’ll load the following URL in our Sylius or Symfony system (replacing sylius.example.com with whatever URL you’re using in your system).

http://sylius.example.com/pulsestorm_di

We should see the Hello Soon to Be Di Injected Services message.

Nothing much new here, so let’s get on with the interesting bits.

Service Names and Class Names

You may have noticed something slightly different with our example today, and that’s our service names.

In our original article, we named our services with a generic lowercase string. Here we’ve named our services with a string that matches a PHP class name. This is perfectly legal, and in fact is required for Symfony’s automatic constructor dependency injection system to work.

Changing our service to inject the second service automatically is a two step process. First, we need to include a constructor method in our service class, with a type hinted argument for each service we want to inject. Let’s edit service A so its constructor looks like this

#File: src/Services/ServiceA.php
public function __construct(
    \App\Services\ServiceB $service
) {
    $this->service = $service;
}

and remove the service arguments from the configuration for service A.

#File: config/services.yaml
services:
    App\Services\ServiceA:
        public: true
    App\Services\ServiceB:
        public: true

Here we’ve type hinted the constructor argument with the class name of the service we want to inject. This is also the Symfony service name. By having our service name match the name of a valid PHP class, Symfony will be able to see the class name of the type hint, use this name to lookup a service name, and automatically pass the instantiated service when it (Symfony) instantiates the App\Services\ServiceA class.

All that said — there’s still work to do. If we reload the page with the above code in place, we’ll get an error

Too few arguments to function App\Services\ServiceA::__construct(), 0 passed in /path/to/var/cache/dev/Container9Nwhdth/getServiceAService.php

Unlike Magento or Laravel’s automatic dependency injection systems, Symfony services will not automatically inject service instances into a constructor. Instead, we need to tell Symfony which services should have automatic constructor dependency injection.

Automatic Wiring

Symfony’s name for its automatic constructor dependency injection system is autowiring — as in automatic wiring. Normally, with a Symfony service, you need to configure each argument manually. These are the “wires”. With autowiring, Symfony will look at the type hint and inject the right service for you.

To turn on autowiring for a service, you set its autowiring configuration to true. For example, to have our App\Services\ServiceA service automatically wired, we’d use the following configuration.

#File: config/services.yaml
services:
    App\Services\ServiceA:
        public: true
        autowire: true
    App\Services\ServiceB:
        public: true

If you make that change and reload your page, you’ll see an exception message is no longer thrown. We see our text! Symfony successfully injects a service instance in the constructor (using the type hint to decide which service to load) and we’ll see the Hello Soon to Be Di Injected Services output!

Congratulations, you just autowired your first Symfony service!

Autowiring and Services for All

Generally speaking, the philosophy of the Symfony project has been to prefer explicit configuration over automatically enabling features. The usual arguments for this are these features might impact performance, or might provide surprising behavior to end-user-programmers. However, in recent years, configuration options have started to appear that allow Symfony engineers to quickly turn on features as though Symfony was a more “magic by default” system.

Put another way — with more recent versions of Symfony, it’s possible to configure your system such that all services are autowired, and all PHP classes in your system will be automatically considered as services.

Remember our backup file, services.yaml.bak? Let’s take a look at that file again.

#File: config/services.yaml.bak
_defaults:
    autowire: true      # Automatically injects dependencies in your services.
    /*...*/

# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
    resource: '../src/*'
    exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'

Based on what we’ve talked about so far, you might think this configuration configures a service named _defaults and a service named App\. However, that’s not the case.

You can’t have a service named _defaults in Symfony. Instead, this key will set default properties for all your services. In the above example, every service we configure will have autowiring enabled unless the service explicitly disables it with a autowiring:false.

The App\: key is a little trickier. The plain english of this configuration

#File: config/services.yaml.bak

App\:
    resource: '../src/*'
    exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'

would be

For every class in the ../src folder (relative from the service file) whose full class name begins with App\, automatically make that class a service if that class is referenced as a service in Symfony’s container or autowiring parameters. However, do NOT do this if the class path matches the glob pattern ../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}.

This exclude property is important. Many classes and patterns that predate Symfony’s use of autowiring will have constructors whose parameters include type hints, but aren’t intended to be autowired. Autowiring these services will likely lead to errors.

Let’s try this with our configuration. First, remove our service configuration from service.yaml — we’ll do this with comments below

#File: config/service.yaml
parameters:
    locale: 'en'
#
#services:
#    App\Services\ServiceA:
#        public: true
#        autowire: true
#    App\Services\ServiceB:
#        public: true

Reload your page, and Symfony will complain about no defined services with the following error message

You have requested a non-existent service “App\Services\ServiceA”.

Now, add the following configuration to our services file

#File: config/services.yaml
services:
    _defaults:
        autowire: true
        public: true

    App\:
        resource: '../src/*'
        exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'

We’ve set all services to autowire by default, and be public by default. We’ve told Symfony that every class in our src/* folder should be considered for use as a service, expect for the class files that match the pattern defined in the file src/Kernel.php.

Reload the page with the above in place and you’ll see that Symfony is automatically able to recognize our classes as services — i.e. our message is still printed out.

With this configuration in place by default, many developers won’t even realize their classes are being registered as Symfony services. However, if you’re going to be able to reason about your system, it’s important to understand that all your automatically injected class instances are still Symfony services, and subject to additional service configuration.

No Global Service Container

At the risk of getting too far ahead of ourselves, there’s one last thing to cover. We’re still doing global shenanigans to get our service container.

#File: src/Controller/Pulsestorm/Di.php
public function run() {
    global $kernel;
    $container = $kernel->getContainer();
    $service = $container->get('App\Services\ServiceA');
    echo $service->sayHello();
    exit;
}

This isn’t how things are done in Symfony. Instead, we can actually inject our service container with autowiring. Building off what we’ve done above, give the following controller a try

#File: src/Controller/Pulsestorm/Di.php
<?php
namespace App\Controller\Pulsestorm;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

class Di extends AbstractController {

    private $fullServiceContainer;
    public function __construct(
        \Symfony\Component\DependencyInjection\ContainerInterface $container
    ) {
        $this->fullServiceContainer = $container;
    }

    public function run() {
        $service = $this->fullServiceContainer->get('App\Services\ServiceA');
        echo $service->sayHello();
        exit;
    }
}

The string Symfony\Component\DependencyInjection\ContainerInterface is a service setup by default in Symfony applications that will contain a reference to the current container object. We’re able to use autowiring in our controller class because we told Symfony that all our classes should be considered as services.

Finally — symfony comes with a huge number of services — you can check them out (as well as the services your own configuration has added), using the Symfony console’s debug:container command.

$ php bin/console debug:container

# ... list of all your services ...

Another common approach here would be to avoid the service container entirely, and inject our App\Services\ServiceA class/service directly. We’ll leave that one as an exercise for the reader.

Wrap Up

That’s where we’ll leave it for today. Next time we’ll we’ll take a brief lookg at every possible service configuration paramater. After that, we’ll be ready to start looking at Sylius code in earnest.

Series Navigation<< Symfony’s Service ContainerA Brief Look at Every Symfony Service Configuration >>

Copyright © Alan Storm 1975 – 2019 All Rights Reserved

Originally Posted: 10th February 2019