Hey guys,
Today I'll share the "everything" of Laravel Service Container & the Dependency Injection.
Also, some interviewing tips ๐
This one is the most important and you might think it is enormous and complex, but honestly, it is not.
Glossary
DI aka Dependency Injection: the simplest way to get the dependent instance for your method/class.
Service Container: the global container in Laravel Lifecycle which holds the source of truth about initializing classes.
What's a Service Container?
If you read the documentation from Laravel, it tells you the high level of the Service Container and its usage.
But on a deeper level, you'll ask how and why? Let me show you.
Service Container is a simple key-value container which holds:
The way to initialize the given class.
The bound singleton instances.
The bound primitive types values.
And Service Container will help you to produce and distribute the build instances. Basically, Factory pattern ๐
bind
// AppServiceProvider.php
public function register(): void
{
$this->app->bind(TransferService::class, function () {
return new TransferService($this->app['db']);
});
}
You'll use bind
to give the instruction to Laravel Service Container the "how to initialize this concrete class or interface".
Every time you requested the TransferService
class using DI, the Laravel Service Container will initialize and return you the brand new instance.
class HelloClass {
public function __construct(
// #1 instance
public TransferService $service
) {}
}
class AloClass {
public function __construct(
// #2 instance
public TransferService $service
) {}
}
singleton
Initialize once, utilize it throughout the entire lifecycle.
public function register(): void
{
// normal
$this->app->singleton(TransferService::class);
// conditional
$this->app->singleton(TransferService::class, fn () => ...);
}
Every time you request the TransferService
class using Dependency Injection, the Laravel Service Container will initialize it (once), cache it, and return it to you. For subsequent accesses, the Service Container will return the cached instance.
bind/singleton as a hardcoded string
Basically the bind
or singleton
takes the first parameter as the key
, it can be:
any string
classpath
Depend on your need.
$this->app->bind('seth', fn () => new SethPhat());
$this->app->bind(SethPhat::class);
The only difference is that, when using a hardcoded string as a key, you cannot inject your concrete class through the constructor.
To get the concrete class using the hardcoded string, you need to use the global app
function:
app('seth'); // returns SethPhat instance
app()->make('seth'); // returns SethPhat instance
Note: you can eventually bind the "primitive" values as well, but please avoid doing so. Let's use Config
for that. Service Container should only produce object instances.
bind/singleton with custom parameters
Yes, it is possible, but the same as the above way, we have to use the app
helper.
$this->app->bind(MyInterface::class, function ($app, $params) {
$service = $params['service'] ?? null;
return match ($service) {
'gotenberg' => $this->app->make(GotenbergService::class),
'mpdf' => $this->app->make(MpdfService::class),
};
});
// get the instance
app(MyInterface::class)->makeWith([
'service' => 'gotenberg',
]);
Dependency Injection
Constructor Injection
From your own classes, you can specify the dependencies:
class RenderService
{
public function __construct(
private GotenbergDriver $renderDriver
) {}
}
Inject dependencies for Controller
You can use Contructor Injection
, as well as method injection
class VideoController extends Controller
{
public function render(
VideoRenderRequest $request, // custom form request
Video $video, // eloquent - model route binding
RenderService $renderService // inject
): JsonResponse {
$result = $renderService->render($video);
//...
}
}
Global App Helper
You can use the app
function helper (or App
facade) too, really convenient.
app(RenderService::class;) // will return the RenderService instance
App::make(RenderService::class);
app(MyInterface::class)->makeWith([
'service' => 'gotenberg',
]);
Testing
One of the most significant advantages of DI is that you can mock the dependencies and test your flow easily.
For example I have this class
class RenderService
{
public function __construct(
private GotenbergDriver $renderDriver
) {}
public function render(Video $video): bool
{
$result = $this->renderDriver->render($video);
if ($result->hasError()) {
return false;
}
return true;
}
}
So to test both cases, we can mock the GotenbergDriver
to return ok and error for both cases.
Here is how you will create a mock and let Laravel Service Container serves your mocked instance.
// normal way, pass as a param
public testRenderOk()
{
$video = Video::factory()->create();
$renderMock = $this->createMock(GotenbergDriver::class);
$renderMock->method('render')
->willReturn(RenderResult::ok(...));
$service = new RenderService($renderMock);
$this->assertTrue($service->render($video));
}
// but if you are using "app" helper, follow this
public testRenderFailed()
{
$video = Video::factory()->create();
$renderMock = $this->createMock(GotenbergDriver::class);
$renderMock->method('render')
->willReturn(RenderResult::error(...));
app()->offsetSet(GotenbergDriver::class, $renderMock);
// for example, no constructor injection
// but use "app" in the "render" method
$service = new RenderService();
$this->assertFalse($service->render($video));
}
Conclusion
I hope this topic has provided you with a deep understanding of the Laravel Service Container, making your life easier while working with it.
Thank you for reading!