August 7, 2025

Understanding Dependency Injection in Laravel: Achieving Flexibility with Service Interfaces

In modern web application development, maintainability and testability are crucial. Laravel, one of the most popular PHP frameworks, makes it easier to follow software engineering best practices such as Dependency Injection (DI) and Inversion of Control (IoC). The IoC is the design principle, which means the control of the program flow is transferred from the application code to the framework or containers. The framework or container begins to manage the lifecycle of the code. While the DI is the specific way to achieve the IoC, the object dependency is from the outside; otherwise, the object creates itself.

In this blog, we’ll explore how Laravel uses dependency injection to decouple components and achieve flexibility by using interfaces and service binding.


The Problem with Hardcoding Dependencies

Let’s start with an anti-pattern example:

class OrderController extends Controller
{
    protected $orderService;

    public function __construct()
    {
        $this->orderService = new RealOrderService(); // ❌ Hardcoded dependency
    }

    public function store(Request $request)
    {
        $this->orderService->placeOrder($request->all());
    }
}

This controller directly depends on RealOrderService, making it:

  • Hard to replace RealOrderService with a mock or different implementation
  • Difficult to write unit tests
  • Rigid and tightly coupled

Solution: Use an Interface and Inject It

Let’s refactor using Dependency Injection.

1. Define an Interface

namespace App\Services;

interface OrderServiceInterface
{
    public function placeOrder(array $orderData);
}

2. Implement the Interface

namespace App\Services;

class RealOrderService implements OrderServiceInterface
{
    public function placeOrder(array $orderData)
    {
        // Real order processing logic
    }
}

You can also create a mock implementation:

namespace App\Services;

class MockOrderService implements OrderServiceInterface
{
    public function placeOrder(array $orderData)
    {
        // Log fake order for testing
    }
}

3. Inject the Interface into the Controller

use App\Services\OrderServiceInterface;

class OrderController extends Controller
{
    protected $orderService;

    public function __construct(OrderServiceInterface $orderService)
    {
        $this->orderService = $orderService;
    }

    public function store(Request $request)
    {
        $this->orderService->placeOrder($request->all());
    }
}

Now OrderController is not tied to any concrete implementation. Laravel will automatically inject the appropriate class.


Binding the Interface to a Concrete Class

// app/Providers/AppServiceProvider.php

use App\Services\OrderServiceInterface;
use App\Services\RealOrderService;

public function register()
{
    $this->app->bind(OrderServiceInterface::class, RealOrderService::class);
}

For testing purposes, you can bind a mock service instead:

$this->app->bind(OrderServiceInterface::class, MockOrderService::class);

You can place this in:

  • A test case’s setUp() method
  • A custom service provider for development
  • Anywhere appropriate for your environment

Benefits of This Approach

  1. Testability
    Easily mock dependencies for unit tests.
  2. Flexibility
    Swap out implementations without touching the controller code.
  3. Maintainability
    Follow SOLID principles, especially the Dependency Inversion Principle.
  4. Separation of Concerns
    The controller is focused on handling HTTP requests, not business logic.

Example Test with Mock Binding

public function testStoreCallsPlaceOrder()
{
    $mock = Mockery::mock(OrderServiceInterface::class);
    $mock->shouldReceive('placeOrder')->once();

    $this->app->instance(OrderServiceInterface::class, $mock);

    $response = $this->post('/orders', [/* ... */]);

    $response->assertStatus(200);
}

Final Thoughts

Using interfaces and dependency injection is a small shift in design that brings massive long-term benefits in any Laravel project. It promotes clean architecture, decoupled components, and testable code. If you’re not already using service interfaces and bindings, now is the time to start.