PHP Dependency Injection — Complete Guide to DI Container and Loose Coupling
In this tutorial, you will learn about PHP Dependency Injection. We cover key concepts, practical examples, and best practices to help you master this topic.
Dependency injection in PHP passes dependencies into a class rather than creating them internally, promoting loose coupling, testability, and maintainable object-oriented code.
What You'll Learn
By the end of this tutorial, you'll implement constructor injection, setter injection, use auto-wiring with PHP-DI, configure service containers, and write testable code with mocked dependencies.
Why DI Matters
When a class creates its own dependencies (e.g., new Database()), it's tightly coupled to that implementation. DI lets you swap implementations without changing the class, making testing and maintenance easier.
Real-World Use
A UserController receives a UserRepositoryInterface in its constructor. In production, a MySQLUserRepository is injected. In tests, a MockUserRepository is injected. The controller never knows the difference.
DI Learning Path
flowchart LR
A[Composer] --> B[DI]
B --> C[MVC]
C --> D[REST API]
D --> E[JWT Auth]
A --> F{You Are Here}
style F fill:#f90,color:#fff
Without DI (Tight Coupling)
<?php
class UserController {
private Database $db;
public function __construct() {
$this->db = new Database("localhost", "root", "", "mydb");
}
public function show(int $id): array {
return $this->db->query("SELECT * FROM users WHERE id = ?", [$id]);
}
}
// Hard to test — always connects to real database
Constructor Injection
<?php
interface UserRepositoryInterface {
public function find(int $id): ?array;
}
class MySQLUserRepository implements UserRepositoryInterface {
public function find(int $id): ?array {
return ["id" => $id, "name" => "Alice"];
}
}
class UserController {
public function __construct(
private UserRepositoryInterface $repository
) {}
public function show(int $id): ?array {
return $this->repository->find($id);
}
}
// Usage
$controller = new UserController(new MySQLUserRepository());
Setter Injection
<?php
class MailerService {
private ?LoggerInterface $logger = null;
public function setLogger(LoggerInterface $logger): void {
$this->logger = $logger;
}
public function send(string $to, string $subject): void {
$this->logger?->info("Sending email to $to");
// Send email...
}
}
Autoloading with PHP-DI
composer require php-di/php-di
<?php
require "vendor/autoload.php";
use function DI\create;
use function DI\get;
$container = new DI\Container();
$container->set(UserRepositoryInterface::class, DI\autowire(MySQLUserRepository::class));
$controller = $container->get(UserController::class);
$user = $controller->show(1);
Laravel Service Container
<?php
// Binding interfaces
app()->bind(UserRepositoryInterface::class, MySQLUserRepository::class);
// Singleton
app()->singleton(Logger::class, fn() => new Logger("app"));
// Resolving
$repo = app(UserRepositoryInterface::class);
$controller = app(UserController::class);
Common Mistakes
1. Using new Inside Classes
Creating dependencies with new inside a class couples it to concrete implementations. Inject dependencies instead.
2. Service Locator Anti-Pattern
Calling a static service locator from within a class hides dependencies. Explicit injection is better.
3. Container as Service Locator
Passing the entire container to a class makes dependencies invisible. Only inject what's needed.
4. Too Many Constructor Parameters
More than 3-4 constructor parameters suggests the class has too many responsibilities. Split it up.
5. Not Using Interfaces
Depend on interfaces, not concrete classes. This allows swapping implementations (real vs mock).
Practice Questions
1. What is dependency injection?
Passing dependencies into a class from outside rather than creating them inside. This decouples the class from its dependencies.
2. What is the difference between constructor and setter injection?
Constructor injection provides dependencies at creation time (required dependencies). Setter injection adds optional dependencies after creation.
3. What is a DI container?
A container that automatically resolves and injects dependencies based on type hints, reducing manual wiring.
4. How does DI improve testability?
Tests can inject mock implementations of interfaces, isolating the class under test from real infrastructure.
5. Challenge: Refactor a tightly coupled class to use dependency injection.
<?php
// Before: Tight coupling
class OrderService {
public function process(Order $order): void {
$db = new Database();
$mailer = new Mailer();
$db->save($order);
$mailer->send($order->getEmail(), "Order confirmed");
}
}
// After: Dependency injection
class OrderService {
public function __construct(
private OrderRepositoryInterface $db,
private MailerInterface $mailer
) {}
public function process(Order $order): void {
$this->db->save($order);
$this->mailer->send($order->getEmail(), "Order confirmed");
}
}
FAQ
Mini Project: DI Container Setup
Create a simple DI container with auto-wiring support.
<?php
class SimpleContainer {
private array $bindings = [];
public function set(string $abstract, callable $factory): void {
$this->bindings[$abstract] = $factory;
}
public function get(string $class): object {
if (isset($this->bindings[$class])) {
return ($this->bindings[$class])($this);
}
return $this->autowire($class);
}
private function autowire(string $class): object {
$reflection = new ReflectionClass($class);
$constructor = $reflection->getConstructor();
if (!$constructor) return $reflection->newInstance();
$params = array_map(fn($p) => $this->get($p->getType()->getName()), $constructor->getParameters());
return $reflection->newInstanceArgs($params);
}
}
// Usage
$container = new SimpleContainer();
$repo = new MySQLUserRepository();
$container->set(UserRepositoryInterface::class, fn() => $repo);
$controller = $container->get(UserController::class);
What's Next
PHP MVC Pattern PHP REST API PHP PDO
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro