Understanding Dependency Injection with Examples in PHP

Dependency Injection (DI) is a design pattern to inject one class into another class. It can be categorized into three main types:

Table of Contents

  1. Constructor Injection
  2. Setter/Method Injection
  3. Interface Injection
  4. Advantages
  5. Disadvantages

Constructor Injection

Suppose, we have a class UserService that requires a Logger dependency. Instead of creating the Logger instance inside the UserService class, we inject it from outside.

Logger.php
// Define the Logger class
class Logger {
    public function log($message) {
        echo "Logging: $message\n";
    }
}
UserService.php
// Define the UserService class with dependency injection
class UserService {
    private $logger;

    public function __construct(Logger $logger) {
        $this->logger = $logger;
    }

    public function createUser($username) {
        // Logic to create a user
        $this->logger->log("User '$username' created.");
    }
}

Here, the UserService class has a constructor that accepts an instance of the Logger class as a parameter. This is the essence of Dependency Injection. The UserService class relies on an external Logger object to perform its logging functionality.

Using the classes:

// Creating instances and injecting dependencies
$logger = new Logger();
$userService = new UserService($logger);

$userService->createUser("john_doe");

Here, we first create an instance of Logger, and then we pass that instance to the constructor of UserService. This way, the UserService class doesn't create its own Logger instance, but rather uses the one provided from outside.

Setter/Method Injection

We can also achieve Dependency Injection using setter methods or method injection. Suppose, we have a NotificationService class that depends on a Logger class for logging messages:

Define the Logger class:

Logger.php
class Logger {
    public function log($message) {
        echo "Logging: $message\n";
    }
}

Define the NotificationService class with method injection:

NotificationService.php
class NotificationService {
    private $logger;
    
    public function setLogger(Logger $logger) {
        $this->logger = $logger;
    }
    
    public function sendNotification($message) {
        // Logic to send notification
        
        $this->logger->log("Notification sent: $message");
    }
}

In this example, instead of injecting the dependency through the constructor, we're using a setter method setLogger() to inject the Logger dependency into the NotificationService class.

Using the classes:

$logger = new Logger();
$notificationService = new NotificationService();

$notificationService->setLogger($logger);
$notificationService->sendNotification("Hello, world!");

In this usage example, we create instances of both Logger and NotificationService, and then we use the setLogger() method to inject the Logger dependency into the NotificationService instance.

Interface Injection

Let's take an example of using Dependency Injection with an interface in PHP. This approach is particularly useful when you want to inject dependencies that adhere to a certain contract (interface).

Suppose we have an interface called LoggerInterface that defines the contract for logging:

LoggerInterface.php
interface LoggerInterface {
    public function log($message);
}

Now, we'll create two classes that implement this interface: FileLogger and DatabaseLogger. Define the FileLogger class:

FileLogger.php
class FileLogger implements LoggerInterface {
    public function log($message) {
        echo "Logging to file: $message\n";
    }
}

Define the DatabaseLogger class:

DatabaseLogger.php
class DatabaseLogger implements LoggerInterface {
    public function log($message) {
        echo "Logging to database: $message\n";
    }
}

Use the Dependency Injection with the interface:

NotificationService.php
class NotificationService {
    private $logger;
    
    public function __construct(LoggerInterface $logger) {
        $this->logger = $logger;
    }
    
    public function sendNotification($message) {
        // Logic to send notification
        
        $this->logger->log("Notification sent: $message");
    }
}

In this example, the NotificationService class depends on the LoggerInterface interface rather than a concrete implementation. This allows you to inject any class that implements LoggerInterface.

Using the classes:

$fileLogger = new FileLogger();
$notificationService = new NotificationService($fileLogger);
$notificationService->sendNotification("Hello from File Logger!");

$databaseLogger = new DatabaseLogger();
$notificationService = new NotificationService($databaseLogger);
$notificationService->sendNotification("Hello from Database Logger!");

Advantages

Dependency injection (DI) offers several advantages in software development:

Modularity and Separation of Concerns: Dependency injection promotes a modular design where components are loosely coupled and have well-defined responsibilities. This separation of concerns makes it easier to manage and maintain the codebase.

Testability: By injecting dependencies rather than creating them within a class, it becomes much easier to write unit tests. Mocking or substituting dependencies for testing purposes is straightforward, enabling more comprehensive and isolated testing.

Reusability: Dependencies can be reused across different parts of the application, promoting a more efficient and consistent use of resources. This reduces code duplication and enhances the overall maintainability of the codebase.

Ease of Maintenance: Since dependencies are managed externally, making changes to a dependency's implementation doesn't require modifications to every class that uses it. This reduces the chances of introducing bugs when updating dependencies.

Clearer Code: With dependency injection, the dependencies a class relies on are explicitly defined in the constructor or method parameters. This makes it easier to understand a class's requirements without digging into its internals.

Disadvantages

One of the main disadvantages of dependency injection is:

Complexity: Implementing dependency injection can introduce complexity to your codebase, especially in larger projects. Components need to be properly configured and injected, which can lead to increased initialization code and potentially make the code harder to understand, especially for developers who are new to the project or the concept of dependency injection.


Software Engineer | Ethical Hacker & Cybersecurity...

Md Obydullah is a software engineer and full stack developer specialist at Laravel, Django, Vue.js, Node.js, Android, Linux Server, and Ethichal Hacking.