Dependency Injection (DI)
Dependency Injection is a design pattern that allows a class to receive its dependencies from an external source, rather than creating them itself.Wrong approach
/**
* Dependency Injection wrong approach
*
* Initialize dependency class in dependant's constructor.
* To use another dependency logger you'll need to alter the dependant class.
* This is wrong.
*/
interface LoggerInterface {
public function log($msg);
}
class OutputLogger implements LoggerInterface {
public function log($msg) {
echo "Output log message: " . $msg . PHP_EOL;
}
}
class UserService {
private $logger;
// Initialize the dependency in constructor - Wrong
public function __construct() {
$this->logger = new OutputLogger();
}
public function createUser($username) {
$this->logger->log("User $username created.");
}
}
$service = new UserService();
$service->createUser("jon_doe");
/**
> php DI_wrong.php
User jon_doe created.
*/
Correct Implementation
/**
* Dependency Injection
*
* The dependency class is injected via constructor.
* This make the service more flexible and testable.
* We can easily swap the logger implementation.
*/
interface LoggerInterface {
public function log($msg);
}
class ConsoleLogger implements LoggerInterface {
public function log($msg) {
echo "[Console] log message:" . $msg . PHP_EOL;
}
}
class FileLogger implements LoggerInterface {
public function log($msg) {
echo "[File] log message:" . $msg . PHP_EOL;
}
}
class UserService {
private $logger;
// Initialize the dependency via constructor param
public function __construct(LoggerInterface $logger) { // Look Here
$this->logger = $logger;
}
public function createUser($username) {
$this->logger->log("User $username created.");
}
}
// Create an instance of dependency()
$logger = new ConsoleLogger();
// Inject the dependency into the service
$service = new UserService($logger); // Look Here
$service->createUser("Jon Doe");
// Swap to another dependecny
$logger = new FileLogger();
$service = new UserService($logger);
$service->createUser("Jon Doe");
/**
> php DI_correct.php
[Console] log message:User Jon Doe created.
[File] log message:User Jon Doe created.
*/
Using Composer
/**
* Install autoload with Composer.
*
* Create composer.json file:
*
* {
* "autoload": {
* "psr-4": {
* "Myproject\\": "src/"
* }
* },
* "require": {}
* }
*
* Run `composer install`
*
* src/
* Logger/
* ConsoleLogger.php
* FileLogger.php
* LoggerInterface.php
* UserService.php
* vendor/
* composer.json
*/
require __DIR__ . '/vendor/autoload.php';
use Myproject\Logger\LoggerInterface;
use Myproject\Logger\ConsoleLogger;
use Myproject\Logger\FileLogger;
use Myproject\UserService;
// Create an instance of dependency()
$logger = new ConsoleLogger();
// Inject the dependency into the service
$service = new UserService($logger); // Look Here
$service->createUser("Jon Doe");
// Swap to another dependecny
$logger = new FileLogger();
$service = new UserService($logger);
$service->createUser("Jon Doe");
/**
> php DI_composer.php
[Console] log message:User Jon Doe created.
[File] log message:User Jon Doe created.
*/
Source classes:
/**
* Logger Interface
*/
namespace Myproject\Logger;
interface LoggerInterface {
public function log($msg);
}
/**
* ConsoleLogger Class (Dependency)
*/
namespace Myproject\Logger;
use Myproject\Logger\LoggerInterface;
class ConsoleLogger implements LoggerInterface {
public function log($msg) {
echo "[Console] log message:" . $msg . PHP_EOL;
}
}
/**
* User Service (Dependant Class)
*/
namespace Myproject;
use Myproject\Logger\LoggerInterface;
class UserService {
private $logger;
// Initialize the dependency via constructor param
public function __construct(LoggerInterface $logger) { // Look Here
$this->logger = $logger;
}
public function createUser($username) {
$this->logger->log("User $username created.");
}
}
Using Container
/**
* PHP-DI (dependency injection container) example.
* PHP-DI will automatically resolve dependencies, making our code
* cleaner and more maintainable.
*
* Install PHP-DI with composer:
* composer require php-di/php-di
*
* PHP-DI automatically injects the Logger instance into Service
* without us needing to manually pass it.
*/
require __DIR__ . '/vendor/autoload.php';
use DI\Container;
use function DI\create;
use function DI\get;
use Myproject\Logger\LoggerInterface;
use Myproject\Logger\ConsoleLogger;
use Myproject\Logger\FileLogger;
use Myproject\UserService;
// Create a DI container
$container = new Container();
// Configure dependencies
$container->set(LoggerInterface::class, create(ConsoleLogger::class));
// Get service from container
$service = $container->get(UserService::class);
// Use the servie
$service->createUser("Jon Doe");
// User another dependency
$container = new Container();
$container->set(LoggerInterface::class, create(FileLogger::class)); // Look Here
$service = $container->get(UserService::class);
$service->createUser("Jon Doe");
/**
> php DI_container.php
[Console] log message:User Jon Doe created.
[File] log message:User Jon Doe created.
*/
Unit Testing DI
T
/**
* Dependency Injection with PHP-DI makes unit testing easier.
* It allows us to use mock objects.
*
* If we instantiate FileLogger or ConsoleLogger directly in UserService,
* it makes UnitTesting harder. Test will actually write logs to file or console.
*
* With PHP-DI, we can pass mock object instead of a real logger.
*
* Install PHPUnit via Composer:
* composer require --dev phpunit/phpunit
*/
use PHPUnit\Framework\TestCase;
use Myproject\Logger\LoggerInterface;
use Myproject\UserService;
class UserServiceTest extends TestCase {
public function testCreateUserLogsMessage() {
// Step 1: Create a mock LoggerInterface
$logger = $this->createMock(LoggerInterface::class);
// Step 2: Expect the 'log' method to be called once
// with the expected message
$logger->expects($this->once()) // Ensure it's called once
->method('log')
->with($this->equalTo("User Jon Doe created."));
// Step 3: Inject the mock object into UserService
$service = new UserService($logger);
// Step 4: Call the method
$service->createUser("Jon Doe");
// The test will pass if 'log' was called exactly once with the expected argument.
}
}
/**
* php vendor/bin/phpunit tests/UserServiceTest.php
*
* Time: 00:00.006, Memory: 8.00 MB
* OK (1 test, 1 assertion)
*/
Last update: 9 days ago