Laravel Service Container

Laravel Service Container

Summary

In the realm of web development, managing dependencies effectively and ensuring code remains flexible and maintainable is crucial. Laravel offers a powerful tool known as the Service Container to address this.

In this post, we’ll delve into understanding the Laravel Service Container, why it’s invaluable, and how it can make your development process smoother. We’ll explore the intricacies of Dependency Injection, the significance of Interfaces, and the convenience of Laravel’s app() helper function.


The Laravel Service Container is a topic that can be a little confusing to developers starting out with Laravel.

In order to understand Service Containers (also called IoC containers – Inversion of Control containers), we’re going to start right at the beginning.

Service Containers are used for registering components, typically classes, for use throughout your Laravel application. Through the use of examples it should start to become clearer.

Note that all code samples below work on PHP8+ and Laravel 10+.

1. Instantiating Dependencies

Before getting into the Service Container, let’s take a look at how we would create a PHP class which has other class dependencies.

For example, let’s assume we want a Company class which depends on a User class (because the Company belongs to a User).

One way of doing that is to new up the User class in the Company class, like this:

<?php
class Company
{
	public function ownerName()
	{
		$user = new User($this->ownerId);
		return $user->name;
	}
}

In this example we’ve new’ed up the User class and passed it the ownerId to retrieve the correct user. We then return the user name property.

The problem with this arrangement is when you need many dependencies, like the User, Departments, Customers, etc. Adding each one would be cumbersome:

<?php
class Company
{
	public function ownerName()
	{
		$user = new User($this->ownerId);
		$department = new Department($this->departmentId);
		$customer = new Customer($this->customerId);
		return $user->name;
	}
}

Besides being an eyesore and cumbersome to write each time you need these dependencies, this approach makes it much harder to switch components if you want to change something later on.

For instance, lets assume we are logging data with a data logger class:

<?php
class Company
{
	public function ownerName()
	{
		$logger = new FileLogger();
		$user = new User($this->id);

		$logger->write(“debug", $user->name);
		return $user->name;
	}
}

What if we want to use a database logger at some point? We’d need to go back throughout our code to make changes to all the classes we’re new’ing up. This is where Dependency Injection could make our lives much easier.

2. Dependency Injection

Dependency Injection is when we “inject” a dependency into the class or method that needs it. That way, the class does not care about the actual class or how the work is done. This of course assumes a standard set of methods and properties in the dependency, so its usually used with Interfaces.

By way of example, lets take our Company class which is logging some data. Using Dependency Injection the code above could look like this:

<?php
// This Logger logs to a file
class Logger
{
	public function write($logLevel, $text)
	{
		file_put_contents($logLevel.“.log”, $text, FILE_APPEND);
	}
}

class Company
{
	public function ownerName(Logger $logger)
	{
		$user = new User($this->id);
		$logger->write(“debug”, $user->name);
		return $user->name;
	}
}

This way, if we wanted to switch the Logger class to rather write to a database, we only need to change our Logger class and the rest of the code using it remains exactly the same.

The way it has been put above has a problem of its own. We might have multiple classes with different class names, so the type hint in the Company class would not be compatible with switching the Logger class.

For instance, if our Logger class was called FileLogger instead of just Logger:

<?php
// This FileLogger logs to a file
class FileLogger
{
	public function write($logLevel, $text)
	{
		file_put_contents($logLevel.“.log”, $text, FILE_APPEND);
	}
}

class Company
{
	public function ownerName(FileLogger $logger)
	{
		$user = new User($this->id);
		$logger->write(“debug”, $user->name);
		return $user->name;
	}
}

$fileLogger = new FileLogger();
$company = new Company();
$company->ownerName($fileLogger);

If we now wanted to switch to our DatabaseLogger class instead we’d still need to make changes to the ownerName function in the Company class, and that’s not achieving our goal of being able to replace class dependencies. So, a better approach is to use an Interface instead, like this:

<?php
interface Logger
{
	public function write(string $logLevel, string $text);
}

// This FileLogger logs to a file
class FileLogger implements Logger
{
	public function write($logLevel, $text)
	{
		file_put_contents($logLevel.“.log”, $text, FILE_APPEND);
	}
}

class Company
{
	public function ownerName(Logger $logger)
	{
		$user = new User($this->id);

		$logger->write(“debug”, $user->name);
		return $user->name;
	}
}

$fileLogger = new FileLogger();
$company = new Company();
$company->ownerName($fileLogger);

In this example our Company class’s ownerName function takes an argument of type Logger which is our interface. When we call the ownerName function, we pass in a FileLogger class as the concrete class and this FileLogger class implements the Logger interface.

This way, if we have a DatabaseLogger class which also implements the same Logger interface, then we can simply switch the Logger concrete class when we call the functions.

This is really handy under a number of situations. For instance, if you have a config file then you could have a “logger” setting in there which references the concrete logger class you want to use.

As an example, this could be helpful during development to set the logger to a File based logger in the config, but in production it could log to S3, or a database, etc.

3. The app() helper function

Laravel has an app() helper function which can help you make components without needing to new them up. If your component has no dependencies or it has only dependencies which Laravel can resolve (because dependencies are type-hinted), then you do not need to explicitly bind this component to the container. Laravel does this by Reflection.

Consider the following code:

//app/Services/ServiceTest2.php
<?php
declare(strict_types=1);
namespace App\Services;
class ServiceTest2
{
    public function say()
    {
        return "<p>This is from ServiceTest2</p>";
    }
}
//routes/web.php
Route::get("/container3", function () {
    $st = app()->make(ServiceTest2::class);
    echo $st->say();
});

In the above example Laravel was able to automatically resolve the component in process called Auto-Resolution.

If, however, the component has dependencies which Laravel cannot automatically resolve, as with our ServiceTest class (the first one, not the second one) we’ll need to bind them into the service container so that Laravel can resolve them.

4. The Service Container

Finally, we get to the service container! Understanding the above concepts, particularly Dependency Injection and Interfaces is important to understanding the Service Container.

In a nutshell, the Service Container is a container of components which Laravel and your application. You register (bind) components into the service container and when you need them the service container can make the components up for you. In a very broad sense you can think of it like a key/value data store where you give it a key (typically a fully qualified class name) and it gives you back a value (typically the class with its dependencies resolved).

But before we even start with classes, lets just take a quick look at the concept of giving a label and getting back something from the service container:

In the Laravel directory, open the file app/Providers/AppServiceProvider.php

In the file’s register method, lets bind the label Pumpkin to the string “I am a pumpkin”. We do that by calling the bind method on the app. Each service provider has an app variable available. That would look like this:

public function register(): void
{
	$this->app->bind(“Pumpkin”, function () {
		return “I am a pumpkin”;
	});
}

Once a label has been bound to the service container you can use the make method on the app to get the “value” of the label, like this:

app()->make(“Pumpkin”);

For example, lets put that into our web route:

Route::get(“/container2”, function () {
	echo app()->make(“Pumpkin”);
});

If you now visit https://localhost/container2 in a browser you should see the text “I am a pumpkin”!

You could also put echo app()->make(“Pumpkin”) into tinker to see the same output.

So, we’ve now resolved the label from the container.

Let’s take a look at an example using classes as we would be using in real applications.

Lets assume we have a couple of classes which we’ve put in an app/Services directory (if Services does not exist, you can create it).

We have two class files in there, ServicesTest and ServicesTest2. Each of them has a single function called say (but note this is just for this example, the classes do not need to have the same functions, they can be completely different).

app/Services/ServiceTest.php
<?php
declare(strict_types=1);
namespace App\Services;
class ServiceTest
{
    public function __construct(private ServiceTest2 $service2, private string $toSay)
    {
    }
    public function say()
    {
        return "<p>This is from ServiceTest1</p>".$this->toSay.$this->service2->say();
    }
}

app/Services/ServiceTest2.php
<?php
declare(strict_types=1);
namespace App\Services;
class ServiceTest2
{
    public function say()
    {
        return "<p>This is from ServiceTest2</p>";
    }
}

We also have a ServiceController (php artisan make:controller ServiceController) with the following code:

//app/Http/Controllers/ServiceController.php
<?php
namespace App\Http\Controllers;
use App\Services\ServiceTest;
class ServiceController extends Controller
{
    public function speak(ServiceTest $service)
    {
        print "<p>This is from controller</p>";
        return $service->say();
    }
}

And finally, in our web route we have a route called container, like this:

Route::get("/container", "\App\Http\Controllers\ServiceController@speak");

At this point, if you tried running the application in a browser you would get a BindingResolutionException.

What’s going on? In our web route we try to call the speak method in our ServiceController.

The speak method has a dependency of ServiceTest $service, but we’re not passing this in from the route. Additionally, when you look at the ServiceTest class you’ll notice that it has two dependencies, ServiceTest2 $service2 and string $toSay.

What’s missing is the binding in the AppServiceProvider.php file. We need to bind the ServiceTest class to the app so that Laravel knows how to resolve this class.

//In app/Providers/AppServiceProvider.php
    public function register(): void
    {
        $this->app->bind(ServiceTest::class, function () {
            return new ServiceTest(new ServiceTest2, "<p>This is from AppServiceProvider</p>");
        });
    }

In the bind method, the first argument is the label we’re binding into the app. In this case, we’re binding the FQCN. We bind it to an anonymous function which returns a concrete ServiceTest class which we new up. When we new up this ServiceTest class we pass in the correct dependencies which the ServiceTest class needs.

That’s it! Laravel is now able to resolve the ServiceTest class. The reason it can automatically resolve it in the Controller is because the Service Container is responsible for creating all controllers. But if you needed this class elsewhere in the code you could use the make method like this:

$service = app()->make(App\Services\ServiceTest::class);
$service->say();

As we discussed Interfaces above you can also use an interface as the first argument of the bind methods, but you return a concrete class.

Conclusion

The Laravel Service Container is an indispensable tool for developers looking to craft efficient, maintainable, and scalable applications. By centralizing the management of dependencies and promoting best practices like Dependency Injection, Laravel ensures that applications remain flexible, especially as they grow and evolve.

Leveraging interfaces allows for smoother changes and adaptability, while the app() helper function streamlines component creation. In essence, mastering the Service Container will not only enhance the quality of your Laravel projects but also simplify many aspects of the development process. Whether you’re starting a new Laravel project or refactoring an existing one, understanding and utilizing the Service Container is a game-changer.

Share

Leave a Reply

Your email address will not be published. Required fields are marked *