All about the Laravel Container and Dependency Injection

Photo by Ian Taylor on Unsplash

All about the Laravel Container and Dependency Injection

ยท

4 min read

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!

ย