4 ways to inject dependencies in Symfony that you probably don’t know about?

Oleg Charnyshevich on 2022-01-08

The DependencyInjection component allows you to standardize and centralize the way objects are constructed in your application.

Introduction

One part of my job is a code review, and I started to notice that some of my colleagues can’t get an idea of dependency injection and the service container. As a result, they tend to over-engineering things.

I believe that if they were aware of possible variants of how to inject dependency and how service container works, they would be able to make a robust solution that would be reusable, testable, and decoupled from others.

As professionals, we must never stop learning and share our expertise with others to continue growing.

What is Dependency Injection?

Dependency Injection (or DI hereafter) and Service Container solve one of the most critical problems — constructing and injecting dependency into a class.

Constructing classes (dependency) yourself all the time is a lousy practice cause your code would look like spaghetti as dependency can have their dependencies and that dependencies their own, and so on.

Service Container — its collection of classes with configuration on how to build them. (You can read how to set up a service container in the official documentation.)

Once you have registered dependencies, you can use DI and Service Container to create new objects. The container will automatically resolve dependencies by instantiating them and injecting them into the newly created objects. The dependency resolution is recursive, meaning that if a dependency has other dependencies, those dependencies will also be resolved automatically.

The primary goal of Dependency Injection is making classes’ dependencies explicit, and requiring that they be injected into it is a good way of making a class more reusable, testable, and decoupled from others.

There are 4 ways that dependency can be injected, namely:

Each technique has advantages and disadvantages to consider and different working patterns when using the service container.

Constructor Injection

The most common way to inject dependencies is via the class’ constructor. For doing this, you need to add arguments to the constructor signature to accept the dependency.

synfony-constructor-Injection.php

<?php
// src/Service/UserManager.php
namespace App\Service;

// ...
class UserManager
{
    public function __construct(private SmsInterface $sms)
    {
    }
    public function restorePassword(Request $request)
    {
        //...
        $this->sms->send(...);
        //..
    }
}

You can specify what service you would like to inject into this, in the service container configuration:

constructor-injection-services.yaml

# config/services.yaml
services:
    # ...

    App\Service\UserManager:
        arguments: ['@sms']

In case if you don’t want to specify what service you would like to inject, you can turn on autowire and autoconfigure to pass the correct arguments automatically.

@see: https://symfony.com/doc/current/service_container.html#the-autowire-option

There are several advantages to using constructor injection:

These advantages mean that constructor injection is not suitable for working with optional dependencies. It also means that classes with constructor dependency are more challenging to inherit because you must always inject parents’ dependencies, and it’s impossible to eliminate them.

The best use case is controllers and services that you don’t want to inherit. Also, avoid using it in abstract classes.

Immutable-setter Injection

The second possible injection is to use a method that returns a clone of the original service. This approach assists us in having an immutable service.

symfony-immutable-setter-Injection.php

<?php
// src/Service/UserManager.php
namespace App\Service;

// ...
class UserManager
{
    private SmsInterface $sms;

    /**
     * @required
     * @return static
     */
    public function withSms(SmsInterface $sms): self
    {
        $new = clone $this;
        $new->sms = $sms;

        return $new;
    }
    public function getSms(): SmsInterface
    {
        if(!isset($this->sms)){
            throw new \Exception("Typed property must not be accessed before initialization");
        }
        return $this->sms;    
    }
    public function restorePassword(Request $request): mixed
    {
        //...
        $this->getSms()->send(...);
        //..
    }
    // ...
}

To use this type of injection, don’t forget to configure it:

symfony-property-Injection-services.yaml

# config/services.yaml
services:
     # ...

     app.user_manager:
         class: App\Service\UserManager
         calls:
             - withSms: !returns_clone ['@sms']

If you decide to use autowiring, this type of injection requires that you add a @return static docblock for the container to register the method.

This approach is helpful when you have to configure your service with optional dependencies, so here are the advantages of immutable-setters:

The disadvantages are:

This approach is not as widespread as others, but in the right hands, it can be helpful.

For example, this approach is implemented in the Symfony component — Messenger. There is an Envelope class, and every time you add a stamp, the method copies the object so that the body of the message remains the same.

Setter Injection

The third possible injection point into a class is accepting dependency through a setter method:

symfony-setter-injection.php

<?php
// src/Service/UserManager.php
namespace App\Service;

// ...
class UserManager
{
    private SmsInterface $sms;

    /**
     * @required
     */
    public function setSms(SmsInterface $sms): void
    {
        $this->sms = $sms;
    }
    public function restorePassword(Request $request)
    {
        //...
        $this->sms->send(...);
        //..
    }
    // ...
}

You can specify what service you would like to inject into this, in the service container configuration:

symfony-setter-injection-services.yaml

# config/services.yaml
services:
    # ...

    app.newsletter_manager:
        class: App\Service\UserManager
        calls:
            - setSms: ['@sms']

This time the advantages are mostly the same as Immutable-setter one:

The disadvantages of setter injection are:

Personally, I like this approach, especially when I have to inject optional dependencies into my class or an abstract class to keep a constructor untouched for descendants.

For example, this approach is cleverly used in PSR-3. There is a trait with an optional dependency that can be composed with any class.

Property Injection

The last injection variant is setting public fields of the class directly:

symfony-property-Injection.php

<?php
// src/Service/UserManager.php
namespace App\Service;

// ...
class UserManager
{
    public SmsInterface $sms;
    public function restorePassword(Request $request)
    {
        //...
        $this->sms->send(...);
        //..
    }
    // ...
}

It is not possible to use autowiring, so don’t forget to configure it:

symfony-property-Injection-services.yaml

# config/services.yaml
services:
    # ...

    app.newsletter_manager:
        class: App\Service\UserManager
        properties:
            sms: '@sms'

This approach has primarily only disadvantages. It is similar to Setter Injection, but with these additional noteworthy problems:

But it is helpful to be aware that can be done with the service container, especially if you are working with a third-party library, which uses public properties for its dependencies.

I DO NOT recommend using this in your code.

Summary

Dependency Injection and Service Container are essential parts of the Symfony eco-system built around the SOLID dependency inversion principle introduced interfaces between higher-level classes and their dependencies to decouple and change them separately without affecting each other.

Mastering it helps you grow professionally and make reusable, testable, and readable code. Knowing possible options enables you to pick the right one and deliver features quickly and better.

I hope you found this article practical.

Sources:

Many of us, myself included, use Symfony at work daily. And if you want to deepen your knowledge in this domain, let’s do it together. Follow me on social media, here on Medium, and Twitter.

If this post was helpful, please click the clap 👏 button below a few times to show your support for the author! ⬇