Контейнер для впровадження залежностей

Вступ

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

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

Основний приклад

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


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());
    }
}

$User = new UserController(new PDO('mysql:host=localhost;dbname=test', 'user', 'pass'));
Flight::route('/user/@id', [ $UserController, 'view' ]);

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;
// не забудьте перепризначити його, як показано нижче!
$container = $container->addRule('PDO', [
    // спільний означає, що той же об'єкт буде повертатися щоразу
    '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', 'view' ]);
// або, альтернативно, ви можете визначити маршрут таким чином
Flight::route('/user/@id', 'UserController->view');
// або
Flight::route('/user/@id', 'UserController::view');

Flight::start();

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


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

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

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, але все одно виконує свою роботу з такими ж перевагами!

Користувацький обробник DIC

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

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

Екземпляр двигуна

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


// Десь у вашому файлі завантаження
$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 у своїх контролерах/проміжних програмах

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 'річ';
    }
}

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');