Контейнер впровадження залежностей
Огляд
Контейнер впровадження залежностей (DIC) — це потужне розширення, яке дозволяє керувати залежностями вашого застосунку. Це також одна з найбільших причин, чому Flight добре працює з AI-інструментами кодування та модульними тестами: контролери отримують те, що їм потрібно, через конструктор, замість того щоб звертатися до глобальних змінних.
Розуміння
Впровадження залежностей (DI) — це ключова концепція сучасних PHP-фреймворків, яка використовується для керування створенням та конфігурацією об'єктів. Деякі приклади бібліотек DIC: flightphp/container, Dice, Pimple, PHP-DI та league/container.
DIC — це вигадливий спосіб створення та керування вашими класами в централізованому місці. Це корисно, коли вам потрібно передати один і той самий об'єкт кільком класам (контролерам, проміжному ПЗ, командам тощо).
Офіційний flightphp/skeleton під'єднує Dice у app/config/services.php, підставляє спільний екземпляр flight\Engine та розв'язує цілі маршрутів як-от [App\Controller\HomeController::class, 'index']. Віддавайте перевагу цьому патерну для нових проєктів, щоб люди та агенти редагували одні й ті самі місця.
Базове використання
Старий спосіб може виглядати так:
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 полягає в тому, що модульне тестування стає набагато простішим. Ви можете створити імітаційний об'єкт і передати його у ваш клас. Це величезна перевага, коли ви пишете тести для свого застосунку — а коли AI-асистент генерує контролер, впровадження через конструктор дає йому чіткий і послідовний патерн для наслідування (посібник із модульного тестування).
Створення централізованого обробника 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 знав, як використовувати його для контролерів/проміжного ПЗ
Flight::registerContainerHandler(function($class, $params) {
return Flight::make($class, $params);
});
// припустімо, у нас є такий приклад класу, який приймає об'єкт PDO у конструкторі
class EmailCron {
protected PDO $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function send() {
// код, який надсилає електронний лист
}
}
// І нарешті ви можете створювати об'єкти за допомогою впровадження залежностей
$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 (обов'язково для впровадження $app)
Якщо ви вказуєте тип flight\Engine у контролерах або проміжному ПЗ, Dice не повинен створювати новий Engine. Підставте той самий екземпляр із bootstrap-коду. Саме це робить офіційний skeleton, і саме цей патерн очікує AGENTS.md для AI-згенерованих контролерів:
// Десь у вашому bootstrap / services.php
use flight\Engine;
use flight\database\SimplePdo;
$app = Flight::app(); // або $engine = Flight::app();
$container = new \Dice\Dice;
$container = $container->addRule('*', [
'substitutions' => [
// Критично важливо: перевикористовуйте ініціалізований Engine — не дозволяйте Dice робити `new Engine()`
Engine::class => $app,
// Віддавайте перевагу SimplePdo для нового коду
// SimplePdo::class => $db,
// Config::class => $config,
// \Twig\Environment::class => $twig,
]
]);
$app->registerContainerHandler(function ($class, $params) use ($container) {
return $container->create($class, $params);
});
// Додатковий помічник для коду поза маршрутами
$app->map('make', function ($class, $params = []) use ($container) {
return $container->create($class, $params);
});// app/Controller/MyController.php (структура skeleton — регістр папок відповідає неймспейсу)
namespace App\Controller;
use flight\Engine;
class MyController
{
protected Engine $app;
public function __construct(Engine $app)
{
$this->app = $app;
}
public function index(): void
{
// Жодного фасаду Flight:: у шарі застосунку — легше тестувати та зрозуміліше для AI-інструментів
$this->app->render('welcome', ['message' => 'Hello']);
}
}// app/config/routes.php
use App\Controller\MyController;
$router->get('/', [MyController::class, 'index']);Якщо ви пропустите підстановку Engine, Dice може створити другий Engine, і ваш контролер не матиме спільних маршрутів, конфігурації або зіставленого Twig render із bootstrap-коду.
Додавання інших спільних сервісів (SimplePdo, Config, Twig)
use flight\database\SimplePdo;
use flight\Engine;
// Після створення $db, $config, $twig у services.php:
$substitutions = [
Engine::class => $app,
SimplePdo::class => $db,
// App\Utils\Config::class => $config,
// \Twig\Environment::class => $twig,
];
$container = $container->addRule('*', [
'substitutions' => $substitutions,
]);Тоді контролери можуть приймати SimplePdo $db (або ваш тип конфігурації) у конструкторі й ніколи не викликати Flight::db(). Це відповідає рекомендаціям із модульного тестування та фірмовому стилю skeleton.
Додавання інших класів
Якщо у вас є інші класи, які ви хочете додати до контейнера, з 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. Ось приклад використання контейнера League PSR-11:
require 'vendor/autoload.php';
use flight\database\SimplePdo;
// та сама ідея UserController, що й вище, але з типом SimplePdo замість сирого PDO
$container = new \League\Container\Container();
$container->add(UserController::class)->addArgument(SimplePdo::class);
$container->add(SimplePdo::class)
->addArgument('mysql:host=localhost;dbname=test')
->addArgument('user')
->addArgument('pass');
Flight::registerContainerHandler($container);
Flight::route('/user', [ 'UserController', 'view' ]);
Flight::start();Це може бути трохи багатослівніше, ніж попередній приклад із Dice, але це все одно виконує роботу з тими самими перевагами!
Дивіться також
- Встановлення — Структура skeleton і де знаходиться
services.php. - Автозавантаження — Неймспейси
App\та регістр папок. - Розширення Flight — Дізнайтеся, як додати впровадження залежностей до ваших власних класів, розширивши фреймворк.
- Конфігурація — Дізнайтеся, як налаштувати Flight для вашого застосунку.
- Маршрутизація — Дізнайтеся, як визначати маршрути для вашого застосунку та як впровадження залежностей працює з контролерами.
- Проміжне ПЗ — Дізнайтеся, як створювати проміжне ПЗ для вашого застосунку та як впровадження залежностей працює з проміжним ПЗ.
- Модульне тестування — Чому впровадження через конструктор перевершує глобальні змінні
Flight::. - AI та досвід розробника — Єдиний патерн DI для людей та агентів.
- SimplePdo — Бажаний помічник для роботи з базою даних для впровадження.
Усунення неполадок
- Якщо у вас виникають проблеми з вашим контейнером, переконайтеся, що ви передаєте правильні назви класів до контейнера.
- Контролери, які вказують тип
Engine, але отримують «порожній» застосунок: додайте підстановку Engine (див. вище). Dice не повинен створювати другий Engine черезnew. - Клас не знайдено для
App\Controller\…: перевірте регістр папок уapp/Controller/— див. Автозавантаження. - Обробник повинен повертати створений об'єкт із
registerContainerHandler(не викликайтеFlight::make()безreturn).
Журнал змін
- Документація – Документовано skeleton Dice + підстановки Engine, SimplePdo та структуру
App\Controllerдля AI-дружніх проєктів. - v3.7.0 - Додано можливість реєструвати обробник DIC у Flight.