Basics of laravel style inversion of control

This is part of the Semicolon&Sons Code Diary - consisting of lessons learned on the job. You're in the architecture category.

Last Updated: 2024-03-28

Imagine Application needs Foo which needs Bar which needs Bim.

1. Without IOC:

Application creates Foo, Foo creates Bar, Bar creates Bim etc.

2. With IOC:

Application creates everything. I.e. it creates Bim, Bar, and Foo. Specifically, after it creates Bar it will give it the Bim it already created. Then after Application creates Foo it will give it the Bar it already created.

i.e. The control of the dependencies is inverted from the one being called to the one doing the calling.

Compare this to the general definition of dependency injection: "Basically, instead of having your objects creating a dependency or asking a factory object to make one for them, you pass the needed dependencies into the object externally, and you make it somebody else’s problem." - this could be with constructors, settings, or dependency injection frameworks.

In Laravel, the Service Container is responsible for injecting dependencies of your classes via their constructors. Its responsibilities are to:

Here's an example of how a constructor receives a dependency (for UserRepository below):

<?php 

class UserController extends Controller
{
    protected $userRepository;
    public function __construct(UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }

Usually, laravel automatically figures out how to construct the object. But you can give the container clearer instructions in the ServiceProvider:

<?php 

class MyServiceProvider extends ServiceProvider
{
    public function register()
    {
       // Within a service provider, you can access the container with `$this->`

       // A closure (the function) returns an instance of the class (this one is
       // a singleton because we used $this->app->singleton)

        $this->app->singleton(\App\Service\Geocoder::class, function ($app) {
            return new \App\Service\Geocoder($app->make('Utils\HttpRequests'));
        });

        // To do a non-singleton, use `$this->app->bind`

        $this->app->bind('HelpSpot\API', function ($app) {
          return new HelpSpot\API($app->make('HttpClient'));
        });
    }
}

Laravel allows us to substitute dependencies. This is most useful when we program to an abstract interface (e.g. EventPusher but substitute concrete instances in different environments - e.g. RedisEventPusher)

<?php 

// This statement tells the container that it should inject the RedisEventPusher when a class needs an implementation of EventPusher. 
$this->app->bind(
    'App\Contracts\EventPusher',
    'App\Services\RedisEventPusher'
);

This can even be contextual, such that depending on when (i.e. from what class) we ask for the dependency, a different one will be provided:

<?php 

$this->app->when(PhotoController::class)
          ->needs(Filesystem::class)
          ->give(function () {
              return Storage::disk('local');
          });

$this->app->when([VideoController::class, UploadController::class])
          ->needs(Filesystem::class)
          ->give(function () {
              return Storage::disk('s3');
          });

References