Dependency Injection and Symfony Services (Updated for 3.3)


With the release of Symfony 3.3 and the dependency injection improvements it provides, it's time for a visit back into Dependency Injection and Symfony Services.

When working with developers who are new to Symfony, there is a common point of pain they tend to experience:

The difference between getting access to a service in a Symfony controller, vs accessing that very same service inside their own custom service.

That sentence may sound confusing, but hopefully the examples below make it more obvious.

Let's imagine that we want to make use of the Symfony Event Dispatcher service.

Firstly, we want to use the Event Dispatcher inside a typical Symfony controller action.

And secondly, we want to use the Event Dispatcher in our own custom service.

Here's how we can achieve both:

Hey Old Timer

In Symfony 3.2 and earlier, we had two options.

First, we could grab a service from the container:

public function indexAction(Request $request)
{
    $eventDispatcher = $this->get('event_dispatcher');

    $eventDispatcher->dispatch(
        'event.whatever',
        new SomeImportantEvent()
    );

We could shorten this further by removing the local $eventDispatcher variable:

public function indexAction(Request $request)
{
    $this->get('event_dispatcher')->dispatch(
        'event.whatever',
        new SomeImportantEvent()
    );

Symfony 3.3 improves this for us, by allowing us to inject dependencies right into our controller actions:

<?php

use Symfony\Component\EventDispatcher\EventDispatcherInterface;

class DefaultController extends Controller
{
    public function indexAction(Request $request, EventDispatcherInterface $eventDispatcher)
    {
        $eventDispatcher->dispatch(
            'event.whatever',
            new SomeImportantEvent()
        );

This makes our dependencies more explicit, and all things being equal, our controller actions easier to test.

With Symfony 3.3's service autowiring and autoconfiguration, we don't even need to write any service definition to achieve this. This has its pro's and con's but that's a different discussion.

I find most developers can understand this concept after a little bit of practice / repetition.

This Was Already Possible

It should be noted that in Symfony 3.2 and earlier, we could inject dependencies into our controller actions.

However, prior to Symfony 3.3, injecting services into controller actions hasn't been the standard operating procedure. We could do this, but it would mean registering our controllers as a service, and that generally this hasn't been the way most people worked (in my experience, at least).

Anyway, where I have noticed that things become less clear is in working with our own services.

Injecting Dependencies Into Our Own Custom Services

Beyond our first forays into Symfony, sooner or later we are going to want to extract all the 'stuff' from our Controller actions, and move this logic into a separate service.

You may be wondering why.

Let's imagine we have a simple system that allows us to add new Orders. What we are ordering isn't really important.

Let's imagine that this system allows us to add new orders by filling in a form on a web page.

All is good. Our logic lives inside the controller action for this web page, and users of our system are happy campers.

Later, management decide that the company needs to be able to accept orders via some different means. This could be in any manner of interesting ways - SMS, as JSON, via Email - it doesn't matter.

What matters is that we do not want to copy / paste (or generally, repeat) the logic from the controller action for our SMS implementation, or our JSON implementation, or our Email implementation.

Instead, we want to extract all of the logic that is involved in the creation of new Orders, and centralise it into one place.

A standalone service.

In this situation it doesn't matter how order data is received, it just matters that we can convert the data that is received into a format that our Order service can work with. How this happens leads to type systems, and developers do love to tell you which side of the fence they sit on such issues :)

Example Order Service

Let's create a service to contain all our imaginary order creation logic:

<?php

// src/AppBundle/Service/OrderService.php

namespace AppBundle\Service;

class OrderService
{
    public function processOrder($data)
    {
        // ... some process
    }
}

Based on our controller action from earlier, maybe our imaginary OrderService needs access to some other things.

Maybe it needs an OrderFactory in order to reliably construct an Order object. An example of this would be in to enable testing.

Maybe it needs the EventDispatcher (or something implementing EventDispatcherInterface) so it can dispatch events to interested parties.

Maybe we'd like the @logger service, for keeping track of interesting data.

There's a ton of possibilities here. Every system is different.

Let's imagine our OrderService needs all of those three:

<?php

// src/AppBundle/Service/OrderService.php

namespace AppBundle\Service;

use AppBundle\Factory\OrderFactory;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

class OrderService
{
    /**
     * @var LoggerInterface
     */
    private $logger;

    public function __construct(
        LoggerInterface $logger,
        EventDispatcherInterface $eventDispatcher,
        OrderFactory $factory
    )
    {
        $this->logger = $logger;
        // etc
    }

    public function processOrder($data)
    {
        // ... some process
    }
}

As of Symfony 3.3 there's very little you need to do to start using OrderService in your code.

We only have one configured Logger by default in Symfony:

Monolog.

Therefore Symfony can guess that when we want LoggerInterface, it should give us our configured Monolog instance.

Nice.

Next, EventDispatcherInterface is much of the same. The Symfony framework comes with a pre-configured Event Dispatcher service. It uses the Symfony Event Dispatcher Component. Who would have thunk it?

So, by virtue of this fact, Symfony can indeed guess our second argument should be the pre-configured Event Dispatcher instance.

Again, nice.

Lastly, we want OrderFactory. We could use an interface, but for the purposes of this example, and indeed most systems of the complexity I have ever worked on do not need to use interfaces for their factories. Again, ymmv. Options, options.

So if we're using an actual implementation then you guessed it, Symfony can definitively say exactly which class it should be injecting for us.

All of this can be automagically figured out for us.

It depends on your familiarity with Symfony as to whether you would want this.

To put it another way, in my opinion autowiring and autoconfiguration are like writing tests.

Tests slow you down, up front, there is no denying it.

They save you time in the long run, if the system survives its often turbulent first few iterations.

By not writing tests you save time up front.

If the system doesn't survive then you lost no extra time.

If it does, then the pain starts to set in if not addressed.

Autoconfiguration / Autowiring are the same.

They save you time up front, but being explicit will be less painful to you in the long run.

The Stanford marshmallow experiment was a series of studies on delayed gratification in the late 1960s and early 1970s led by psychologist Walter Mischel, then a professor at Stanford University.

In these studies, a child was offered a choice between one small reward provided immediately or two small rewards (i.e., a larger later reward) if they waited for a short period, approximately 15 minutes, during which the tester left the room and then returned.

In follow-up studies, the researchers found that children who were able to wait longer for the preferred rewards tended to have better life outcomes, as measured by SAT scores, educational attainment, body mass index (BMI), and other life measures.

With the Stanford marshmallow experiment in mind, let's quickly cover one way we could write this service definition:

# app/config/services.yml

services:

    # possible Symfony 3.3 approach
    _defaults:
        autowire: true
        autoconfigure: true
        public: false

    AppBundle\Factory\OrderFactory:

    AppBundle\Service\OrderService:
        arguments:
            $logger: "@logger"
            $eventDispatcher: "@event_dispatcher"
            $factory: "@crv.order_factory"

    # backwards compatibility, if needed
    crv.order:
        alias: AppBundle\Service\OrderService
        public: true

    crv.order_factory:
        alias: AppBundle\Factory\OrderFactory
        public: true

    # Prior to Symfony 3.3
    crv.order:
        class: AppBundle\Service\OrderService
        arguments:
            - "@logger"
            - "@event_dispatcher"
            - "@crv.order_factory"

    crv.order_factory:
        class: AppBundle\Factory\OrderFactory

Trim The Fat

You can cut down your config is Symfony 3.3 yet further still.

Indeed, by default this is how new Symfony 3.3 projects work:

# app/config/services.yml

services:

    # possible Symfony 3.3 approach
    _defaults:
        autowire: true
        autoconfigure: true
        public: false

    AppBundle\:
        resource: '../../src/AppBundle/*'
        exclude: '../../src/AppBundle/{Document,Entity,Features,Repository}'

In other words, everything thats not in one of the four directories:

  • src/AppBundle/Document
  • src/AppBundle/Entity
  • src/AppBundle/Features
  • src/AppBundle/Repository

is going to be autowired and auto configured for us, by default.

Only if you run into problems will you need to be more explicit.

How do I access My Own Service in a Symfony controller?

Earlier we learned how we get access to the @event_dispatcher service from our controller actions.

By way of a quick recap:

<?php

use Symfony\Component\EventDispatcher\EventDispatcherInterface;

class DefaultController extends Controller
{
    # symfony 3.2 and before
    public function indexAction(Request $request)
    {
        $this->get('event_dispatcher')->dispatch(
            'event.whatever',
            new SomeImportantEvent()
        );

    # possible in Symfony 3.3+
    public function indexAction(Request $request, EventDispatcherInterface $eventDispatcher)
    {
        $eventDispatcher->dispatch(
            'event.whatever',
            new SomeImportantEvent()
        );

How do we repeat this process, but get access to our own services?

The short version is we do this:

<?php

use AppBundle\Service\OrderService;

class DefaultController extends Controller
{
    # symfony 3.2 and before
    public function indexAction(Request $request)
    {
        $this->get('crv.order')
             ->processOrder([...]);

    # possible in Symfony 3.3+
    public function indexAction(OrderService $orderService)
    {
        $orderService->processOrder([...]);

Yup, we just inject it.

We already did all the config. It doesn't matter if it's a Symfony provided service, or our own. The process is the same.

How do I access My Own Service in another of My Own Services?

We've already seen how to do this.

Again, we just inject it.

The only requirement is that both services need to have been properly defined:

# app/config/services.yml

services:

    # possible Symfony 3.3 approach
    _defaults:
        autowire: true
        autoconfigure: true
        public: false

    AppBundle\:
        resource: '../../src/AppBundle/*'
        exclude: '../../src/AppBundle/{Document,Entity,Features,Repository}'

    # Prior to Symfony 3.3
    crv.order:
        class: AppBundle\Service\OrderService
        arguments:
            - "@logger"
            - "@event_dispatcher"
            - "@crv.order_factory"

    crv.order_factory:
        class: AppBundle\Factory\OrderFactory

Our OrderService needs OrderFactory as a constructor argument.

Symfony 3.3 can take care of this entirely.

In Symfony 3.2 and earlier, by default we would have been explicit.

Episodes

# Title Duration
1 Symfony Services 101 06:27
2 Symfony Services and Dependency Injection 05:13
3 Symfony Services in the Real World 09:49
4 Dependency Injection and Symfony Services (Updated for 3.3) 08:33