Laravel9 Service Container

Service container is a heart of Laravel Application yet most of the developers do not know what it is and how to properly use service container to ease the development process.

In this tutorial, I will explain in detail how it works and why it is the most important concept in laravel framework. According to laravel:

The Laravel service container is a powerful tool for managing class dependencies and performing dependency injection.

The first question you might have is what is dependency injection? Right? Let's understand it first.

What is Dependency injection ?

Dependency injection is a technique in which class dependencies are "injected" into the class via the constructor or, in some cases, "setter" methods.

In simple terms, let say you have a class that depends on some other class. If this other class is injected via the constructor method of your class it is called dependency injection.

You are injecting a class via constructor method to some other class because of class dependency. Still not sure what it is, let’s understand by an example.

Let's say that we want to create a new email notification service which implements a notification interface.

Notification Interface:

<?php

namespace App\Service\Contract;

interface Notification
{
    /**
     * @param string $email
     * @param string $subject
     * @param string $message
     * @return void
     */
    public function send(string $email, string $subject, string $message): void;
}

EmailNotification Class:

<?php

namespace App\Service;

use App\Service\Contract\Notification;

class EmailNotification implements Notification
{
    /**
     * @param string $email
     * @param string $subject
     * @param string $message
     * @return void
     */
    public function send(string $email, string $subject, string $message): void
    {
        echo "Email notification sent to: {$email} with subject: {$subject} and content: {$message}";
    }
}

Let’s say that in your laravel application you want to use this service in your controller function, There are different ways you can accomplish this task even without using dependency injection.

Let's see how we can do it without using dependency injection:

<?php

namespace App\Http\Controllers;

use App\Service\EmailNotification;
use Illuminate\Contracts\View\View;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\Foundation\Application;

class HomeController extends Controller
{
    /**
     * @description Homepage of website
     *
     * @return Factory|View|Application
     */
    public function index(): Factory|View|Application
    {
        $emailNotification = new EmailNotification();
        $emailNotification->send('abc@mail.com', 'Hello', 'This is a sample message');

        return view('homepage');
    }
}

Let’s assume your project is getting bigger and you have similar logic implemented all over. Now, your EmailNotficiation service needs a constructor with some parameters.

What will you do now? You would manually go to each controller and have to update the code to pass new parameters into the EmailNotification service.

What if we can  create a class somewhere else and then inject this dependency in the controller method through dependency injection laravel provides.

What are the benefits of doing this?

  • When class constructor changes it won't affect implementation in controllers
  • When class name changes it won't affect implementation in controllers

Laravel service binding

I know you are still confused about what I am saying but don't worry, let's understand this practically. Let's say that we want to bind our service to a service container.

Laravel service containers need to be aware of our service and we have to tell service containers that run some logic before you inject certain dependencies.

Let's understand that our EmailNotification service implements Notification interface right? We will use binding to let the service container know that whenever we inject this interface class you can run the following logic and provide us with an EmailNotification class.

Place following logic in AppServiceProvider class:

<?php

namespace App\Providers;

use App\Service\Contract\Notification;
use App\Service\EmailNotification;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        // binding interface with class instance
        $this->app->bind(Notification::class, function ($app) {
            return new EmailNotification();
        });
    }
}

Next, we will update our controller class in such a way that we will inject Notification interface as an argument of our function as seen below.

<?php

namespace App\Http\Controllers;

use Illuminate\Contracts\View\View;
use App\Service\Contract\Notification;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\Foundation\Application;

class HomeController extends Controller
{
    /**
     * @description Homepage of website
     *
     * @param Notification $notification
     * @return Factory|View|Application
     */
    public function index(Notification $notification): Factory|View|Application
    {
        $notification->send('abc@mail.com', 'Hello', 'This is a sample message');

        return view('homepage');
    }
}

If you check the above code you will realize that now the index function is taking the interface as an argument right. We injected interface as our dependency, service container will look for a binding and return us EmailNotification class in return.

So far you are following me right. You still wondering why we are doing this and what is wrong with following logic:

$emailNotification = new EmailNotification();
$emailNotification->send('abc@mail.com', 'Hello', 'This is a sample message');

Let's say that you have 20 different controllers running above logic. If the following change happens you will have to manually go to each controller and change the logic again.

  • What if the class name changes?
  • What if the class takes a new argument in the constructor?

Let say that our EmailNotification service changes and we have create a constructor with some arguments as seen below:

<?php

namespace App\Service;

use App\Service\Contract\Notification;

class EmailNotification implements Notification
{
    /**
     * EmailNotification constructor.
     * @param string|null $apiKey
     * @param string|null $apiSecret
     */
    public function __construct(
        protected ?string $apiKey,
        protected ?string $apiSecret
    ) {}

    /**
     * @param string $email
     * @param string $subject
     * @param string $message
     * @return void
     */
    public function send(string $email, string $subject, string $message): void
    {
        echo "Email notification sent to: {$email} with subject: {$subject} and content: {$message}";
    }
}

Now, that our constructor requires two more arguments, what will happen if we use dependency injection?

Simply, because our class creation is locationed in AppServiceProvider class we would change this logic in one place as seen below:

<?php

namespace App\Providers;

use App\Service\Contract\Notification;
use App\Service\EmailNotification;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        $this->app->bind(Notification::class, function ($app) {
            return new EmailNotification(env('APP_KEY'), env('APP_SECRET'));
        });
    }
}

That's all we do not need to change our implementation in controller class our controller will work as is. Let us look at our controller logic again:

<?php

namespace App\Http\Controllers;

use Illuminate\Contracts\View\View;
use App\Service\Contract\Notification;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\Foundation\Application;

class HomeController extends Controller
{
    /**
     * @description Homepage of website
     *
     * @param Notification $notification
     * @return Factory|View|Application
     */
    public function index(Notification $notification): Factory|View|Application
    {
        $notification->send('abc@mail.com', 'Hello', 'This is a sample message');

        return view('homepage');
    }
}

As you can see we do not need to change anything in our controller right. Similarly, if we decided to change the name of our class EmailNotification to something else we would just update AppServiceProvider with a new class name and it will not break our controllers.

Dependency injection is a really cool concept. I just explained to you only two benefits but there might be more benefits which I might not be aware of but I am exploring new ways.

To bind class or interface to service container there are different methods as seen below:

  • Binding a singleton
  • Binding scoped singleton
  • Binding instances
  • Binding interfaces to implementations
  • Contextual bindings
  • Binding primitives
  • Binding typed variadics

Beside binding you can also explore following concepts:

  • Tagging
  • Extending Bindings
  • Resolving
  • Automatic Injection
  • Method invocation & injection
  • Container events