Introduction to the Laravel Service Container
The Laravel's Service Container can be a bit of a mystery, especially if you're not using it that often. The way it works is beautiful and powerful, allowing you to easily inject dependencies, resolve custom-built services, replace live services with fake services for testing, and even create Singleton instances that persist their properties throughout the app. If you don't know what some of these things mean, don't worry - I'll walk you through everything.
Although the topic might be a little daunting, it pays to know it and you'll become a better developer because of it.
What is a Service Container?
A service container is essentially a map of all the registered services in your application. A typical service might be your ClientRepository, or a StatisticsService. Some sort of a class that's used in many parts of your application.
But don't worry, you won't have to set up or register every single service class that you use in the application. Laravel provides a zero-config way of utilising the Service Container to resolve the required services or inject a dependency, and, more than not, you won't have to write any additional code to reap the benefits of cleaner code and composition.
I can keep talking about how awesome it is, but it's best to just see it in action. In this article I'll run through a few example code snippets, which you can easily improve by utilising the Service Container. Let's begin!
The sample service class
Below is a very simple PHP class that we'll be working with in this article. SubscriptionService
is a custom and hypothetical class that deals with our user subscriptions. Things like adding a new credit card, charging a subscription, or unsubscribing the user. The implementations here don't matter, so I haven't included them in order to make the code easier to read and focused on the topic at hand. Now let's see at a few different ways that we can use take this class and use it in a Laravel controller.
Using the service in a controller
The classic approach
If you are new to the Laravel or Symfony framework, you might think that the best way to use this service in a controller is to just create a new instance of it. We need to know which user is this subscription related to, so we fetch the currently logged-in user as well. Then we just return the view and pass it some data from the
SubscriptionService
. You can see here that theSubscriptionService
is a dependency in this controller method. Without it, we would not be able to get the necessary data.user(); $subscriptionService = new SubscriptionService(); return view('billing.index', [ 'creditCards' => $subscriptionService->getCreditCards($user), 'isSubscribed' => $subscriptionService->isSubscribed($user), ]); } }There's a couple of better ways to include this dependency in the controller, which have more configuration power and chained dependency injection, which I will talk about later. But for now, let's run through some examples.
Resolving the service from the Service Container
user(); $subscriptionService = resolve(SubscriptionService::class); return view('billing.index', [ 'creditCards' => $subscriptionService->getCreditCards($user), 'isSubscribed' => $subscriptionService->isSubscribed($user), ]); } }The only thing we've done here is replace the
new
keyword with aresolve()
function call, passing the class name to it. We can easily just pass the"App\Services\SubscriptionService"
string to theresolve()
function and avoid having to include theuse ...
statement, but I prefer to extract the class namespaces for easier navigation in the code editor, as well as shorter lines of code for better comprehension.Well, how does it work? The
resolve()
is a helper function, which goes to the Service Container, where all the different services are registered, and asks if the Service Container has a binding for the requested class. If there is, it returns the bound instance, or builds it from scratch based on configured options. In our case we haven't done any configuration or binding at all! That's okay, because if the Service Container cannot find the requested class binding, it just creates a new instance of that class and returns it to you.Well that's no different from using the
new
keyword to create a new instance yourself! That is true, and the benefits of resolving an instance like that become apparent later once the services become more complex and have their own dependencies to resolve. Now let's check the other, the preferred, way to utilise the Service Container.The Dependency Injection
user(); return view('billing.index', [ 'creditCards' => $subscriptionService->getCreditCards($user), 'isSubscribed' => $subscriptionService->isSubscribed($user), ]); } }We have now moved the
SubscriptionService
up to the function parameters list. This controller method index now has one parameter and expects an instance of theSubscriptionService
class. But how does it get that? We're not calling this "index" method ourselves, are we? This is something that the Laravel Router does automatically by matching the requested URL paths with the respective Controllers and their methods. You set these up in the router files, remember? So how does Laravel know that this function expects an argument?When Laravel matches the request with a controller method, it first inspects it, thanks to the PHP's Reflection capabilities. It then knows what parameters your controller method is expecting, and then goes ahead and resolves (remember the
resolve()
method?) them from the Service Container. Once the Router has all the required instances, it calls the controller method, passing down the instances as parameters. That is called Dependency Injection and Laravel does a wonderful job at it.But what's the difference? Surely this is just making extra function calls and trips back and forth to the Service Container just to get this class instance, right? Is it even efficient? How is it better than creating a new service instance yourself?
- First of all, yes, it's efficient! PHP nowadays is so fast, that you won't even notice the difference. Furthermore, thanks to the PSR-4 autoloader, the classes are lazy-loaded, which means that even if you have hundreds of different services in your app, the Service Container will not load any of them until you actually need to resolve them or configure them;
- Second of all, this helps you clean up your code and separate the concerns. By injecting a service as a function parameter, we think of it as a dependency, and then we leave the body of the function to actually perform the required actions instead of setting up all the necessary class instances.
- Lastly, This will allow you to easily set up chain dependency injection. That's where the Service Container really starts to shine, so let me explain!
The Dependency Injection Chain
Now, let's expand the requirements (as it always happens in real life) of the SubscriptionService
. The business has decided that we cannot keep credit card data on our servers, and we need to use a different, external service for that. Perhaps we have decided to use a service like Stripe to store the user's credit cards so we don't have to deal with the financial regulations that come with storing such data. In this case, the SubscriptionService
needs to have access to that external service in order to perform these tasks. It needs a dependency of its own.
Let's introduce a StripeService
:
The class looks very similar to the SubscriptionService, but the implementation deals with accessing the credit cards from Stripe instead of our own database.
And here's the updated
SubscriptionService
with theStripeService
dependency:stripeService = $stripeService; } public function addCreditCard(User $user, CreditCard $card) { $this->stripeService->addCreditCard($user, $card); // ... } // ... }Now the constructor for
SubscriptionService
expects an instance of theStripeService
in order to access the credit cards from Stripe. Great! Now, how about the controller? Let's look again at the traditional method first.The traditional method
user(); $stripeService = new StripeService; $subscriptionService = new SubscriptionService($stripeService); return view('billing.index', [ 'creditCards' => $subscriptionService->getCreditCards($user), 'isSubscribed' => $subscriptionService->isSubscribed($user), ]); } }You can see the controller is growing with an increasing number of instances you need to create in order to retrieve some data about the user. Sure, that's just two new instances right now, but it can easily grow as you expand the service with more dependencies. You might have other dependencies in the future. For example, a PayPal service for some users who choose to subscribe using their PayPal accounts.
Now let's see if we can clean up some code using the Service Container.
Resolving from the Service Container
I like the Dependency Injection method, the injection of services by adding them as type-hinted parameters to the controller methods, so let's move the SubscriptionService to the parameters list first.
user(); return view('billing.index', [ 'creditCards' => $subscriptionService->getCreditCards($user), 'isSubscribed' => $subscriptionService->isSubscribed($user), ]); } }We were able to remove the code for
StripeService
instantiation, because we're not instantiating theSubscriptionService
service here. But wait a minute... TheSubscriptionService
constructor requires an instance ofStripeService
, so how will it work?Service Container will take care of it! And there's no configuration needed for it to happen.
Here's a breakdown of how the Service Container resolves this service for you and how it deals with chain dependencies.
- First, thanks to PHP Reflection abilities, it sees that the controller's
index()
method requires one parameter and that parameter is of typeSubscriptionService
. - It then tries to resolve this service from the Service Container. Because we haven't done any special configuration to register this service with the Service Container, it simply tries to create a new instance of this service.
- Before creating a new instance of a class, the Service Container inspects the
__constructor()
method of that class. As a refresher, here's what the constructor of the SubscriptionService class looks like:
class SubscriptionService { /** @var StripeService */ protected $stripeService; public function __construct(StripeService $stripeService) { $this->stripeService = $stripeService; } // ... }
The Service Container now sees that this constructor requires one parameter and it's also type-hinted. This helps the Service Container figure out what class instance is expected here.
- The Service Container now knows this
SubscriptionService
requires an instance of aStripeService
class, it goes ahead and resolves theStripeService
as well! Once again, because we haven't done any special configuration to set up theStripeService
with the Service Container, it simply creates a new instance of it and returns that. - Now that we have an instance of
StripeService
, the Service Container passes that to the__constructor()
method of theSubscriptionService
and thus completes the initialisation for the service. - The Service Container now has a working instance of the
SubscriptionService
and so has completed the resolution of it, which was required by the controller method. The Laravel Router can now finally call the controller method, passing the requiredSubscriptionService
instance as a parameter.
All of this happens automatically, behind the scenes. So essentially, you could replace code like this:
$firstDependency = new FirstDependency; $secondDependency = new SecondDependency; $service = new Service($firstDependency, $secondDependency);
With a simple resolution call like this:
$service = resolve(Service::class);
The Service Container will resolve any dependencies for this class automatically and inject them into the constructor. This way you spend less time setting up this service, and more time on other, more important aspects of your app.
The caveats, the "gotchas"
Alright, so all of this looks like a lot of so-called "Laravel magic" is happening behind the scenes. I gave you an abstract, top-level view of what's happening, and although you don't need to know the inner workings of the Service Container in order to utilise it, you do need to know a few gotchas, or rules, about the Service Container to help you save hours of debugging in the future.
Learn where the Dependency Injection actually works
Although there are no limits as to where you can call the resolve()
or app()
methods to resolve a class instance with all its dependencies, injecting dependencies as function parameters can be a little limiting. In short, you can be almost guaranteed that the function parameter will be automatically resolved if it's a method that's being called by the Laravel framework itself, not by you. Let me explain.
Here's an example Laravel Job class. A hypothetical class to add the given credit card to the user's subscription using the SubscriptionService
class (which has a dependency of it's own, remember?)
user = $user; $this->creditCard = $creditCard; } /** * Execute the job. * * @return void */ public function handle() { $this->subscriptionService->addCreditCard($this->user, $this->creditCard); // ... } }
Now you see that the constructor of this job requires 2 parameters - a user, and the credit card being added to that user's subscription. Notice that the handle()
method requires us to have an instance of the SubscriptionService
, which we use to handle all credit card and subscription-related activities. So how can we do this properly by using Dependency Injection?
Your first instinct might tell you to inject it in the __constructor()
parameters, right? The Service Container, after all, resolves the constructor dependencies automatically, right? Not always. The Service Container only resolves the constructor parameters if that class itself is being resolved by the Service Container. In other words, if we were to build up the job like so:
$job = resolve(App\Jobs\AddCreditCardToSubscription::class); // wrong approach - how do we pass the user and the credit card?
But the above is not the way to create or dispatch a Laravel Job. Let's look at the correct ways:
use App\Jobs\AddCreditCardToSubscription; // option 1 - use the dispatch() helper method dispatch(new AddCreditCardToSubscription($user, $creditCard)); // option 2 AddCreditCardToSubscription::dispatch($user, $creditCard);
Both of these work great and they work the same way - it just looks different and it's up to your taste. Although, do bear in mind, that the second option requires your job class to use the Illuminate\Foundation\Bus\Dispatchable
trait.
The wrong approach
Can we inject a SubscriptionService
instance ourselves? Sure, we can, but where's the magic in that:
/** * Create a new job instance. * * @return void */ public function __construct(User $user, CreditCard $creditCard, SubscriptionService $subscriptionService) { $this->user = $user; $this->creditCard = $creditCard; $this->subscriptionService = $subscriptionService; }
Because we're not asking Laravel to resolve a new job class and we're creating a new instance of the job ourselves - we have to provide all of the constructor parameters ourselves. Which means we need an instance of the SubscriptionService
:
use App\Services\SubscriptionService; use App\Jobs\AddCreditCardToSubscription; // ... $subscriptionService = resolve(SubscriptionService::class); dispatch(new AddCreditCardToSubscription($user, $creditCard, $subscriptionService));
That's not the most elegant approach. Can we do better?
The correct way
Remember how I said that Laravel would resolve the dependencies for a function that the Laravel framework itself calls? Well, in a job class, there's a special method called handle()
, which is called by the Laravel's Worker when it begins processing the job. We can use this fact to utilise Dependency Injection by adding the function parameter here:
user = $user; $this->creditCard = $creditCard; } /** * Execute the job. * * @return void */ public function handle(SubscriptionService $subscriptionService) { $subscriptionService->addCreditCard($this->user, $this->creditCard); // ... } }
Since we now have an instance of the SubscriptionService
in the handle()
method as a parameter, we no longer need it in the constructor and we no longer need to save this instance in a class property $subscriptionService
.
Not only does the code look cleaner without the extra class property, but it also frees us from having provide the service instance ourselves.
Now we can easily dispatch the job like so:
use App\Jobs\AddCreditCardToSubscription; // ... dispatch(new AddCreditCardToSubscription($user, $creditCard));
And the Service Container will take care of injecting the right dependencies into the handle()
method of this job. Much better, isn't it?
So what other places can I use Dependency Injection in?
Here are some of the parts or Laravel where you can utilise Dependency Injection by providing a type-hinted function parameters:
- Controller constructors
- Controller action/route methods
- Event listeners
- Job handlers
- View Composer constructors
- Form Request constructors
- Command handlers
- Command closures
For anything else, you can easily just use the resolve()
or app()
helper methods to resolve a complete instance of a given class, including all of its dependencies.
What can it NOT resolve?
If, anywhere in the dependency chain, a class requires a parameter that's not a type-hinted class, or if the parameter is of scalar types (integer, string, boolean, array, etc), the Service Container will not be able to resolve these parameters and it will throw a Illuminate\Contracts\Container\BindingResolutionException
exception, giving you the details of the class and its parameter that it could not resolve.
Most common fixes would be to make these scalar types optional by giving them default values:
class SubscriptionService { /** @var StripeService */ protected $stripeService; /** @var bool */ protected $shouldNotifyUser; public function __construct(StripeService $stripeService, bool $shouldNotifyUser = true) { $this->stripeService = $stripeService; } // ... }
Be careful with Route Parameter Binding
You might've also seen "Dependency Injection" in the form of binding route parameters to the respective Eloquent model instance. For example:
The one thing to be careful about here, is the parameter names. The parameter names in the URL definition must match the parameter names in the Controller method. If the names don't match, the model binding will fail and you'll simply receive an empty instance of that model straight from the Service Container.
The above example will NOT work, because the controller method parameter name is different from the router's URL definition. The parameter name has to be changed to
$client
in order for the correct model binding resolution to work.A note on Facades
Facades are another great way of linking a contract to a specific implementation. It brings additional features helpful when testing, and allows easy swapping of underlying implementation classes without changing your use of contract.
For example, if you have 2 different drivers for handling user's subscription, such as
StripeSubscriptionService
andArraySubscriptionService
(for testing purposes), you might have aSubscriptionService
facade that resolves to a particular binding when needed:use Illuminate\Support\Facades\Facade; class SubscriptionService extends Facade { protected static function getFacadeAccessor() { return 'subscription-service'; // Alternatively, you can just return the implementation // class name, but you'll need to use that when switching // the bindings. return StripeSubscriptionService::class; } }Then you only need to register the
'subscription-service'
binding in yourAppServiceProvider
like so:class AppServiceProvider extends ServiceProvider { public function boot() { $this->app->bind('subscription-service', StripeSubscriptionService::class); } }Once that is set up, you can easily use your new facade to call any instance methods on the implementation class:
$isSubscribed = SubscriptionService::isSubscribed($user);There is one caveat with Laravel Facades, and that is the fact that the resolved instances are cached and returned in future calls. You can learn more about it here.
Summary
I know this has been a lot to take in, but hopefully you have learned something new! So far, we have talked about using the Service Container with zero configuration - that's a great way to start utilising some of the "Laravel's magic" to make your life easier. More often that not, you won't need to configure anything.
But if your app is more complex (and it will be at some point!) and you wish to learn more about it, look forward to my future articles about the Advanced usage of the Service Container.