Dependency Injection in PHP
Introduction to Dependency Injection
Dependency Injection (DI) is a design pattern that implements Inversion of Control (IoC) for resolving dependencies. Instead of creating dependencies directly within a class, dependencies are provided (injected) from external sources. This promotes loose coupling, testability, and maintainability in your applications.
Benefits of Dependency Injection
- Loose Coupling: Classes depend on abstractions, not concrete implementations
- Testability: Easy to mock dependencies for unit testing
- Flexibility: Change implementations without modifying dependent code
- Single Responsibility: Classes focus on their core responsibility
- Configuration Management: Centralized dependency configuration
Types of Dependency Injection
Constructor Injection
Dependencies are provided through the constructor, ensuring they're available when the object is created.
<?php
interface LoggerInterface
{
public function log(string $message): void;
}
class FileLogger implements LoggerInterface
{
private string $logFile;
public function __construct(string $logFile)
{
$this->logFile = $logFile;
}
public function log(string $message): void
{
file_put_contents($this->logFile, date('Y-m-d H:i:s') . " - {$message}\n", FILE_APPEND);
}
}
class DatabaseLogger implements LoggerInterface
{
private PDO $pdo;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
public function log(string $message): void
{
$stmt = $this->pdo->prepare("INSERT INTO logs (message, created_at) VALUES (?, NOW())");
$stmt->execute([$message]);
}
}
class UserService
{
private LoggerInterface $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function createUser(string $name, string $email): void
{
// Create user logic
$this->logger->log("User created: {$name} ({$email})");
}
}
// Usage
$logger = new FileLogger('/var/log/app.log');
$userService = new UserService($logger);
$userService->createUser('John Doe', '[email protected]');
?>
Setter Injection
Dependencies are provided through setter methods, allowing optional dependencies.
<?php
class EmailService
{
private LoggerInterface $logger;
private CacheInterface $cache;
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
public function setCache(CacheInterface $cache): void
{
$this->cache = $cache;
}
public function sendEmail(string $to, string $subject, string $body): bool
{
// Check cache first
if (isset($this->cache)) {
$cacheKey = md5($to . $subject . $body);
if ($this->cache->has($cacheKey)) {
return true; // Already sent
}
}
// Send email logic
$result = mail($to, $subject, $body);
// Log if logger is available
if (isset($this->logger)) {
$status = $result ? 'sent' : 'failed';
$this->logger->log("Email {$status} to {$to}: {$subject}");
}
// Cache result
if (isset($this->cache) && $result) {
$this->cache->set($cacheKey, true, 3600);
}
return $result;
}
}
?>
Interface Injection
Dependencies are injected through interfaces that define injection methods.
<?php
interface LoggerAwareInterface
{
public function setLogger(LoggerInterface $logger): void;
}
class PaymentProcessor implements LoggerAwareInterface
{
private LoggerInterface $logger;
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
public function processPayment(float $amount): bool
{
// Payment processing logic
$this->logger->log("Processing payment of \${$amount}");
// Simulate payment processing
$success = rand(0, 1) === 1;
$status = $success ? 'successful' : 'failed';
$this->logger->log("Payment {$status}");
return $success;
}
}
?>
Simple Dependency Injection Container
<?php
class Container
{
private array $bindings = [];
private array $instances = [];
public function bind(string $abstract, $concrete = null): void
{
if ($concrete === null) {
$concrete = $abstract;
}
$this->bindings[$abstract] = $concrete;
}
public function singleton(string $abstract, $concrete = null): void
{
$this->bind($abstract, $concrete);
$this->instances[$abstract] = null;
}
public function instance(string $abstract, $instance): void
{
$this->instances[$abstract] = $instance;
}
public function get(string $abstract)
{
// Return singleton instance if it exists
if (array_key_exists($abstract, $this->instances) && $this->instances[$abstract] !== null) {
return $this->instances[$abstract];
}
// Get concrete implementation
$concrete = $this->bindings[$abstract] ?? $abstract;
// Build the object
$object = $this->build($concrete);
// Store singleton instance
if (array_key_exists($abstract, $this->instances)) {
$this->instances[$abstract] = $object;
}
return $object;
}
private function build($concrete)
{
// If concrete is a closure, execute it
if ($concrete instanceof Closure) {
return $concrete($this);
}
// If concrete is a string, assume it's a class name
if (is_string($concrete)) {
return $this->buildClass($concrete);
}
return $concrete;
}
private function buildClass(string $className)
{
$reflectionClass = new ReflectionClass($className);
// Check if class is instantiable
if (!$reflectionClass->isInstantiable()) {
throw new Exception("Class {$className} is not instantiable");
}
$constructor = $reflectionClass->getConstructor();
// If no constructor, create instance without dependencies
if ($constructor === null) {
return new $className;
}
// Resolve constructor dependencies
$dependencies = $this->resolveDependencies($constructor->getParameters());
return $reflectionClass->newInstanceArgs($dependencies);
}
private function resolveDependencies(array $parameters): array
{
$dependencies = [];
foreach ($parameters as $parameter) {
$type = $parameter->getType();
if ($type === null) {
// No type hint, check for default value
if ($parameter->isDefaultValueAvailable()) {
$dependencies[] = $parameter->getDefaultValue();
} else {
throw new Exception("Cannot resolve parameter {$parameter->getName()}");
}
} elseif ($type instanceof ReflectionNamedType && !$type->isBuiltin()) {
// Type hint is a class or interface
$dependencies[] = $this->get($type->getName());
} elseif ($parameter->isDefaultValueAvailable()) {
$dependencies[] = $parameter->getDefaultValue();
} else {
throw new Exception("Cannot resolve parameter {$parameter->getName()}");
}
}
return $dependencies;
}
public function has(string $abstract): bool
{
return isset($this->bindings[$abstract]) || isset($this->instances[$abstract]);
}
}
// Usage
$container = new Container();
// Bind interfaces to implementations
$container->bind(LoggerInterface::class, FileLogger::class);
$container->bind(CacheInterface::class, RedisCache::class);
// Bind with closure for complex initialization
$container->bind(PDO::class, function() {
return new PDO('mysql:host=localhost;dbname=myapp', 'user', 'password');
});
// Register as singleton
$container->singleton(ConfigService::class);
// Get service from container
$userService = $container->get(UserService::class);
?>
Advanced Container Features
Service Providers
Service providers organize related bindings and provide a way to register services.
<?php
abstract class ServiceProvider
{
protected Container $container;
public function __construct(Container $container)
{
$this->container = $container;
}
abstract public function register(): void;
public function boot(): void
{
// Override in subclasses if needed
}
}
class DatabaseServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->container->singleton(PDO::class, function() {
$config = $this->container->get(ConfigService::class);
return new PDO(
$config->get('database.dsn'),
$config->get('database.username'),
$config->get('database.password'),
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
});
$this->container->bind(UserRepository::class, function(Container $container) {
return new UserRepository($container->get(PDO::class));
});
}
}
class LoggingServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->container->singleton(LoggerInterface::class, function(Container $container) {
$config = $container->get(ConfigService::class);
$logType = $config->get('logging.type', 'file');
switch ($logType) {
case 'database':
return new DatabaseLogger($container->get(PDO::class));
case 'file':
default:
return new FileLogger($config->get('logging.file', 'app.log'));
}
});
}
}
// Application class to manage container and providers
class Application
{
private Container $container;
private array $providers = [];
public function __construct()
{
$this->container = new Container();
$this->container->instance(Container::class, $this->container);
$this->container->instance(Application::class, $this);
}
public function register(ServiceProvider $provider): void
{
$this->providers[] = $provider;
$provider->register();
}
public function boot(): void
{
foreach ($this->providers as $provider) {
$provider->boot();
}
}
public function get(string $abstract)
{
return $this->container->get($abstract);
}
}
// Usage
$app = new Application();
$app->register(new DatabaseServiceProvider($app->container));
$app->register(new LoggingServiceProvider($app->container));
$app->boot();
$userService = $app->get(UserService::class);
?>
Auto-wiring
Automatically resolve dependencies based on type hints.
<?php
class AutoWiringContainer extends Container
{
public function autowire(string $className)
{
$reflectionClass = new ReflectionClass($className);
if (!$reflectionClass->isInstantiable()) {
throw new Exception("Class {$className} is not instantiable");
}
$constructor = $reflectionClass->getConstructor();
if ($constructor === null) {
return new $className;
}
$dependencies = [];
foreach ($constructor->getParameters() as $parameter) {
$type = $parameter->getType();
if ($type && $type instanceof ReflectionNamedType && !$type->isBuiltin()) {
$typeName = $type->getName();
// Try to get from container first
if ($this->has($typeName)) {
$dependencies[] = $this->get($typeName);
} else {
// Auto-wire the dependency
$dependencies[] = $this->autowire($typeName);
}
} elseif ($parameter->isDefaultValueAvailable()) {
$dependencies[] = $parameter->getDefaultValue();
} else {
throw new Exception("Cannot auto-wire parameter {$parameter->getName()} in {$className}");
}
}
return $reflectionClass->newInstanceArgs($dependencies);
}
public function get(string $abstract)
{
try {
return parent::get($abstract);
} catch (Exception $e) {
// If not bound, try auto-wiring
if (class_exists($abstract)) {
return $this->autowire($abstract);
}
throw $e;
}
}
}
?>
Practical Examples
Repository Pattern with DI
<?php
interface UserRepositoryInterface
{
public function find(int $id): ?User;
public function save(User $user): void;
public function findByEmail(string $email): ?User;
}
class DatabaseUserRepository implements UserRepositoryInterface
{
private PDO $pdo;
private LoggerInterface $logger;
public function __construct(PDO $pdo, LoggerInterface $logger)
{
$this->pdo = $pdo;
$this->logger = $logger;
}
public function find(int $id): ?User
{
$stmt = $this->pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$id]);
$userData = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$userData) {
return null;
}
return new User($userData['id'], $userData['name'], $userData['email']);
}
public function save(User $user): void
{
if ($user->getId()) {
$this->update($user);
} else {
$this->insert($user);
}
$this->logger->log("User saved: {$user->getEmail()}");
}
public function findByEmail(string $email): ?User
{
$stmt = $this->pdo->prepare("SELECT * FROM users WHERE email = ?");
$stmt->execute([$email]);
$userData = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$userData) {
return null;
}
return new User($userData['id'], $userData['name'], $userData['email']);
}
private function insert(User $user): void
{
$stmt = $this->pdo->prepare("INSERT INTO users (name, email) VALUES (?, ?)");
$stmt->execute([$user->getName(), $user->getEmail()]);
$user->setId($this->pdo->lastInsertId());
}
private function update(User $user): void
{
$stmt = $this->pdo->prepare("UPDATE users SET name = ?, email = ? WHERE id = ?");
$stmt->execute([$user->getName(), $user->getEmail(), $user->getId()]);
}
}
class UserService
{
private UserRepositoryInterface $userRepository;
private EmailServiceInterface $emailService;
private EventDispatcherInterface $eventDispatcher;
public function __construct(
UserRepositoryInterface $userRepository,
EmailServiceInterface $emailService,
EventDispatcherInterface $eventDispatcher
) {
$this->userRepository = $userRepository;
$this->emailService = $emailService;
$this->eventDispatcher = $eventDispatcher;
}
public function registerUser(string $name, string $email, string $password): User
{
// Check if user already exists
if ($this->userRepository->findByEmail($email)) {
throw new Exception("User with email {$email} already exists");
}
// Create new user
$user = new User(null, $name, $email);
$user->setPassword(password_hash($password, PASSWORD_DEFAULT));
// Save user
$this->userRepository->save($user);
// Send welcome email
$this->emailService->sendWelcomeEmail($user);
// Dispatch event
$this->eventDispatcher->dispatch(new UserRegisteredEvent($user));
return $user;
}
}
?>
Service Layer with Multiple Dependencies
<?php
class OrderService
{
private UserRepositoryInterface $userRepository;
private ProductRepositoryInterface $productRepository;
private PaymentServiceInterface $paymentService;
private InventoryServiceInterface $inventoryService;
private LoggerInterface $logger;
private EventDispatcherInterface $eventDispatcher;
public function __construct(
UserRepositoryInterface $userRepository,
ProductRepositoryInterface $productRepository,
PaymentServiceInterface $paymentService,
InventoryServiceInterface $inventoryService,
LoggerInterface $logger,
EventDispatcherInterface $eventDispatcher
) {
$this->userRepository = $userRepository;
$this->productRepository = $productRepository;
$this->paymentService = $paymentService;
$this->inventoryService = $inventoryService;
$this->logger = $logger;
$this->eventDispatcher = $eventDispatcher;
}
public function processOrder(int $userId, array $items, PaymentDetails $paymentDetails): Order
{
$this->logger->log("Processing order for user {$userId}");
// Validate user
$user = $this->userRepository->find($userId);
if (!$user) {
throw new Exception("User not found");
}
// Validate and reserve inventory
$orderItems = [];
$totalAmount = 0;
foreach ($items as $item) {
$product = $this->productRepository->find($item['product_id']);
if (!$product) {
throw new Exception("Product {$item['product_id']} not found");
}
if (!$this->inventoryService->reserve($product, $item['quantity'])) {
throw new Exception("Insufficient inventory for product {$product->getName()}");
}
$orderItem = new OrderItem($product, $item['quantity'], $product->getPrice());
$orderItems[] = $orderItem;
$totalAmount += $orderItem->getTotal();
}
// Process payment
try {
$paymentResult = $this->paymentService->charge($paymentDetails, $totalAmount);
} catch (PaymentException $e) {
// Release reserved inventory
foreach ($orderItems as $orderItem) {
$this->inventoryService->release($orderItem->getProduct(), $orderItem->getQuantity());
}
throw new Exception("Payment failed: " . $e->getMessage());
}
// Create order
$order = new Order($user, $orderItems, $paymentResult);
// Dispatch events
$this->eventDispatcher->dispatch(new OrderCreatedEvent($order));
$this->logger->log("Order {$order->getId()} processed successfully");
return $order;
}
}
?>
Testing with Dependency Injection
DI makes testing much easier by allowing you to inject mock dependencies:
<?php
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\MockObject\MockObject;
class UserServiceTest extends TestCase
{
private UserService $userService;
private MockObject $userRepository;
private MockObject $emailService;
private MockObject $eventDispatcher;
protected function setUp(): void
{
$this->userRepository = $this->createMock(UserRepositoryInterface::class);
$this->emailService = $this->createMock(EmailServiceInterface::class);
$this->eventDispatcher = $this->createMock(EventDispatcherInterface::class);
$this->userService = new UserService(
$this->userRepository,
$this->emailService,
$this->eventDispatcher
);
}
public function testRegisterUser(): void
{
// Arrange
$name = 'John Doe';
$email = '[email protected]';
$password = 'password123';
$this->userRepository
->expects($this->once())
->method('findByEmail')
->with($email)
->willReturn(null);
$this->userRepository
->expects($this->once())
->method('save')
->with($this->isInstanceOf(User::class));
$this->emailService
->expects($this->once())
->method('sendWelcomeEmail')
->with($this->isInstanceOf(User::class));
$this->eventDispatcher
->expects($this->once())
->method('dispatch')
->with($this->isInstanceOf(UserRegisteredEvent::class));
// Act
$user = $this->userService->registerUser($name, $email, $password);
// Assert
$this->assertInstanceOf(User::class, $user);
$this->assertEquals($name, $user->getName());
$this->assertEquals($email, $user->getEmail());
}
public function testRegisterUserWithExistingEmail(): void
{
// Arrange
$email = '[email protected]';
$existingUser = new User(1, 'Existing User', $email);
$this->userRepository
->expects($this->once())
->method('findByEmail')
->with($email)
->willReturn($existingUser);
// Act & Assert
$this->expectException(Exception::class);
$this->expectExceptionMessage("User with email {$email} already exists");
$this->userService->registerUser('New User', $email, 'password');
}
}
?>
Popular DI Containers
PSR-11 Container Interface
<?php
use Psr\Container\ContainerInterface;
class PSR11Container implements ContainerInterface
{
private array $bindings = [];
private array $instances = [];
public function get(string $id)
{
if (!$this->has($id)) {
throw new NotFoundException("Service {$id} not found");
}
// Return singleton if exists
if (isset($this->instances[$id])) {
return $this->instances[$id];
}
$concrete = $this->bindings[$id];
if ($concrete instanceof Closure) {
$instance = $concrete($this);
} else {
$instance = $this->resolve($concrete);
}
// Store as singleton if configured
if (isset($this->instances[$id])) {
$this->instances[$id] = $instance;
}
return $instance;
}
public function has(string $id): bool
{
return isset($this->bindings[$id]);
}
public function bind(string $id, $concrete): void
{
$this->bindings[$id] = $concrete;
}
public function singleton(string $id, $concrete): void
{
$this->bind($id, $concrete);
$this->instances[$id] = null;
}
private function resolve($concrete)
{
if (is_string($concrete) && class_exists($concrete)) {
return $this->buildClass($concrete);
}
return $concrete;
}
private function buildClass(string $className)
{
$reflectionClass = new ReflectionClass($className);
$constructor = $reflectionClass->getConstructor();
if ($constructor === null) {
return new $className;
}
$dependencies = [];
foreach ($constructor->getParameters() as $parameter) {
$type = $parameter->getType();
if ($type && $type instanceof ReflectionNamedType && !$type->isBuiltin()) {
$dependencies[] = $this->get($type->getName());
} elseif ($parameter->isDefaultValueAvailable()) {
$dependencies[] = $parameter->getDefaultValue();
} else {
throw new Exception("Cannot resolve parameter {$parameter->getName()}");
}
}
return $reflectionClass->newInstanceArgs($dependencies);
}
}
?>
Best Practices
1. Program to Interfaces
<?php
// Good - Depend on abstraction
class UserController
{
public function __construct(private UserServiceInterface $userService) {}
}
// Avoid - Depend on concrete implementation
class UserController
{
public function __construct(private UserService $userService) {}
}
?>
2. Use Constructor Injection for Required Dependencies
<?php
class OrderService
{
// Required dependencies in constructor
public function __construct(
private UserRepositoryInterface $userRepository,
private PaymentServiceInterface $paymentService
) {}
// Optional dependencies via setter
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
}
?>
3. Keep Dependencies Minimal
<?php
// Good - Focused dependencies
class EmailService
{
public function __construct(
private MailerInterface $mailer,
private TemplateEngineInterface $templateEngine
) {}
}
// Avoid - Too many dependencies (violates SRP)
class EmailService
{
public function __construct(
private MailerInterface $mailer,
private TemplateEngineInterface $templateEngine,
private DatabaseInterface $database,
private CacheInterface $cache,
private LoggerInterface $logger,
private ConfigInterface $config,
private EventDispatcherInterface $eventDispatcher
) {}
}
?>
4. Use Service Providers for Organization
<?php
class ServiceProvider
{
public static function register(Container $container): void
{
// Group related bindings
$container->bind(UserRepositoryInterface::class, DatabaseUserRepository::class);
$container->bind(UserServiceInterface::class, UserService::class);
$container->bind(UserControllerInterface::class, UserController::class);
}
}
?>
Common Anti-Patterns to Avoid
1. Service Locator Anti-Pattern
<?php
// Avoid - Service Locator hides dependencies
class UserService
{
public function createUser($userData)
{
$logger = ServiceLocator::get(LoggerInterface::class); // Hidden dependency
$repository = ServiceLocator::get(UserRepositoryInterface::class); // Hidden dependency
// Use services...
}
}
// Better - Explicit dependencies
class UserService
{
public function __construct(
private LoggerInterface $logger,
private UserRepositoryInterface $repository
) {}
public function createUser($userData)
{
// Use injected services...
}
}
?>
2. Circular Dependencies
<?php
// Avoid - Circular dependency
class A
{
public function __construct(B $b) {}
}
class B
{
public function __construct(A $a) {} // Circular dependency!
}
// Better - Break the cycle with an interface or event system
class A
{
public function __construct(BInterface $b) {}
}
class B implements BInterface
{
// Don't depend on A directly
// Use events or callbacks instead
}
?>
Dependency Injection is a powerful pattern that promotes clean, testable, and maintainable code. By properly implementing DI, you create flexible applications that are easy to test and extend.