Dependency Injection – With Laravel
Dependency injection is a commonly used design pattern in object oriented programming. Through some pre-established conventions, we are able to manage the creation of our dependencies more easily. We can declare, replace, or even mock the dependencies as needed without the need to change the code that relies on the dependency.
For example, let’s say we have some authentication logic
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class AuthLogic { function authenticate(Request $request): bool { try { $payload = $request->all(); $token = $this->verifyAuthToken($payload); $this->verifyClient($token); $this->verifyPermissions($token); return true; } catch (Exception $e) { return false; } } } |
We want to write a test for any code that interacts with this logic, you can see that we would need to seed a lot of data upfront. For any authenticated controller test, we would need to seed a client, a user, some permissions, and then generate an auth token for the above. Using dependency injection, we can avoid all this setup and just mock authenticate
to always return true
or false
. Let’s take a look at how to do so in Laravel!
How to perform Dependency Injection
Like most frameworks, Laravel allows us to use dependency injection to organize our code. Let’s take a look at how to do it! These are the steps that we will need to perform.
- Define the dependency
- Bind the dependency
- Inject the dependency
- (Optional) Mock/Replace the dependency
Define the Dependency
First, you need to define a class of objects that other classes will depend on. Here, we will create a simple HelperService
class that we want to use in other parts of our code.
1 2 3 4 5 6 7 8 9 10 11 12 |
class HelperService { function __construct() { $this->counter = 0; } function incrementAndGetCounter(): int { $this->counter += 1; return $this->counter; } } |
Bind the Dependency
Now, we need to construct the dependency and bind it to the Laravel service container. In Laravel, this is usually done by creating a service provider and registering it.
1 |
php artisan make:provider HelperServiceProvider |
In the generated file, you would register your binding and the function that should be executed whenever that binding is needed. This is where we would construct the class.
1 2 3 4 5 6 7 8 9 10 11 12 |
use Illuminate\Support\ServiceProvider; class HelperServiceProvider extends ServiceProvider { function register() { // use $this->app->singleton to only instantiate once $this->app->bind(HelperService::class, function() { return new HelperService(); }); } } |
Finally, you would bind the HelperServiceProvider
to the Laravel service container in config/app.php
1 2 3 4 |
'providers' => [ # ...existing providers App\Providers\HelperServiceProvider::class, ] |
Inject the Dependency
Now that the dependency binding is declared to the Laravel service container, we can inject it into our Laravel code wherever it is needed. For instance, your can use it in your controllers or middleware through automatic injection. Laravel will detect that you want a HelperService
class which it can match to the binding you declared earlier.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class HomeController extends Controller { function __construct(HelperService $helperService) { $this->helperService = $helperService; } function home() { return [ 'counter' => $this->helperService->incrementAndGetCounter() ]; } } |
We could also manually inject that dependency into any other class or function directly using the Laravel App
facade.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
use Illuminate\Support\Facades\App; class UserService { function trySomething(HelperService $helperService) { print($this->helperService->incrementAndGetCounter()); } } // automatically inject the instance as a function argument $result = App::call([new UserService, 'trySomething']); // alternatively you can resolve the instance and pass it in yourself $helperService = App::make(HelperService::class); $result = (new UserService)->trySomething($helperService); |
You can see that we can now make use of the new HelperService()
invoked in the service provider in all the subsequent code without explicitly constructing it. The construction of the class is controlled by the Laravel startup process.
Mock/Replace the Dependency
Now that we are using dependency injection to manage the dependency objects, we have the ability to mock them and replace them during runtime. This is particularly useful during tests (e.g. mocking your authentication to always return true).
You can read more about binding objects to the service container here: https://laravel.com/docs/9.x/container
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
use Mockery\MockInterface; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class HomeControllerTest extends TestCase { use RefreshDatabase; public function testHome(): void { $this->mock(HelperService::class, function (MockInterface $mock) { $mock->shouldReceive('incrementAndGetCounter')->andReturn(5); }); $response = $this->getJson('/', []); $this->assertEquals($response['counter'], 5); } } |
Now, instead of instantiating the actual HelperService
in our tests, we can use a mock of that object instead! This is great for isolating parts of your code to perform your unit tests more easily.
You can find more about mocking here: https://laravel.com/docs/9.x/mocking#mocking-objects
When to use Dependency Injection
Dependency injection is a great tool for organizing our code, but like any other tool, we should only use it when appropriate. Personally, I find that dependency injection is most useful for the following 2 purposes:
- We need to maintain and modify some state throughout the request lifecycle (e.g. a singleton that is used throughout middlewares, controllers, and service classes). This is somewhat similar to why I would put something in Redux, when passing some object down through many layers of components/services becomes unwieldy.
-
We want some logic or state that we may want to abstract away and make it mockable (e.g. http clients, auth state)
A great example is authenticate state. Usually, you may need to perform many steps before you can create an authenticated request. This could be too complicated and make your tests too coupled with your authentication logic. Do you really need to re-test how to generate an authentication token in every API request test that you create?
Using dependency injection, we could instead just seed the bare minimum and mock the authentication layer. The resulting testing code could look like below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
class AuthMiddleware { function __construct(AuthLogic $authLogic) { $this->authLogic = $authLogic; } function handle(Request $request, Closure $next) { if ($this->authLogic->authenticate($request)) { return $next($request); } else { abort(Response:HTTP_FORBIDDEN); } } } class HomeControllerTest extends TestCase { function testHome() { $this->mock(AuthLogic::class, function (MockInterface $mock) { $mock->shouldReceive('authenticate')->andReturn(true); }); $response = $this->getJson('/', []); $this->assertNotEquals($response->code, Response:HTTP_FORBIDDEN); } } |
Conclusion
Now you know why, when, and how to use Dependency Injection in Laravel. This technique can also be applied to other frameworks, Spring Boot in Java, Ruby on Rails, Django, and most other web frameworks all have ways to implement this technique. Just remember to use it when appropriate! Don’t go overboard with your mocking! Testing with the real thing is always needed at some point!