Dependency Injection with Constructors in PHP

This article explains the core concept of dependency injection (DI) in software development and demonstrates how to implement it using constructor injection in PHP. You will learn the difference between tightly coupled and decoupled code, view a practical PHP code example, and understand the main benefits this design pattern brings to modern PHP applications.

What is Dependency Injection?

Dependency injection is a design pattern used to achieve Inversion of Control (IoC) between classes and their dependencies. Instead of a class creating the external objects it needs to function (its dependencies) internally, those objects are “injected” or passed into the class from the outside.

This prevents classes from being tightly coupled to specific implementations, making your codebase more flexible, maintainable, and easier to test.

Understanding Constructor Injection

Constructor injection is the most common and recommended method of implementing dependency injection. With this approach, you pass a class’s dependencies directly through its constructor (__construct) when the class is instantiated. This guarantees that the class always has its required dependencies before any of its methods are executed.

Step 1: The Tightly Coupled Approach (Without DI)

To understand the benefits of dependency injection, look at this example of tightly coupled code. Here, the OrderService class is directly responsible for instantiating the StripePayment class.

class StripePayment {
    public function charge(float $amount) {
        // Charge logic
    }
}

class OrderService {
    private $payment;

    public function __construct() {
        // Tightly coupled dependency creation
        $this->payment = new StripePayment();
    }

    public function checkout(float $total) {
        $this->payment->charge($total);
    }
}

If you ever want to switch from Stripe to PayPal, you have to modify the internal code of the OrderService class. This violates the Open-Closed Principle of SOLID design.

Step 2: The Decoupled Approach (With Constructor Injection)

To implement constructor injection, you should first define an interface. This allows you to type-hint the interface in the constructor rather than a specific class implementation.

interface PaymentProcessor {
    public function charge(float $amount);
}

class StripeProcessor implements PaymentProcessor {
    public function charge(float $amount) {
        // Stripe charging logic
    }
}

class PaypalProcessor implements PaymentProcessor {
    public function charge(float $amount) {
        // PayPal charging logic
    }
}

Now, inject the PaymentProcessor interface into the OrderService constructor:

class OrderService {
    private PaymentProcessor $paymentProcessor;

    // The dependency is injected here
    public function __construct(PaymentProcessor $paymentProcessor) {
        $this->paymentProcessor = $paymentProcessor;
    }

    public function checkout(float $total) {
        $this->paymentProcessor->charge($total);
    }
}

Step 3: Instantiating the Classes

When you use the code, you create the dependency first and then pass it to the constructor of the dependent class:

// Using Stripe
$stripe = new StripeProcessor();
$orderService = new OrderService($stripe);
$orderService->checkout(100.00);

// Using PayPal instead requires zero changes to OrderService
$paypal = new PaypalProcessor();
$orderService = new OrderService($paypal);
$orderService->checkout(150.00);

In modern PHP applications (such as those built with Laravel, Symfony, or PHP-DI), you rarely instantiate these classes manually. Instead, a Dependency Injection Container automatically resolves and injects these dependencies for you based on your constructor type-hints.

Benefits of Constructor Injection in PHP