Please wait

Dependency Injection

Dependency Injection (DI) is a design pattern used in PHP and other programming languages to achieve Inversion of Control. Instead of objects creating their dependencies, they receive them from an external source, such as a DI container. This makes the code more modular, testable, and maintainable, as dependencies can be easily swapped or mocked for testing purposes. By decoupling components, DI promotes more flexible and reusable code.

The Problem

To understand the usefulness of dependency injection, let's look at an example. Let's say we had a class for working with a payment gateway for processing payments.

class PaymentGateway {
  public function charge($amount) {
      // Code to charge the payment
  }
}

This class has one method called charge() for charging a payment. Next, let's imagine you have a class OrderProcessor that handles processing orders in an e-commerce application. This class relies on a specific PaymentGateway to handle payments.

class OrderProcessor {
  private $paymentGateway;
 
  public function __construct() {
    $this->paymentGateway = new PaymentGateway();
  }
 
  public function process($order) {
    $this->paymentGateway->charge($order->amount);
  }
}

There are a few problems arising from this approach.

  • Tight Coupling: The OrderProcessor is tightly coupled to the PaymentGateway class. If you want to use a different payment gateway or a mock gateway for testing, you would have to modify the OrderProcessor class itself.
  • Difficulty in Testing: If the PaymentGateway involves interaction with a real payment service, testing the OrderProcessor will become difficult since you can't easily replace the PaymentGateway with a mock object.
  • Lack of Flexibility: Any changes to the PaymentGateway (such as additional parameters in the constructor) would require changes in every place where it's instantiated, making the code harder to maintain.
  • Violation of the Single Responsibility Principle: The OrderProcessor class is not only responsible for processing orders but also for managing the creation of the PaymentGateway. This makes the class more complex and less focused on its primary purpose.

By not using dependency injection, the code becomes tightly interwoven, making it harder to test, maintain, and extend. If different parts of the application or tests need to use different implementations of the payment gateway, the current design does not support that without altering the OrderProcessor itself.

Using Dependency Injection

We can fix the issue by using dependency injection (DI), specifically constructor injection, to provide the PaymentGateway dependency to the OrderProcessor class. By doing so, we can inject different implementations of the payment gateway, which makes the code more flexible, maintainable, and testable.

The first step is to refactor the PaymentGateway class by implementing an interface. Interfaces are going to allow us to use different payment gateways. As long as a payment gateway implements this interface, it'll be acceptable by our OrderProcessor class.

interface PaymentGatewayInterface {
  public function charge($amount);
}

The PaymentGateway class implements the interface:

class PaymentGateway implements PaymentGatewayInterface {
  public function charge($amount) {
    // Code to charge the payment
  }
}

The OrderProcessor class accepts the PaymentGatewayInterface as a constructor parameter:

class OrderProcessor {
  public function __construct(private PaymentGatewayInterface $paymentGateway) {
  }
 
  public function process($order) {
    $this->paymentGateway->charge($order->amount);
  }
}

When creating an OrderProcessor instance, we can now inject any class that implements the PaymentGatewayInterface. This could be the real PaymentGateway or a mock implementation for testing:

$paymentGateway = new PaymentGateway();
$orderProcessor = new OrderProcessor($paymentGateway);

The OrderProcessor is now decoupled from the specific PaymentGateway implementation, allowing for easy substitution with other implementations. You can inject a mock payment gateway when testing the OrderProcessor, allowing for controlled and isolated testing. Different parts of the application can use different implementations of the payment gateway without altering the OrderProcessor. The OrderProcessor now focuses solely on processing orders, leaving the creation and management of the payment gateway elsewhere.

By using dependency injection, the code becomes more modular and adheres to best practices, making future development and testing more streamlined and effective.

Dependency Injection Containers

If instances shouldn't be created directly in classes, where should they be created? The most common solution is to use a dependency injection container.

Dependency Injection Containers, also known as DI Containers or Service Containers, are tools used to manage dependencies in a software application. They help to centralize the creation and management of objects and their dependencies, simplifying the process of injecting those dependencies into various parts of the application.

You can register classes and their dependencies within the container. This defines how instances of a class should be created and what dependencies they require. When you need an instance of a class, you ask the container for it. The container takes care of creating the instance and injecting all the necessary dependencies. The container can manage singletons or shared instances, ensuring that the same instance of a class is returned whenever it's requested.

In most cases, there are dozens of libraries online available for creating a dependency injection container. These are the most popular solutions:

  • Symfony DependencyInjection Component: Part of the Symfony framework, this component can be used on its own to manage dependencies. It offers features like auto-wiring, configuration, and compilation.
  • Laravel Service Container: Laravel's Service Container is a powerful and flexible DI container that comes with the Laravel framework. It supports auto-resolving, tagging, and contextual binding.
  • PHP-DI: PHP-DI is a popular stand-alone DI container that is compatible with many frameworks. It supports annotations, auto-wiring, and various other configurations.

Key Takeaways

  • Dependency Injection is a design pattern that allows objects to receive dependencies from an external source rather than creating them, enhancing flexibility and testability.
  • DI promotes loose coupling by allowing dependencies to be easily swapped or replaced, improving code maintainability.
  • DI can be performed manually or through DI containers, depending on the complexity and requirements of the application.
  • There are several popular DI containers in PHP, including Symfony's DependencyInjection, Laravel's Service Container, PHP-DI, and others.
  • Overall, dependency injection contributes to writing cleaner, more modular, and scalable code, aligning with modern software development practices.

Comments

Please read this before commenting