1. php
  2. /advanced
  3. /dependency-injection

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

  1. Loose Coupling: Classes depend on abstractions, not concrete implementations
  2. Testability: Easy to mock dependencies for unit testing
  3. Flexibility: Change implementations without modifying dependent code
  4. Single Responsibility: Classes focus on their core responsibility
  5. 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');
    }
}
?>

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.