Unveiling the Power of Dependency Injection in Symfony

Nikolay Nikolov
ITNEXT
Published in
4 min readFeb 21, 2024

--

Icon by Flowicon

Dependency Injection (DI) is a fundamental concept in Symfony that contributes to cleaner, more maintainable, and testable code. It promotes the principles of separation of concerns and inversion of control, enabling developers to build modular, flexible applications.

In this article, we’ll explore the benefits of Dependency Injection in Symfony, along with practical examples comparing code with and without DI, including unit tests to demonstrate its advantages.

Explanation of Dependency Injection

Dependency Injection is a design pattern used to remove the responsibility of creating dependencies from the class that requires them. Instead of classes creating their own dependencies, they receive them from an external source. In Symfony, dependency injection is facilitated through the service container, which manages the instantiation and injection of dependencies.

Benefits of Dependency Injection:

1. Decoupling: With DI, classes are not tightly coupled to their dependencies. This promotes modularization and allows for easier substitution or modification of dependencies without affecting the core functionality of the class.

2. Testability: By injecting dependencies, classes become easier to test because they can be instantiated with mock or stub dependencies during testing. This facilitates unit testing and improves the overall test coverage of the application.

3. Flexibility: DI promotes flexibility by allowing developers to configure and customize dependencies externally. This makes it easier to adapt the application to changing requirements and to reuse components across different contexts.

Code Without Dependency Injection:

Consider a class `EmailSender` responsible for sending emails:

// src/Service/EmailSender.php
<?php

declare(strict_types=1);

namespace App\Service;

class EmailSender
{
public function sendEmail(
string $recipient,
string $subject,
string $message
): void {
// Logic to send email
}
}

Suppose we have another class `NotificationService` that depends on `EmailSender` to send notifications:

// src/Service/NotificationService.php
<?php

declare(strict_types=1);

namespace App\Service;

use App\Service\EmailSender;

class NotificationService
{
private EmailSender $emailSender;

public function __construct()
{
$this->emailSender = new EmailSender();
}

public function sendNotification(string $recipient, string $message): void
{
$subject = "New Notification";
$this->emailSender->sendEmail($recipient, $subject, $message);
}
}

In this example, `NotificationService` directly creates an instance of `EmailSender` within its constructor, making it impossible to replace `EmailSender` with a mock or stub during testing.

Unit Test Without Dependency Injection:

Without dependency injection, it’s challenging to isolate `NotificationService` for testing. Here’s an attempt to write a unit test:

// tests/Unit/NotificationServiceTest.php
<?php

declare(strict_types=1);

namespace App\Tests\Unit;

use PHPUnit\Framework\TestCase;
use App\Service\NotificationService;

class NotificationServiceTest extends TestCase
{
public function testSendNotification(): void
{
$service = new NotificationService();
$service->sendNotification('example@example.com', 'Test message');

// Assert: It's impossible to assert the behavior without actually sending an email
}
}

In this test, we cannot verify the behavior of `sendNotification` without actually sending an email, making it impossible to write a meaningful unit test.

Refactoring with Dependency Injection:

Now, let’s refactor `NotificationService` to use Dependency Injection:

// src/Service/NotificationService.php
<?php

declare(strict_types=1);

namespace App\Service;

use App\Service\EmailSender;

class NotificationService
{
public function __construct(private EmailSender $emailSender)
{
}

public function sendNotification(string $recipient, string $message): void
{
$subject = "New Notification";
$this->emailSender->sendEmail($recipient, $subject, $message);
}
}

Refactoring `NotificationService` to use Dependency Injection improves the cleanliness of the code. By injecting the `EmailSender` dependency through the constructor, we adhere to the principle of separation of concerns. Now, `NotificationService` is solely responsible for sending notifications, while the creation and management of the `EmailSender` instance are handled externally.

This approach promotes code clarity and maintainability, as each class has a clear and distinct responsibility. Additionally, Dependency Injection enhances testability by allowing easy substitution of dependencies during unit testing, ensuring that our code remains reliable and adaptable to future changes.

Overall, implementing dependency injection leads to cleaner, more modular code that is easier to understand, test, and maintain.

Unit Test With Dependency Injection:

With dependency Injection, we can easily write a unit test for `NotificationService`:

// tests/Unit/NotificationServiceTest.php
<?php

declare(strict_types=1);

namespace App\Tests\Unit;

use PHPUnit\Framework\TestCase;
use App\Service\NotificationService;
use App\Service\EmailSender;

class NotificationServiceTest extends TestCase
{
public function testSendNotification(): void
{
$emailSenderMock = $this->createMock(EmailSender::class);
$emailSenderMock->expects($this->once())
->method('sendEmail')
->with('example@example.com', 'New Notification', 'Test message');

$service = new NotificationService($emailSenderMock);
$service->sendNotification('example@example.com', 'Test message');
}
}

In this test, we mock `EmailSender`, allowing us to isolate `NotificationService` for testing and verify its behavior without actually sending emails.

Conclusion:

The examples demonstrate situations where Dependency Injection is essential for writing meaningful unit tests. By embracing Dependency Injection in Symfony, developers can write cleaner, more maintainable, and testable code, ultimately leading to higher code quality and better software development practices.

If you enjoyed this, subscribe to my future articles, follow me if you like 🚀

Clap 👏🏻, drop a comment 💬, and share this article with anyone you think would find it valuable.

Thank you for your support!

For further exploration, you can also delve into my other articles, such as “Fix your Software Stack”, “Unlocking Success: Mastering Project Delivery in Software Development” or “Why Clean Code?”.

Happy coding!

--

--

Head of Software Development at CONUTI GmbH | 20 years experience | Passionate about clean code, design patterns, and software excellence.