Tinkerwell 3 is available now! 🎉 Autocompletion, magic-comments, and more! Get your license now

Go back to Blog

The Laravel Service Container demystified

Marcel Pociot

At almost every Laravel conference you can find at least one talk that covers Laravel's service container – and rightfully so.

The service container is one of the most important pieces of the framework. It is responsible for managing your class dependencies and allows you to perform dependency injection.

Why should you use dependency injection?

If you are new to Laravel / modern PHP frameworks you might wonder what "dependency injection" actually means, so let me give you an example.

In your PHP classes, no matter if its a controller, a "service" class, or anything else, you will at some point need to include "dependencies".

Lets say that your application has a GoogleMaps class, which needs some API credentials and you want to use that class in one of your controllers:

class GoogleMaps
{
	// ...
}

class LocationController
{

	public function index(Request $request) 
	{
		$maps = new GoogleMaps();
		
		$coordinates = $maps->getCoordinates($request->get('address'));
	}

}

As you can see, inside the index method, we created a new instance of the GoogleMaps class.

So what's the problem with this, you might ask. Well, what if you want to use the same GoogleMaps class in another part of your application? Or maybe the GoogleMaps class has additional dependencies that you would need to pass.

This quickly gets cumbersome to manage. Because of this, we have the concept of dependency injection. In fact, in the code snippet above, we already use dependency injection. We inject the Request class into our controller method, without creating a new instance of it.

Automatic dependency injection

By default, Laravel allows you to use a completely configuration-free dependency injection system. All you need to do is type hint the class that you want to inject – for example in your controller, middleware, jobs, event listeners, etc.

The example above would become:

class LocationController
{

	public function index(Request $request, GoogleMaps $maps) 
	{		
		$coordinates = $maps->getCoordinates($request->get('address'));
	}

}

By simply type-hinting the GoogleMaps class in our controller method, Laravel is automatically looking up the GoogleMaps class and its dependencies, tries to create a new instance of the class and pass it to the controller method.

In a controller, you might also need a dependency throughout all of the controller methods. In this case, you can simply inject the class in the controllers' constructor:

class LocationController
{
	protected GoogleMaps $maps;
	
	public function __construct(GoogleMaps $maps) 
	{
		$this->maps = $maps;
	}
	
	public function index(Request $request) 
	{		
		$coordinates = $this->maps->getCoordinates($request->get('address'));
	}

}

This zero-configuration dependency injection is amazing and you will most likely not need to manually configure dependency injection. There are, however, certain situations where you might want to handle dependency injection manually. Let's take a look at some examples.

Manual binding

Imagine that our GoogleMaps class requires some API credentials in its constructor, like this:

class GoogleMaps
{
	public function __construct(protected $apiKey) {}
}

When you now try to inject this class in your controllers, events, etc. Laravel will not know what to do with the $apiKey constructor argument and throw an exception, as its unable to create a new instance of the class for you:

'Unresolvable dependency resolving [Parameter #0 [ $apiKey ]] in class GoogleMaps'

So let's tell Laravel how this class can be resolved. Typically, you will register all manual dependencies in a service provider. As the name suggests, this should take place in the register method.

Each service provider has the container available as the app property.

class AppServiceProvider extends ServiceProvider
{
	public function register()
	{
		$this->app->bind(GoogleMaps::class, function($app) {
			return new GoogleMaps(config('services.google.key'));
		});
	}
}

Using the bind method on the app/container, we have now told Laravel that when it should try to resolve the GoogleMaps class, it should create a new instance of the class using a key from our configuration file.

Resolving additional dependencies

When manually binding a class, you might still want to make use of the automatic dependency resolution. For example, our GoogleMaps class could have a second constructor dependency that we want to be resolved from the container automatically instead.

In this case, we can make use of the $app variable that is automatically available when binding something to the container:

class AppServiceProvider extends ServiceProvider
{
	public function register()
	{
		$this->app->bind(GoogleMaps::class, function($app) {
			return new GoogleMaps(config('services.google.key'), $app->make(CoordinateParser::class));
		});
	}
}

As you might have guessed already, the $app->make method can be used to resolve anything from the service container manually.

Binding singletons

Using the bind method will register a regular binding. This means that, every time this dependency will be resolved within one request/response lifecycle, Laravel is going to create a new object. In some situations you do not want this behavior, but you rather want to reuse the same object at various different places. This is called a singleton and can be registered using the singleton method:

class AppServiceProvider extends ServiceProvider
{
	public function register()
	{
		$this->app->singleton(GoogleMaps::class, function($app) {
			return new GoogleMaps(config('services.google.key'));
		});
	}
}

Now resolving the GoogleMaps dependency twice, will return the same object.

Binding existing instances

Instead of instructing Laravel how to build a certain dependency when it is required, you can also bind an actual object instance in the service container. Just like the singleton above, Laravel will always return this one instance when you resolve the dependency:

class AppServiceProvider extends ServiceProvider
{
	public function register()
	{
		$maps = new GoogleMaps(config('services.google.key'));
		
		$this->app->instance(GoogleMaps::class, $maps);
	}
}

Binding primitive values

Laravel's service container does not only allow you to bind classes, but you can also bind primitive values. For example, imagine that one of your controllers requires a value from a config file:

class UserReportController
{
	public function __construct(protected $cacheTtl) {}
}

To bind this, we need to make use of "contextual binding". This tells the service container when to bind which value to a certain class.


class AppServiceProvider extends ServiceProvider
{
	public function register()
	{
		$this->app->when(UserReportController::class)
	    ->needs('$cacheTtl')
	    ->give(now()->addDay());
	}
}

A lot of times, these primitive values might already be available in one of your configuration files. To make your life easier, Laravel has a convenient helper method that allows you to pass a configuration value directly:


class AppServiceProvider extends ServiceProvider
{
	public function register()
	{
		$this->app->when(UserReportController::class)
	    ->needs('$cacheTtl')
	    ->giveConfig('cache.ttl');
	}
}

Its time to Tinkerwell

The must-have tinker tool for every PHP and Laravel developer. This magical desktop app may not be able to fly. But when it comes to PHP code, it sure knows how to run.

Learn more