DEV Community

loading...
Cover image for Laravel Service Container and Service Providers Explained

Laravel Service Container and Service Providers Explained

Farhan Hasin Chowdhury
Software developer with a knack for learning new things and writing about them.
Updated on ・11 min read

Laravel's service container is one of the most important pieces of the framework yet it gets so little attention from a lot of developers. Being interviewed a large number of candidates, I've realized that there are two main reasons behind this ignorance.

  • They find the idea of dependency injection hard to understand. Let alone the idea of IoC and IoC container.
  • They don't understand if they're ever going to use the container or not.

So in this article, I'll take you through step by step in understanding this magical concept. I'll begin with the fundamental concepts and finally, by the end, you should get an idea of how the different pieces fit together.

Going forward I am assuming you have a sound understanding of object-oriented programming, classes, objects, interfaces, and namespaces.

Table of Content

Dependency Injection and IoC

An oversimplified definition of dependency injection is the process of passing a class dependency as an argument to one of its methods (usually the constructor).

Take a look at the following piece of code with no dependency injection:


<?php

namespace App;

use App\Models\Post;
use App\Services\TwitterService;

class Publication {

    public function __construct()
    {
        // dependency is instantiated inside the class
        $this->twitterService = new TwitterService();
    }

    public function publish(Post $post)
    {
        $post->publish();

        $this->socialize($post);
    }

    protected function socialize($post)
    {
        $this->twitterService->share($post);
    }

}

Enter fullscreen mode Exit fullscreen mode

This class which can be a part of an imaginary blogging platform is responsible for publishing a post and sharing it on social media.

The socialize() method uses an instance of the TwitterService class, which contains a single public method called share().


<?php

namespace App\Services;

use App\Models\Post;

class TwitterService {
    public function share(Post $post)
    {
        dd('shared on Twitter!');
    }
}

Enter fullscreen mode Exit fullscreen mode

In the constructor, as you can see, a new instance of the TwitterService class has been created. Instead of performing the instantiation inside the class, you can inject an instance into the constructor as an argument from the outside.


<?php

namespace App;

use App\Models\Post;
use App\Services\TwitterService;

class Publication {

    public function __construct(TwitterService $twitterService)
    {
        $this->twitterService = $twitterService;
    }

    public function publish(Post $post)
    {
        $post->publish();

        $this->socialize($post);
    }

    protected function socialize($post)
    {
        $this->twitterService->share($post);
    }

}

Enter fullscreen mode Exit fullscreen mode

For this simple demonstration, you may use the / route callback in the routes/web.php file.


<?php

// routes/web.php

use App\Publication;
use App\Models\Post;
use App\Services\TwitterService;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    $post = new Post();

    // dependency injection
    $publication = new Publication(new TwitterService());

    dd($publication->publish($post));

    // shared on Twitter!
});

Enter fullscreen mode Exit fullscreen mode

This is dependency injection at a basic level. Applying dependency injection to a class causes an inversion of control. Previously, the dependent class i.e. Publication was in control of instantiating the dependency class i.e. TwitterService whereas later, the control has been handed over to the framework.

IoC Container

In the previous section, I've introduced you to the idea of dependency injection and showed you how it causes a class to hand over instantiation control to the framework.

An IoC container can make the process of dependency injection more efficient. It is a simple class capable of saving and providing pieces of data when asked for. A simplified IoC container can be written as follows:


<?php

namespace App;

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];
            }
        }
    }

}

Enter fullscreen mode Exit fullscreen mode

You can bind any data to this container using the bind() method:


<?php

// routes/web.php

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

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

    $container->bind('name', 'Farhan Hasin Chowdhury');

    dd($container->make('name'));

    // Farhan Hasin Chowdhury
});

Enter fullscreen mode Exit fullscreen mode

You can bind classes to this container by passing a callback function that returns an instance of the class as the second argument.


<?php

// routes/web.php

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

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

    $container->bind(TwitterService::class, function(){
        return new App\Services\TwitterService;
    });

    ddd($container->make(TwitterService::class));

    // App\Services\TwitterService {#269}
});

Enter fullscreen mode Exit fullscreen mode

Assume your TwitterService class needs an API key for authentication. In that case, you can do something as follows:


<?php

// routes/web.php

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

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

    $container->bind('ApiKey', 'very-secret-api-key');

    $container->bind(TwitterService::class, function() use ($container){
        return new App\Services\TwitterService($container->make('ApiKey'));
    });

    ddd($container->make(TwitterService::class));

    // App\Services\TwitterService {#269 ▼
    //     #apiKey: "very-secret-api-key"
    // }
});

Enter fullscreen mode Exit fullscreen mode

Once you bind a piece of data to the container, you can ask for it whenever necessary. This way you have to use the new keyword only once.

I'm not saying that using new is bad. But every time you use new, you'll have to be careful about passing the correct dependencies to the class. With an IoC container, however, the container takes care of injecting the dependencies.

An IoC container can make your code a lot more flexible. Consider a situation where you want to swap the TwitterService class with something else like a LinkedInService class.

The current implementation of the system is not very suitable for that. To replace the TwitterService class, you'll have to make a new class, bind that to the container, and replace all references to the previous class.

It doesn't have to be like that. You can make this process much easier by utilizing interfaces. Start by creating a new SocialMediaServiceInterface.


<?php

namespace App\Interfaces;

use App\Models\Post;

interface SocialMediaServiceInterface {
    public function share(Post $post);
}

Enter fullscreen mode Exit fullscreen mode

Now make your TwitterService class implement this interface.


<?php

namespace App\Services;

use App\Models\Post;
use App\Interfaces\SocialMediaServiceInterface;

class TwitterService implements SocialMediaServiceInterface {
    protected $apiKey;

    public function __construct($apiKey)
    {
        $this->apiKey = $apiKey;
    }

    public function share(Post $post)
    {
        dd('shared on Twitter!');
    }
}

Enter fullscreen mode Exit fullscreen mode

Instead of binding the concrete class to the container, bind the interface. In the callback, return an instance of the TwitterService class just like before.


<?php

// routes/web.php

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

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

    $container->bind('ApiKey', 'very-secret-api-key');

    $container->bind(SocialMediaServiceInterface::class, function() use ($container){
        return new App\Services\TwitterService($container->make('ApiKey'));
    });

    ddd($container->make(SocialMediaServiceInterface::class));

    // App\Services\TwitterService {#269 ▼
    //     #apiKey: "very-secret-api-key"
    // }
});

Enter fullscreen mode Exit fullscreen mode

So far the code is working just as before. The fun begins when you want to use LinkedIn instead of Twitter. With the interface in place, you can do that in two simple steps.

Create a new LinkedInService class that implements the SocialMediaServiceInterface.


<?php

namespace App\Services;

use App\Models\Post;
use App\Interfaces\SocialMediaServiceInterface;

class LinkedInService implements SocialMediaServiceInterface {
    public function share(Post $post)
    {
        dd('shared on LinkedIn!');
    }
}

Enter fullscreen mode Exit fullscreen mode

Update the call to the bind() method to return an instance of the LinkedInService class instead.


<?php

// routes/web.php

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

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

    $container->bind(SocialMediaServiceInterface::class, function() {
        return new App\Services\LinkedInService();
    });

    ddd($container->make(SocialMediaServiceInterface::class));

    // App\Services\LinkedInService {#269}
});

Enter fullscreen mode Exit fullscreen mode

Now you're getting an instance of the LinkedInService class. The beauty of this approach is that all the codes in other places remain the same. You only have to update the bind() method call. As long as a class implements the SocialMediaServiceInterface it can be bound to the container as a valid social media service.

Service Container and Service Providers

Laravel comes with a more powerful IoC container, known as the service container. You can rewrite the example from the previous section using the service container as follows:


<?php

// routes/web.php

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

Route::get('/', function () {
    app()->bind('App\Interfaces\SocialMediaService', function() {
        return new App\Services\LinkedInService();
    });

    ddd(app()->make('App\Interfaces\SocialMediaService'));

    // App\Services\LinkedInService {#262}
});

Enter fullscreen mode Exit fullscreen mode

In every Laravel application, the app instance is the container. The app() helper returns an instance of the container.

Just like your custom container, the Laravel service container has a bind() and a make() method used for binding services and retrieving services.

There is another method called singleton(). When you bind a class as a singleton, there can be only one instance of that class.

Let me show you an example. Update your code to make two instances of a given class.


<?php

// routes/web.php

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

Route::get('/', function () {
    app()->bind('App\Interfaces\SocialMediaService', function() {
        return new App\Services\LinkedInService();
    });

    ddd(app()->make('App\Interfaces\SocialMediaService'), app()->make('App\Interfaces\SocialMediaService'));

    // App\Services\LinkedInService {#262}
    // App\Services\LinkedInService {#269}
});

Enter fullscreen mode Exit fullscreen mode

Indicated by the numbers (#262 and #269) at the end, the two instances are different from each other. If you bind the class as a singleton, you'll see something different.


<?php

// routes/web.php

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

Route::get('/', function () {
    app()->singleton('App\Interfaces\SocialMediaService', function() {
        return new App\Services\LinkedInService();
    });

    ddd(app()->make('App\Interfaces\SocialMediaService'), app()->make('App\Interfaces\SocialMediaService'));

    // App\Services\LinkedInService {#262}
    // App\Services\LinkedInService {#262}
});

Enter fullscreen mode Exit fullscreen mode

As you can see, now the two instances are numbered the same indicating they are the same instance.

Now that you've learned about the bind(), singleton(), and make() methods, The next thing you will have to learn about is where to put these method calls. You certainly can not put them in your controllers or models.

The right place to put your bindings is service providers. Service providers are classes that reside inside the app/Providers directory. These are the bedrock of the framework, responsible for bootstrapping the majority of the framework services.

Every new Laravel project comes with five service provider classes by default. Among them, the AppServiceProvider class comes as empty by default with two methods. They are register() and boot().

The register() method is used for registering new services to the application. This is where you put your bind() and singleton() method calls.


<?php

namespace App\Providers;

use App\Interfaces\SocialMediaService;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind(SocialMediaService::class, function() {
            return new \App\Services\LinkedInService;
        });
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

Enter fullscreen mode Exit fullscreen mode

Inside providers, you get access to the container by simply writing $this->app instead of calling the app() helper function. But you can do that as well.

The boot() method is used for the logic necessary to bootstrap the registered service. A good example is the BroadcastingServiceProvider class.


<?php

namespace App\Providers;

use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\ServiceProvider;

class BroadcastServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        Broadcast::routes();

        require base_path('routes/channels.php');
    }
}

Enter fullscreen mode Exit fullscreen mode

As you can see, it makes a call to the Broadcast::routes() method and requires the routes/channels.php file, making the broadcasting routes active in the process.

For one or two simple bindings like in this example, you can use the AppServiceProvider but in the case of services that require more complex logic to be executed, you can create new service providers using the php artisan make:provider <provider name> command.

The Full Picture

In the previous sections, I've introduced you to different concepts such as dependency injection, inversion of control, service container, service providers. By now you should have a solid idea of what the container is, how to bind classes to it and retrieve them when necessary. In this section, I'll show you how all these ideas work in harmony.

Let's go back to the Publication class you worked with a few sections ago. I hope you remember that the Publication class was previously dependent on the TwitterService class. But now that you've introduced interfaces to the project, let's update Publication to make use of that.


<?php

namespace App;

use App\Models\Post;
use App\Interfaces\SocialMediaServiceInterface;

class Publication {
    protected $socialMediaService;

    public function __construct(SocialMediaServiceInterface $socialMediaService)
    {
        $this->socialMediaService = $socialMediaService;
    }

    public function publish(Post $post)
    {
        $post->publish();

        $this->socialize($post);
    }

    protected function socialize($post)
    {
        $this->socialMediaService->share($post);
    }

}

Enter fullscreen mode Exit fullscreen mode

Now instead of depending on a single class, this Publication will accept any class that implements the SocialMediaServiceInterface.

You've also bound the SocialMediaServiceInterface to an implementation of the LinkedInService class, so executing app()->make(SocialMediaServiceInterface::class); should return an instance of the LinkedInService class.

One class that you haven't bound to the container however is the Publication class itself. But what if you execute app()->make(Publication::class); without binding it? Let's try this out.


<?php

// routes/web.php

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

Route::get('/', function () {
    $publication = app()->make(Publication::class);

    ddd($publication);

    // App\Publication {#273 ▼
    //     #socialMediaService: App\Services\LinkedInService {#272}
    // }      
});

Enter fullscreen mode Exit fullscreen mode

Turns out it works. Laravel has somehow successfully instantiated an instance of the Publication class without it being bound to the container.

At first glance, this may seem like magic. But in reality, this is the result of all the previously explained concepts working in harmony.

When Laravel encounters the app()->make(Publication::class); line, it goes looking for a corresponding entry in the container. When it doesn't find a binding this class, it looks at the class constructor.

<?php

namespace App;

use App\Models\Post;
use App\Interfaces\SocialMediaServiceInterface;

class Publication {
    protected $socialMediaService;

    public function __construct(SocialMediaServiceInterface $socialMediaService)
    {
        $this->socialMediaService = $socialMediaService;
    }

    //

}

Enter fullscreen mode Exit fullscreen mode

Laravel realizes that the Publication class has a dependency $socialMediaService of type SocialMediaServiceInterface and goes looking for this interface in the container.


<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Interfaces\SocialMediaServiceInterface;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind(SocialMediaServiceInterface::class, function() {
            return new \App\Services\LinkedInService;
        });
    }

    //
}

Enter fullscreen mode Exit fullscreen mode

Upon finding the desired entry, Laravel instantiates a new social media service, feeds that to the Publication class, and successfully creates a new instance.

It should be clear to you that Laravel can instantiate type-hinted dependencies automatically as long as they are not implementing any interface. Keeping this in mind, update your routes/web.php as follows:


<?php

// routes/web.php

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

Route::get('/', function (Publication $publication) {
    ddd($publication);

    // App\Publication {#276 ▼
    //     #socialMediaService: App\Services\LinkedInService {#275}
    // }
});

Enter fullscreen mode Exit fullscreen mode

And the project works just as expected. You see, due to the automatic resolution capability of the container you'll hardly ever take out instances from the container manually. As long as you're binding interfaces properly and type-hinting your dependencies, Laravel will do the heavy lifting.

To Bind or Not To Bind

The final question to answer in this article when should you bind a service to the container. Given the container is capable of instantiating classes without the need for binding, do you even need to consider binding.

The answer should be clear by now but here it is -

You bind a class to the container when it implements an interface.

In the example above, the LinkedInService class implements an interface, so you have to bind the interface to an implementation of the class.

The Publication class, however, doesn't implement any interface. Its only dependency is an implementation of the SocialMediaServiceInterface which is already bound to the container. Hence binding this class was not necessary.

Suggested Reads

Closing Thoughts

The service container is a tough concept to understand, there is no denying that. The fact that you don't use it directly very often, makes it even less important to many developers. But understanding the service container is one of the most important steps of mastering Laravel.

I hope I was able to make the various concepts regarding the service container a bit clearer to you. Knowing what's going on behind the scenes will help you in the long run.

From now on, you should be a tad bit more confident when type-hinting dependencies in your class constructors.

Discussion (7)

Collapse
bawa_geek profile image
Lakhveer Bawa

Thanks, you made it super easy

Collapse
fhsinchy profile image
Farhan Hasin Chowdhury Author

Thanks a lot for letting me know @bawa_geek I almost thought maybe no one's understanding anything 😅

Collapse
bawa_geek profile image
Lakhveer Bawa

your custom Container class made it super clear, however I am trying to understand where we might wan tto use that app()->make(someClass); how can it help.

I use dependency injection already, but not sure how iOC container can help

Thread Thread
fhsinchy profile image
Farhan Hasin Chowdhury Author • Edited

The IoC container helps by performing the instantiation for you.

Assume one of your classes depends on another class and a bunch of other data.

Now every time you want to instantiate an object from that class, you'll have to use new and carefully pass all the parameters.

Assume that one of those dependencies changes during development, now you'll have to painstakingly find all the new statements and update them.

The IoC container allows you to bind a closure that can return an instance of that class. Now anytime you want an object, ask the IoC container. It knows how to make that object. Also if any change occurs, you'll have to change your code in only one place.

Coding with Laravel means coding with elegance. Your code should be functional and beautiful. The auto resolution feature is another example of that. If you type hint your dependencies properly, you won't even have to use app()->make(), saving you some keystrokes and making your code more readable in the process.

There are other benefits regarding testability and stuff but I'll talk about those another day hopefully. Let me know if you've further questions @bawa_geek

Collapse
chinyeins profile image
Chinyeins

Very nice, thanks for the brief summary 💪🏼

Collapse
aryangomes profile image
Aryan Gomes

Great explanation! 😃

Collapse
legend_arome profile image
Onoja Abraham

Very informative and expository. Thank you