4 ways to inject dependencies in Symfony that you probably don’t know about?
- Date
-
Oleg Charnyshevich
In this tutorial, I will show you how to use the Dependency Injection features as an independent component in Symfony. The knowledge from this article is reusable and can be applied to other frameworks with slight changes.
This part of the tutorial will help you familiarize yourself with terms and all possible ways to inject services into your code with my examples.
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:
-
Constructor Injection
-
Immutable-setter Injection
-
Setter Injection
-
Property Injection
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.
<?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:
# 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:
-
If a dependency in a requirement and class cannot work without it, inject it through the constructor to guarantee it is present while using the class because the class cannot be instantiated without it.
-
The constructor is only ever called once when the class is initialized, so you can be sure that the dependency is immutable.
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.
<?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:
# 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:
-
You don’t need to call the setter if you don’t need a dependency.
-
The dependency stays the same during a service’s lifetime.
-
This type of injection works perfectly with traits, as the service can be composed and quickly adapted to your application requirements.
-
Adding a new dependency is relatively easy, and it doesn’t spoil readability.
-
You can easily inherit a class that implemented this approach and extend it without a problem.
The disadvantages are:
-
Because the setter call is optional, a dependency can be null or uninitialized. You must check that the dependency is available before using it.
-
Unless the service is declared lazy, it is incompatible with services that reference each other in circular loops.
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:
<?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:
# 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:
-
This approach works as well as Immutable-setter Injection with optional dependency. If you don’t need dependencies, don’t call a setter.
-
You can call the setter multiple times. It is convenient if the method adds a dependency to a collection. You may have a variable number of dependencies.
-
As Immutable-setter Injection, this type of injection works well with traits.
-
Same with adding a new dependency is relatively easy, and it doesn’t spoil readability.
-
As well, you can easily inherit a class that implemented this approach and extend it without a problem.
The disadvantages of setter injection are:
-
The setter can be called countless times, also long after initialization, so you can’t guarantee that the dependency has been changed (except by writing the setter method to prevent overwriting or checking if it has already been called).
-
You cannot be confident that the setter will be called, so you need to add checks that any of your required dependencies have been injected.
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:
<?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:
# 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:
- It is not possible to control when the dependency is established. You cannot prevent overwriting this property.
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
-
https://symfony.com/doc/current/components/dependency_injection.html
-
https://symfony.com/doc/current/service_container/injection_types.html
-
https://symfony.com/doc/current/components/messenger.html#adding-metadata-to-messages-envelopes
-
https://github.com/symfony/symfony/blob/6.1/src/Symfony/Component/Messenger/Envelope.php#L57
-
https://en.wikipedia.org/wiki/Dependency_inversion_principle
Stay tuned for new articles by following me on Twitter or Medium! Subscribe to my RSS.
Tags:
#php #symfony