Dependency Injection (DI) in PHP is a design pattern where you pass an object’s dependencies from the outside instead of creating them inside the class.
It’s used to make code more flexible, testable, and maintainable.
1. Without Dependency Injection (Tightly Coupled)
class Logger {
public function log($message) {
echo "Log: $message";
}
}
class UserService {
private $logger;
public function __construct() {
// Creating dependency inside (BAD)
$this->logger = new Logger();
}
public function register($username) {
$this->logger->log("User $username registered.");
}
}
$userService = new UserService();
$userService->register("Himanshu");
Problem:
UserServiceis tightly coupled toLogger.- Hard to change logger type or mock it in tests.
2. Constructor Injection (Most Common)
class Logger {
public function log($message) {
echo "Log: $message";
}
}
class UserService {
private $logger;
// Inject dependency through constructor
public function __construct(Logger $logger) {
$this->logger = $logger;
}
public function register($username) {
$this->logger->log("User $username registered.");
}
}
// Create dependency outside and inject it
$logger = new Logger();
$userService = new UserService($logger);
$userService->register("Himanshu");
✅ Advantages:
- No hardcoding of dependencies.
- Easy to swap
Loggerwith another logger (e.g.,FileLogger). - Easy unit testing with mock objects.
3. Setter Injection
class UserService {
private $logger;
public function setLogger(Logger $logger) {
$this->logger = $logger;
}
public function register($username) {
$this->logger->log("User $username registered.");
}
}
$logger = new Logger();
$userService = new UserService();
$userService->setLogger($logger);
$userService->register("Himanshu");
When to use:
- Dependency is optional or can be changed during object’s lifecycle.
4. Interface-Based Injection
interface LoggerInterface {
public function log($message);
}
class FileLogger implements LoggerInterface {
public function log($message) {
file_put_contents('log.txt', $message . PHP_EOL, FILE_APPEND);
}
}
class UserService {
private $logger;
public function __construct(LoggerInterface $logger) {
$this->logger = $logger;
}
public function register($username) {
$this->logger->log("User $username registered.");
}
}
// Swap loggers easily
$userService = new UserService(new FileLogger());
$userService->register("Himanshu");
✅ Best Practice: Use interfaces so classes depend on abstractions, not concrete implementations.
5. Using a Dependency Injection Container (Advanced)
Many frameworks (Laravel, Symfony) use Service Containers:
class Container {
private $services = [];
public function set($name, $service) {
$this->services[$name] = $service;
}
public function get($name) {
return $this->services[$name];
}
}
// Register dependencies
$container = new Container();
$container->set('logger', new Logger());
$container->set('userService', new UserService($container->get('logger')));
// Use them
$userService = $container->get('userService');
$userService->register("Himanshu");
💡 Key Points for Interviews:
- Dependency Injection decouples classes from specific implementations.
- Types:
- Constructor Injection (most common)
- Setter Injection
- Interface Injection
- Improves testability and flexibility.
- Often combined with IoC (Inversion of Control) containers in frameworks.
let’s take a real-world Laravel example so you can see how Dependency Injection is used in a modern PHP framework.
Example Scenario
We have a PaymentService that processes payments.
We might use PayPal, Stripe, or any other gateway — but our controller should not care which one.
1. Define an Interface
We create an interface for the payment service so our code depends on an abstraction.
namespace App\Contracts;
interface PaymentGatewayInterface
{
public function pay(float $amount): bool;
}
2. Create Implementations
namespace App\Services;
use App\Contracts\PaymentGatewayInterface;
class PayPalPaymentGateway implements PaymentGatewayInterface
{
public function pay(float $amount): bool
{
// PayPal API integration here
echo "Paid ₹{$amount} via PayPal";
return true;
}
}
class StripePaymentGateway implements PaymentGatewayInterface
{
public function pay(float $amount): bool
{
// Stripe API integration here
echo "Paid ₹{$amount} via Stripe";
return true;
}
}
3. Inject via Laravel Controller
namespace App\Http\Controllers;
use App\Contracts\PaymentGatewayInterface;
class PaymentController extends Controller
{
private $paymentGateway;
// Constructor Injection (Laravel resolves automatically)
public function __construct(PaymentGatewayInterface $paymentGateway)
{
$this->paymentGateway = $paymentGateway;
}
public function checkout()
{
$this->paymentGateway->pay(500);
}
}
Here, Laravel automatically injects the correct implementation based on bindings.
4. Bind Interface to Implementation in AppServiceProvider
In app/Providers/AppServiceProvider.php:
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Contracts\PaymentGatewayInterface;
use App\Services\PayPalPaymentGateway;
// use App\Services\StripePaymentGateway;
class AppServiceProvider extends ServiceProvider
{
public function register()
{
// Bind interface to PayPal implementation
$this->app->bind(PaymentGatewayInterface::class, PayPalPaymentGateway::class);
// To switch to Stripe, just change:
// $this->app->bind(PaymentGatewayInterface::class, StripePaymentGateway::class);
}
public function boot() {}
}
5. How it Works
- You never manually
newan object in the controller. - Laravel’s Service Container automatically resolves dependencies by reading type hints in the constructor.
- If tomorrow you want to change PayPal to Stripe, just update the binding — no changes in the controller.
✅ Benefits:
- No hardcoded dependencies.
- Easy to swap services.
- Perfect for unit testing (mock the interface).
- Cleaner, maintainable, and follows SOLID principles.
