What is Dependency Injection in PHP?
Dependency Injection (DI) is a software design pattern that helps you write more modular, testable, and maintainable code. In the PHP ecosystem, DI has become a cornerstone of modern frameworks such as Laravel, Symfony, and Slim. This article explains the core concepts, why DI matters, and how to implement it effectively in PHP projects.
Why Use Dependency Injection?
Before diving into the implementation, it’s useful to understand the problems DI solves:
1. Reduces Tight Coupling
When a class creates its own dependencies (e.g., new PDO() inside a repository), it becomes tightly coupled to a specific implementation. DI removes that direct dependency, allowing you to swap implementations without touching the consumer class.
2. Improves Testability
Injectable dependencies can be replaced with mocks or stubs during unit testing, making it possible to test a class in isolation.
3. Enhances Reusability
Because classes depend on abstractions (interfaces) rather than concrete classes, they can be reused across different contexts.
4. Centralises Configuration
DI containers let you define how objects are wired in a single place, simplifying configuration and reducing duplication.
Core Concepts of Dependency Injection
DI revolves around three main components:
1. Service (Dependency)
A class that provides a specific piece of functionality (e.g., a logger, database connection, or mailer).
2. Consumer (Client)
The class that needs the service to perform its work.
3. Injector (Container)
The mechanism that creates the service and injects it into the consumer. This can be a simple factory, a manual constructor call, or a full‑featured DI container.
Ways to Perform Dependency Injection in PHP
1. Constructor Injection
The most common method. Dependencies are passed through the class constructor.
interface LoggerInterface {
public function log(string $message): void;
}
class FileLogger implements LoggerInterface {
public function log(string $message): void {
// Write to a file...
}
}
class UserService {
private LoggerInterface $logger;
public function __construct(LoggerInterface $logger) {
$this->logger = $logger;
}
public function register(string $username, string $email): void {
// Business logic...
$this->logger->log("User {$username} registered.");
}
}
// Manual wiring
$logger = new FileLogger();
$userService = new UserService($logger);
2. Setter (Property) Injection
Dependencies are provided via a setter method or public property after object creation.
class UserService {
private ?LoggerInterface $logger = null;
public function setLogger(LoggerInterface $logger): void {
$this->logger = $logger;
}
public function register(string $username, string $email): void {
// Business logic...
$this->logger?->log("User {$username} registered.");
}
}
// Wiring
$userService = new UserService();
$userService->setLogger(new FileLogger());
3. Interface Injection
Less common in PHP, this pattern requires the consumer to implement an interface that receives the dependency.
interface LoggerAwareInterface {
public function setLogger(LoggerInterface $logger): void;
}
class UserService implements LoggerAwareInterface {
private LoggerInterface $logger;
public function setLogger(LoggerInterface $logger): void {
$this->logger = $logger;
}
// ...
}
Using a DI Container
Frameworks often provide a container that automates wiring. Below is a minimal example using the popular PHP‑DI library.
use DI\ContainerBuilder;
require 'vendor/autoload.php';
$containerBuilder = new ContainerBuilder();
$containerBuilder->addDefinitions([
LoggerInterface::class => \DI\create(FileLogger::class),
UserService::class => \DI\create()
->constructor(\DI\get(LoggerInterface::class)),
]);
$container = $containerBuilder->build();
/** @var UserService $userService */
$userService = $container->get(UserService::class);
$userService->register('alice', 'alice@example.com');
Best Practices for Dependency Injection in PHP
- Depend on abstractions, not concrete classes. Use interfaces to keep the contract stable.
- Prefer constructor injection. It makes required dependencies explicit and encourages immutability.
- Avoid service location. Pulling services from a container inside the class defeats the purpose of DI.
- Keep the container configuration simple. Over‑engineering the container can lead to hard‑to‑debug code.
- Document the purpose of each dependency. Use PHPDoc or type hints for clarity.
- Leverage autowiring when possible. Modern containers can resolve many dependencies automatically, reducing boilerplate.
Common Pitfalls
Even seasoned developers can stumble on the following:
- Too many constructor arguments. If a class needs more than 4‑5 dependencies, consider refactoring it into smaller services.
- Circular dependencies. They cause runtime errors. Break the cycle by introducing an intermediate service or using setter injection.
- Mixing DI with static calls. Static methods bypass the container, making testing difficult.
Conclusion
Dependency Injection is a powerful pattern that brings flexibility, testability, and cleaner architecture to PHP applications. Whether you choose manual wiring, a lightweight factory, or a full‑featured DI container, the core principle remains the same: inject, don’t create. By embracing DI, you’ll write code that scales gracefully, adapts to change, and stands the test of time.