minte9
LearnRemember / PHP



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