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
- Improved Testability: You can easily pass mock
objects into the constructor during unit testing. This allows you to
test
OrderServicewithout executing actual payment API calls. - Loose Coupling: The
OrderServiceclass no longer cares how the payment is processed; it only requires that the injected object adheres to thePaymentProcessorcontract. - Explicit Dependencies: By looking at the constructor signatures, developers can immediately see what dependencies a class requires to function properly.