First, let’s be aware that Service Container is not a Laravel or PHP only thing. This concept is present in the frame works of many other Programming languages too.

Please note that Service Container is also known as DI Container (Dependency Injection Container), Application Container, IoC Container (Inversion of Control).

In simple word, Service Container is a class to store pieces of data and retrieve them whenever they are needed throughout our app. And it benefits us in many ways. So, let us explore them, inshaAllaah (by the permission of Allaah t’ala):

To get it more clearly, let’s create a directory called ‘Test’ inside the app directory of one of our Laravel project and create a simple Service Container class as shown below inside this ‘Test’ directory:

<?php

namespace App\Test;

class Container {

    // array for keeping the container bindings
    protected $bindings = [];

    // binds new data to the container
    public function bind($key, $value)
    {
        // bind the given value with the given key
        $this->bindings[$key] = $value;
    }

    // returns bound data from the container
    public function make($key)
    {
        if (isset($this->bindings[$key])) {
            // check if the bound data is a callback
            if (is_callable($this->bindings[$key])) {
                // if yes, call the callback and return the value
                return call_user_func($this->bindings[$key]);
            } else {
                // if not, return the value as it is
                return $this->bindings[$key];
            }
        }
    }

}

We can store/bind any data to this container using its bind() method and retrieve them whenever we need using its make() methond. Let’s test things creating a route called ‘test’ as follows:

<?php

// routes/web.php

use App\Container;
use Illuminate\Support\Facades\Route;

<?php

use App\Test\Container;
use Illuminate\Support\Facades\Route;

class MyClass{

    public function __construct($name)
    {

    }
}

Route::get('/', function () {
    return view('welcome');
});

/*Let's create a route called 'test' to test our  simple Container class */

Route::get('/test', function () {
    $container = new Container();

    $container->bind('myName', 'Mohammad Nurul Amin Tawfique');

    dd($container->make('myName')); /* output: Mohammad Nurul Amin Tawfique  */
});

Now interestingly we can even store or bind code syntax in this way:

<?php

// routes/web.php

use App\Container;
use Illuminate\Support\Facades\Route;

<?php

use App\Test\Container;
use Illuminate\Support\Facades\Route;

class MyClass{

    public function __construct($name)
    {

    }
}

Route::get('/', function () {
    return view('welcome');
});

/*Let's create a route called test to tset our  simple Container class */

Route::get('/test', function () {
    $container = new Container();

    $container->bind('myName', 'Mohammad Nurul Amin Tawfique');

    // dd($container->make('myName')); /* output: Mohammad Nurul Amin Tawfique (please test it by uncommenting the line) */

    /* way to store "code syntax to instantiate a class" */
    $container->bind(MyClass::class, function () {
        $dependency = 'Tawfique';
        return new MyClass($dependency); /* we have created MyClass above in this file */
    });

    /* way to retrieve and run the stored code syntax */
    $MyClass = $container->make(MyClass::class);
    dd($MyClass); /* output: MyClass */

});

And here comes the power of Service Container. Look here we store the code syntax (new MyClass($dependency)) to instantiate our MyClass and whenever we need to instantiate this class we can simply call the Service Container make() method as like this: $container->make(MyClass::class); and it will run our syntax (to instantiate our class) that we store beforehand (using bind() method).

So now the question is why do we need such a weird way to instantiate a class? Well, definitely, it has very good reasons; and you may be amazed by that. Let’s get it:

Let’s create a directory named Billing inside our Laravel app directory and inside it Let’s create a class called PaymentGateway as follows:

<?php

//App/Billing/PaymentGateway.php

namespace App\Billing;

use Illuminate\Support\Str;

class PaymentGateway
{
    public function __construct($currency)
    {
        $this->currency = $currency;
    }
    public function charge($amount)
    {
        return [
            'amount' => $amount,
            'confirmation_number' => Str::random(),
            'currency' => $this->currency,


        ];
    }
}

Now, let’s create another four web Routes (test1. test2. test3, test4) in our Web.php :


<?php

use App\Test\Container;
use Illuminate\Support\Facades\Route;

class MyClass{

    public function __construct($name)
    {

    }
}

Route::get('/', function () {
    return view('welcome');
});

/*Let's create a route called test to tset our  simple Container class */

Route::get('/test', function () {
    $container = new Container();

    $container->bind('myName', 'Mohammad Nurul Amin Tawfique');

    // dd($container->make('myName')); /* output: Mohammad Nurul Amin Tawfique (please test it by uncommenting the line) */

    /* way to store "code syntax to instantiate a class" */
    $container->bind(MyClass::class, function () {
        $dependency = 'Tawfique';
        return new MyClass($dependency); /* we have created MyClass above in this file */
    });

    /* way to retrieve and run the stored code syntax */
    $MyClass = $container->make(MyClass::class);
    dd($MyClass); /* output: MyClass */

});

/* Now let's create the following four routes  (test1, test2, test3, test4) to test Laravel Service Container */

Route::get('/test1', [App\Http\Controllers\PayOrderController::class, 'method1']);
Route::get('/test2', [App\Http\Controllers\PayOrderController::class, 'method2']);
Route::get('/test3', [App\Http\Controllers\PayOrderController::class, 'method3']);
Route::get('/test4', [App\Http\Controllers\PayOrderController::class, 'method4']);

Now let’s create a controller named “PayOrderController” and four methods named method1, method2, method3, method4 in it , and instantiate our class PaymentGateway inside them in different ways to understand Service Container way to bind a class and its dependencies and instantiate it :

<?php

namespace App\Http\Controllers;

use App\Container;
use App\Billing\PaymentGateway;
use Illuminate\Http\Request;

class PayOrderController extends Controller
{
    public function method1()
    {
        /*our PaymentGateway class needs an argument to be instantiated
        and this argument is the dependency of this class */
        $dependency = $currency =  'usd';

        /* this way we NEED to pass dependency evry time */
        $PaymentGateway = new PaymentGateway($dependency);

         /* please, see the output in your browser  */
        dd($PaymentGateway->charge(2500));
    }

    public function method2()
    {
        $container = app(); /* Laravel app() helper function returns its Service Container */

       /* $container->bind('currency', 'usd'); // this will not work in Laravel
       context as Laravel container class actually designed for instantiating classes
       So, instead let's write it below manner */

        /* storing value of currency (our class dependency) to Laravel Service Container  */
        $container->bind('currency', function () {
            return 'usd';
        });

        $dependency  = $container->make('currency');

        /* using Laravel Service Container bind() method,
        we are storing the code syntax to instantiate our PaymentGateway class */
        $container->bind('PaymentGateway', function () use ($dependency) {
            return new PaymentGateway($dependency);
        });

        /* this way NO need to pass dependency every time */
        $PaymentGateway = $container->make('PaymentGateway');

        /* please, see the output in your browser  */
        dd($PaymentGateway->charge(2500));
    }

    public function method3()
    {
        /* to work this, you must bind its syntax in some global place using app()->bind() method */
        $PaymentGateway = app()->make('PaymentGateway');
        dd($PaymentGateway->charge(2500));
    }

    public function method4(PaymentGateway $PaymentGateway)
    {
       /* If your method3 works, it will work too.
       But how does it work, are you aware?
       Are you aware of type-hinting?
       Are you aware of php self reflection class?
       */

       dd($PaymentGateway->charge(2500));
    }
}

We can instantiate any of our class as shown in method1() above. There is nothing wrong with it.

But the problem is with its argument ( dependency). What if we need this class in many other places? Or we need to pass more arguments (dependencies) to it.

Soon it may become hard to manage and here comes the benefits of Service Container.

Using Service Container bind() method (as shown in method2() ) we can store the values of a class’ dependencies as well as syntax to instantiate it in one place ( and then we can instantiate the class in any part of our application without passing its arguments/dependencies further. So, if our class dependencies need to be changed at a later period, they can be easily changed in one place and we need not to go each place where we have instantiated our class.

Now, it’s your job to get method3() and method4() worked. Let’s get our hands dirty. But if you cannot do that right this moment, please don’t worry. Hope things will be clear over time, at least let’s get the method1() and method() to be clear inshaAllaah (by the permission of Allaah t’ala).

I owe to the below resources to get me the concept clear. You may also explore them too.

Laravel Service Container and Service Providers Explained by Farhan Hasin Chowdhury

The Laravel Service Container demystified by Marcel Pociot

Laravel Service Container Explained by Bitfumes youtube channel