Контейнер для ін'єкції залежностей

Огляд

Контейнер для ін'єкції залежностей (DIC) — це потужне розширення, яке дозволяє керувати залежностями вашого додатка.

Розуміння

Ін'єкція залежностей (DI) — це ключове поняття в сучасних PHP-фреймворках і використовується для керування створенням та конфігурацією об'єктів. Деякі приклади бібліотек DIC: flightphp/container, Dice, Pimple, PHP-DI, і league/container.

DIC — це вишуканий спосіб дозволити вам створювати та керувати вашими класами в централізованому місці. Це корисно, коли вам потрібно передавати той самий об'єкт до кількох класів (наприклад, до ваших контролерів або middleware).

Основне використання

Старий спосіб робити речі може виглядати так:


require 'vendor/autoload.php';

// клас для керування користувачами з бази даних
class UserController {

    protected PDO $pdo;

    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }

    public function view(int $id) {
        $stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = :id');
        $stmt->execute(['id' => $id]);

        print_r($stmt->fetch());
    }
}

// у вашому файлі routes.php

$db = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');

$UserController = new UserController($db);
Flight::route('/user/@id', [ $UserController, 'view' ]);
// інші маршрути UserController...

Flight::start();

З наведеного вище коду видно, що ми створюємо новий об'єкт PDO і передаємо його до класу UserController. Це нормально для малого додатка, але коли ваш додаток росте, ви помітите, що створюєте або передаєте той самий об'єкт PDO в кількох місцях. Тут DIC стає корисним.

Ось той самий приклад з використанням DIC (використовуючи Dice):


require 'vendor/autoload.php';

// той самий клас, як вище. Нічого не змінилося
class UserController {

    protected PDO $pdo;

    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }

    public function view(int $id) {
        $stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = :id');
        $stmt->execute(['id' => $id]);

        print_r($stmt->fetch());
    }
}

// створюємо новий контейнер
$container = new \Dice\Dice;

// додаємо правило, щоб сказати контейнеру, як створювати об'єкт PDO
// не забудьте перепризначити його собі, як нижче!
$container = $container->addRule('PDO', [
    // shared означає, що той самий об'єкт буде повертатися кожного разу
    'shared' => true,
    'constructParams' => ['mysql:host=localhost;dbname=test', 'user', 'pass' ]
]);

// Це реєструє обробник контейнера, щоб Flight знав використовувати його.
Flight::registerContainerHandler(function($class, $params) use ($container) {
    return $container->create($class, $params);
});

// тепер ми можемо використовувати контейнер для створення нашого UserController
Flight::route('/user/@id', [ UserController::class, 'view' ]);

Flight::start();

Я впевнений, ви можете думати, що було додано багато зайвого коду до прикладу. Магія приходить, коли у вас є інший контролер, якому потрібен об'єкт PDO.


// Якщо всі ваші контролери мають конструктор, якому потрібен об'єкт PDO
// кожен з маршрутів нижче автоматично отримає його ін'єкцію!!!
Flight::route('/company/@id', [ CompanyController::class, 'view' ]);
Flight::route('/organization/@id', [ OrganizationController::class, 'view' ]);
Flight::route('/category/@id', [ CategoryController::class, 'view' ]);
Flight::route('/settings', [ SettingsController::class, 'view' ]);

Додатковою перевагою використання DIC є те, що модульне тестування стає набагато простішим. Ви можете створити мок-об'єкт і передати його до вашого класу. Це величезна користь, коли ви пишете тести для вашого додатка!

Створення централізованого обробника DIC

Ви можете створити централізований обробник DIC у вашому файлі сервісів, розширюючи ваш додаток. Ось приклад:

// services.php

// створюємо новий контейнер
$container = new \Dice\Dice;
// не забудьте перепризначити його собі, як нижче!
$container = $container->addRule('PDO', [
    // shared означає, що той самий об'єкт буде повертатися кожного разу
    'shared' => true,
    'constructParams' => ['mysql:host=localhost;dbname=test', 'user', 'pass' ]
]);

// тепер ми можемо створити мапований метод для створення будь-якого об'єкта. 
Flight::map('make', function($class, $params = []) use ($container) {
    return $container->create($class, $params);
});

// Це реєструє обробник контейнера, щоб Flight знав використовувати його для контролерів/middleware
Flight::registerContainerHandler(function($class, $params) {
    Flight::make($class, $params);
});

// припустимо, у нас є наступний приклад класу, який приймає об'єкт PDO в конструкторі
class EmailCron {
    protected PDO $pdo;

    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }

    public function send() {
        // код, що надсилає email
    }
}

// І нарешті, ви можете створювати об'єкти з використанням ін'єкції залежностей
$emailCron = Flight::make(EmailCron::class);
$emailCron->send();

flightphp/container

Flight має плагін, який надає простий контейнер, сумісний з PSR-11, який ви можете використовувати для керування вашими залежностями. Ось швидкий приклад, як його використовувати:


// index.php наприклад
require 'vendor/autoload.php';

use flight\Container;

$container = new Container;

$container->set(PDO::class, fn(): PDO => new PDO('sqlite::memory:'));

Flight::registerContainerHandler([$container, 'get']);

class TestController {
  private PDO $pdo;

  function __construct(PDO $pdo) {
    $this->pdo = $pdo;
  }

  function index() {
    var_dump($this->pdo);
    // виведе це правильно!
  }
}

Flight::route('GET /', [TestController::class, 'index']);

Flight::start();

Розширене використання flightphp/container

Ви також можете розв'язувати залежності рекурсивно. Ось приклад:

<?php

require 'vendor/autoload.php';

use flight\Container;

class User {}

interface UserRepository {
  function find(int $id): ?User;
}

class PdoUserRepository implements UserRepository {
  private PDO $pdo;

  function __construct(PDO $pdo) {
    $this->pdo = $pdo;
  }

  function find(int $id): ?User {
    // Реалізація ...
    return null;
  }
}

$container = new Container;

$container->set(PDO::class, static fn(): PDO => new PDO('sqlite::memory:'));
$container->set(UserRepository::class, PdoUserRepository::class);

$userRepository = $container->get(UserRepository::class);
var_dump($userRepository);

/*
object(PdoUserRepository)#4 (1) {
  ["pdo":"PdoUserRepository":private]=>
  object(PDO)#3 (0) {
  }
}
 */

DICE

Ви також можете створити свій власний обробник DIC. Це корисно, якщо у вас є власний контейнер, який ви хочете використовувати, що не є PSR-11 (Dice). Дивіться основне використання для того, як це зробити.

Крім того, є деякі корисні налаштування за замовчуванням, які полегшать ваше життя при використанні Flight.

Екземпляр Engine

Якщо ви використовуєте екземпляр Engine у ваших контролерах/middleware, ось як би ви його налаштували:


// Десь у вашому файлі завантаження
$engine = Flight::app();

$container = new \Dice\Dice;
$container = $container->addRule('*', [
    'substitutions' => [
        // Тут ви передаєте екземпляр
        Engine::class => $engine
    ]
]);

$engine->registerContainerHandler(function($class, $params) use ($container) {
    return $container->create($class, $params);
});

// Тепер ви можете використовувати екземпляр Engine у ваших контролерах/middleware

class MyController {
    public function __construct(Engine $app) {
        $this->app = $app;
    }

    public function index() {
        $this->app->render('index');
    }
}

Додавання інших класів

Якщо у вас є інші класи, які ви хочете додати до контейнера, з Dice це легко, оскільки вони будуть автоматично розв'язані контейнером. Ось приклад:


$container = new \Dice\Dice;
// Якщо вам не потрібно ін'єктувати залежності до ваших класів
// вам не потрібно нічого визначати!
Flight::registerContainerHandler(function($class, $params) use ($container) {
    return $container->create($class, $params);
});

class MyCustomClass {
    public function parseThing() {
        return 'thing';
    }
}

class UserController {

    protected MyCustomClass $MyCustomClass;

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

    public function index() {
        echo $this->MyCustomClass->parseThing();
    }
}

Flight::route('/user', 'UserController->index');

PSR-11

Flight також може використовувати будь-який контейнер, сумісний з PSR-11. Це означає, що ви можете використовувати будь-який контейнер, який реалізує інтерфейс PSR-11. Ось приклад з використанням контейнера PSR-11 від League:


require 'vendor/autoload.php';

// той самий клас UserController, як вище

$container = new \League\Container\Container();
$container->add(UserController::class)->addArgument(PdoWrapper::class);
$container->add(PdoWrapper::class)
    ->addArgument('mysql:host=localhost;dbname=test')
    ->addArgument('user')
    ->addArgument('pass');
Flight::registerContainerHandler($container);

Flight::route('/user', [ 'UserController', 'view' ]);

Flight::start();

Це може бути трохи більш багатослівним, ніж попередній приклад з Dice, але все однаково виконує роботу з тими самими перевагами!

Дивіться також

  • Розширення Flight - Дізнайтеся, як ви можете додати ін'єкцію залежностей до ваших власних класів, розширюючи фреймворк.
  • Конфігурація - Дізнайтеся, як налаштувати Flight для вашого додатка.
  • Маршрутизація - Дізнайтеся, як визначати маршрути для вашого додатка та як працює ін'єкція залежностей з контролерами.
  • Middleware - Дізнайтеся, як створювати middleware для вашого додатка та як працює ін'єкція залежностей з middleware.

Вирішення проблем

  • Якщо у вас проблеми з вашим контейнером, переконайтеся, що ви передаєте правильні назви класів до контейнера.

Журнал змін

  • v3.7.0 - Додано можливість реєструвати обробник DIC до Flight.