How to inject dependencies into route closures in Laravel

Updated: January 15, 2024 By: Guest Contributor Post a comment

Introduction

Dependency injection is a powerful technique in object-oriented programming that allows for decoupling components and making the code more modular and testable. In Laravel, dependency injection can be effortlessly used across different parts of the application including controllers, services, and even route closures. This tutorial explores how you can inject dependencies into Laravel’s route closures, taking you from the basics to more advanced concepts.

Understanding Dependency Injection in Laravel

Broadly speaking, dependency injection means providing a class or function with the instances of objects it requires instead of creating them internally. Laravel’s service container is a powerful tool for managing class dependencies and performing dependency injection.

Basic Route Closure with Dependency Injection

Route::get('/user', function (UserRepository $userRepo) {
   // You now have access to the UserRepository here.
   $user = $userRepo->find(1);
   return $user;
});

In the example above, the UserRepository is injected into the route closure. When the route is hit, Laravel will automatically resolve the UserRepository out of the service container and pass it to the closure.

Resolving More Than One Dependency

What if your route needs more than one dependency? Laravel gracefully handles multiple injections. Here’s an example:

Route::get('/post/{id}', function ($id, PostRepository $postRepo, LoggerInterface $logger) {
    $logger->info('Fetching post with id: ' . $id);
    $post = $postRepo->find($id);
    return view('post.show', compact('post'));
});

This route closure is getting injected with both a PostRepository and a LoggerInterface, which Laravel resolves automatically for you. Note that route parameters like $id don’t need to be resolved out of the container and are simply passed as arguments in the order they appear.

Type-Hinting Controller Methods Instead

Although it’s possible to inject dependencies directly into route closures, it’s often preferred to use controllers, especially when dealing with complex logic. Injecting dependencies into controllers works in a similar fashion:

class UserController extends Controller
{
    protected $userRepo;

    public function __construct(UserRepository $userRepo)
    {
        $this->userRepo = $userRepo;
    }

    public function show($id)
    {
        $user = $this->userRepo->find($id);
        return view('users.show', compact('user'));
    }
}

You can then define your route to use the controller method:

Route::get('/users/{id}', 'UserController@show');

This is a cleaner and more scalable approach as your application grows in complexity.

Advancing with Contextual Binding

If you need to inject a specific instance of a class when a particular closure is executed, contextual binding is your go-to. This allows for fine-grained control over the instances that are injected. Here’s how to do it.

this->app->when('App\Http\Controllers\UserController')
   ->needs('App\Contracts\UserRepository')
   ->give(function () {
       return new CacheUserRepository(new EloquentUserRepository);
   });

Contextual binding provides a specific repository implementation only when resolving dependencies for the UserController.

Using Service Providers for Complex Injection

For advanced dependency resolution, especially when multiple bindings and contextual injections are involved, service providers are a solid architecture choice. Inside a service provider, you can define how dependencies should be resolved in a systematic and reusable way.

A Big Example

Suppose you have an application where you need to inject different implementations of a service based on the context. For instance, let’s say you have an interface PaymentGatewayInterface with two implementations: StripePaymentGateway and PayPalPaymentGateway. The implementation to be used might depend on the user’s preference or any other business logic.

First, define your interface and implementations:

interface PaymentGatewayInterface {
    public function processPayment($amount);
}

class StripePaymentGateway implements PaymentGatewayInterface {
    public function processPayment($amount) {
        // Stripe payment processing logic
    }
}

class PayPalPaymentGateway implements PaymentGatewayInterface {
    public function processPayment($amount) {
        // PayPal payment processing logic
    }
}

Next, create a Service Provider:

1. Generate a service provider using Artisan:

php artisan make:provider PaymentServiceProvider

2. Open the newly created PaymentServiceProvider in the app/Providers directory.

3. In the register method of the service provider, you can define the logic for deciding which implementation to bind to the PaymentGatewayInterface:

use Illuminate\Support\ServiceProvider;

class PaymentServiceProvider extends ServiceProvider {
    public function register() {
        $this->app->bind(PaymentGatewayInterface::class, function ($app) {
            // Logic to determine which implementation to use
            if (/* some condition */) {
                return new StripePaymentGateway();
            } else {
                return new PayPalPaymentGateway();
            }
        });
    }
}

In this example, the closure within the bind method contains the logic to determine which implementation of PaymentGatewayInterface to return.

4. Finally, register the PaymentServiceProvider in your config/app.php under the providers array:

'providers' => [
    // Other Service Providers...

    App\Providers\PaymentServiceProvider::class,
],

Now, whenever you type-hint PaymentGatewayInterface in your controllers or other classes, Laravel’s service container will automatically inject the correct implementation based on the logic you’ve defined in your service provider.

use App\Services\PaymentGatewayInterface;

class PaymentController extends Controller {
    protected $paymentGateway;

    public function __construct(PaymentGatewayInterface $paymentGateway) {
        $this->paymentGateway = $paymentGateway;
    }

    public function processPayment(Request $request) {
        $amount = $request->amount;
        $this->paymentGateway->processPayment($amount);
        // Further logic
    }
}

This example demonstrates how you can use a service provider for complex dependency injection scenarios, enabling you to manage dependencies in a clean and flexible manner.

Best Practices and Pitfalls

A few points to keep in mind are:

  • Avoid using closures for routes that will be cached, because PHP cannot cache closures.
  • Adhere to the Single Responsibility Principle even within route closures to make your code more maintainable.
  • Use route model binding to resolve model instances directly.

Dependency injection is an important aspect of writing testable, maintainable code in Laravel. As you’ve seen, Laravel’s service container makes this process intuitive and scalable. By following the practices outlined in this article, you can leverage Laravel’s capabilities to write cleaner and more testable code with dependency injection.

Conclusion

Incorporating dependency injection into your Laravel route closures can significantly improve your workflow and application’s modularity. Understand Laravel’s service container, start with simple injections, and utilize service providers and contextual bindings for intricate scenarios. Embrace these techniques to craft maintainable and testable code.