How to Implement Dependency Injection in PHP

Updated: January 11, 2024 By: Guest Contributor Post a comment

Introduction

Dependency Injection (DI) has become a staple in modern software development, due to its role in facilitating loose coupling, enhancing code maintainability, and simplifying unit testing. In PHP, DI is just as significant as in any other programming ecosystem, and this tutorial aims to guide you through understanding and implementing dependency injection in your PHP applications.

Before diving into implementation, it’s critical to understand what Dependency Injection is. DI is a design pattern that allows a class to receive its dependencies from an external source rather than creating them internally. This promotes a modular architecture and makes your code more testable and maintainable. PHP, being a dynamic language with object-oriented capabilities, offers several ways to implement DI, including constructor injection, setter injection, and interface-based injection. We will explore these methods and walk through practical examples.

Understanding the Basics of Dependency Injection

At its simplest, Dependency Injection involves 3 key components:

  • Client Class: The class that requires a dependency. This is the class into which the dependency will be injected.
  • Service: The dependency being used by the client class.
  • Injector: The mechanism that injects the service into the client class. This can be manual or managed by a DI container or service locator.

Types of Dependency Injection

There are three primary methods of Dependency Injection:

  1. Constructor Injection: Dependencies are provided through a class constructor.
  2. Setter Injection: Dependencies are provided through setter methods.
  3. Interface-based Injection: Dependencies are provided through an interface that the class implements.

Implementing Constructor Injection in PHP

Constructor injection is the most common form of DI. It involves passing the required dependencies into the class’s constructor.

<?php

class Logger {
    public function log($message) {
        // Log the message to a file or other medium
    }
}

class Application {
    protected $logger;
    
    public function __construct(Logger $logger) {
        $this->logger = $logger;
    }
    
    public function run() {
        // Application logic
        $this->logger->log('Application is running.');
    }
}

$logger = new Logger();
$app = new Application($logger);
$app->run();
?>

In the example above, the Application class requires an instance of Logger to function. We inject a Logger instance into Application through its constructor.

Implementing Setter Injection in PHP

Setter Injection involves providing the dependency through a dedicated setter method. This method is particularly useful when the dependency is optional, or when there might be a need to replace the dependency at a later stage.

<?php

class DatabaseConnection {
    // ...
}

class UserRepository {
    protected $dbConnection;
    
    public function setDatabaseConnection(DatabaseConnection $dbConnection) {
        $this->dbConnection = $dbConnection;
    }
    
    // Repository methods
}

$userRepo = new UserRepository();
$userRepo->setDatabaseConnection(new DatabaseConnection());

//...
?>

Here, the UserRepository class does not require a DatabaseConnection instance to be provided upon instantiation. Instead, it exposes a setter method that can be used to inject the dependency at any point before the repository needs to use it.

Implementing Interface-Based Injection in PHP

Interface-based Injection involves defining an interface that includes a method declaration for setting the dependency. Any class that needs to inject the dependency must implement this interface and provide the concrete method.

<?php

interface DatabaseConnectionInterface {
    public function setDatabaseConnection(DatabaseConnection $dbConnection);
}

class UserRepository implements DatabaseConnectionInterface {
    protected $dbConnection;

    public function setDatabaseConnection(DatabaseConnection $dbConnection) {
        $this->dbConnection = $dbConnection;
    }
    
    // Repository methods
}

//...

?>

The example above illustrates how the UserRepository implements the DatabaseConnectionInterface, ensuring that it provides a setDatabaseConnection method for DI.

Using a Dependency Injection Container in PHP

A more advanced implementation of DI involves using a DI container, which is responsible for instantiating classes and injecting dependencies automatically. PHP has several DI containers; popular ones include Pimple, PHP-DI, and Symfony’s dependency injection component.

Here, we illustrate a simplified example using Pimple as the DI container:

<?php

// Include Pimple's autoloader
require 'vendor/autoload.php';

use Pimple\Container;

$container = new Container();

$container['logger'] = function() {
    return new Logger();
};

$container['application'] = function ($c) {
    return new Application($c['logger']);
};

$app = $container['application'];
$app->run();

?>

In this example, Pimple container’s anonymous functions define how to instantiate the Logger and Application classes. When $container['application'] is accessed, Pimple automatically injects the Logger dependency.

DI Best Practices

Proper implementation of Dependency Injection comes with a set of best practices:

  • Aim for a clear contract between the client class and its dependencies.
  • Prefer constructor injection for mandatory dependencies and setter injection for optional ones.
  • Consider the single responsibility principle to avoid injecting an excessive number of dependencies in a single class.
  • Utilize interfaces to define the abstract agreement for dependencies.

Conclusion

Implementing Dependency Injection in PHP is about understanding the architectural design pattern and applying it to manage your dependencies elegantly. Through constructor, setter, or interface-based injection, you can decrease coupling and increase the modularity and testability of your PHP application. For large-scale applications, leveraging a DI container can streamline the management of dependencies, saving you time and making your codebase more robust.