Skip to content

Mohcin Bounouara

Thoughts about software engineering and life

Advanced Dependency Injection in PHP

What is Dependency Injection?

Dependency Injection is a technique (Design Pattern = Which means its a common solution to a common problem in software development design) that allows us to create a code that is loosely coupled, making it easier to manage, test, and maintain.

In simple terms: (DI) means that the Object (a class) should not create its dependencies. Instead, it should receive them from the outside of the class. This way, you can easily change dependencies without changing the class itself, so we are here automatically respect the Single Responsibility presented in the SOLID Principals.

HINT: Dependencies are the objects or services that a class needs to work properly or to perform its functions. These dependencies are essential for the class to do its tasks.

The goals of using DI?

  • The dependency: Classes that are using DI don’t need to know how their dependencies are created or configured, they just be in use,
  • Easy testing: when using DI it become easy to test components separately and create mock tests based on the objects that are using DI itself, also isolating unit tests,
  • Re-usability: Classes that use DI can easily be used in other classes, services, or interfaces to do different implementation on the system
  • Easy to change and Maintainability: when you are using DI in different places on your app, its easy to change what you need in your DI dependencies in one-shot

Practical example Without Dependency Injection

Let’s suppose class that send reminders to users

class ReminderService {
    public function sendReminder($remind) {
        echo "Send reminder: " . $remind;
    }
}

class UserController {
    private $reminderService;

    public function __construct() {
        $this->reminderService = new ReminderService(); // Directly creating dependency
    }

    public function notifyUser($remind) {
        $this->reminderService->send($remind);
    }
}

// Usage of this class
$controller = new UserController(); // create new user instance
$controller->notifyUser("This is a reminder"); // use the notify user that is used the reminder services that is used by the constructor

The problem here:

The UserController is creating an instance of ReminderService itself, this means that UserController is forcefully coupled with the ReminderService, you don’t have the right to use another service, or you would have to change the UserController class

Practical example With Dependency Injection

Let’s refactor the above example to have the same with DI pattern in action

class ReminderService {
    public function sendReminder($remind) {
        echo "Send reminder: " . $remind;
    }
}

class UserController {
    private $reminderService;

    // Injecting dependency via the constructor
    public function __construct(ReminderService $reminderService) {
        $this->reminderService = $reminderService;
    }

    public function notifyUser($remind) {
        $this->reminderService->send($remind);
    }
}

// Usage of this class
$emailService = new ReminderService();
$controller = new UserController($reminderService);
$controller->notifyUser("This is a reminder sent using DI");

The solution that comes with DI:

In this solution the UserController is using the ReminderService that is injected via the constructor, so the User Controller class is not responsible of creating the Reminder instance, it receives it from outside the class, here we have couples of things as a gain in return, the user class now is doing one thing which notifies users, so it’s easy to test and maintain, you can change the ReminderService logic without make the user class like mess, you can also reflect the changes in many places if you are using the ReminderService in many places, also you can easily change the whole ReminderService with another services implementation.

We can do the same DI in the cases of using PHP Interfaces.

Since Laravel is the framework that made the DI patterns shine in today PHP’s community let’s add a real-world example based on it;

Let’s suppose that we have a task management app, and we have the TaskStoreService that is responsible of storing the new tasks, like below:

class TaskStoreService
{
    public function storeTask(array $taskData): Task
    {
        // Handle file uploads
        $attachments = $this->handleFileUploads($taskData['files'] ?? []);

        // Convert the array of attachments into a string
        $attachmentsString = implode(',', $attachments);

        // Create the task
        $task = Auth::user()->tasks()->create([
            'project_id' => $taskData['project_id'],
            'title' => $taskData['title'],
            'description' => $taskData['description'] ?? '',
            'status' => $taskData['status'],
            'attachments' => $attachmentsString,
            'due_date' => $taskData['due_date'] ?? null,
        ]);

        return $task;
    }
}

And we have TaskManagementController that contain the store store function that will use the TaskStoreService via injection, like below:

class TaskManagementController extends Controller
{
    function store(Request $request, TaskStoreService $taskStoreService): RedirectResponse
    {
        $validated = $request->validate([
            'title' => 'required|string|max:200',
            'description' => 'nullable|string',
            'status' => 'required|in:Backlog,To Do,Doing,Done',
            'attachments.*' => 'nullable|file|mimes:pdf,jpeg,png,jpg,gif',
            'project' => 'required|exists:projects,id'
        ]);

        // Include files in the task data array if present
        $taskData = $validated;
        $taskData['project_id'] = $validated['project'];
        $taskData['published'] = $request->input('action') === 'publish';


        if ($request->hasFile('attachments')) {
            $taskData['files'] = $request->file('attachments');
        }

        // Use the service to store the task
        $taskStoreService->storeTask($taskData);

        return redirect(route('tasks.index'));
    }
}

So this is a real-world scenario based on Laravel 🙂

Final words:

Dependency Injection is a powerful technique that will make your coding skills more mature, up your software engineering mindset, and make your code more readable, testable, and flexible, also easy to change based on the progression of the project, it’s not easy to understand at first attempts, but bit by bit the pattern will be more clear, I’m also new at it, but I’m trying to use it in my work and learning, every time the situation allows, and writing this blog post is one of my processes to remember it more and this always makes me understand technical things deeper.

See you in my next blog!