Контейнер внедрения зависимостей
Обзор
Контейнер внедрения зависимостей (DIC) — это мощное расширение, которое позволяет управлять зависимостями вашего приложения. Это также одна из главных причин, по которой Flight хорошо сочетается с инструментами ИИ для написания кода и модульными тестами: контроллеры получают то, что им нужно, через конструктор, а не обращаются к глобальным переменным.
Понимание
Внедрение зависимостей (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 в том, что модульное тестирование становится намного проще. Вы можете создать мок-объект и передать его в ваш класс. Это огромное преимущество при написании тестов для вашего приложения — а когда ИИ-ассистент генерирует контроллер, внедрение через конструктор даёт ему понятный и последовательный шаблон для следования (руководство по модульному тестированию).
Создание централизованного обработчика 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. Подставьте тот же экземпляр из начальной загрузки. Именно так делает официальный скелет, и это шаблон, который AGENTS.md ожидает для контроллеров, сгенерированных ИИ:
// Где-то в вашем файле начальной загрузки / 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 (структура скелета — регистр папки соответствует пространству имен)
namespace App\Controller;
use flight\Engine;
class MyController
{
protected Engine $app;
public function __construct(Engine $app)
{
$this->app = $app;
}
public function index(): void
{
// Никакого фасада Flight:: в прикладном слое — проще тестировать и понятнее для ИИ-инструментов
$this->app->render('welcome', ['message' => 'Hello']);
}
}// app/config/routes.php
use App\Controller\MyController;
$router->get('/', [MyController::class, 'index']);Если вы пропустите подстановку Engine, Dice может создать второй Engine, и ваш контроллер не будет использовать общие маршруты, конфигурацию или сопоставленный Twig render из начальной загрузки.
Добавление других общих сервисов (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(). Это соответствует рекомендациям из модульного тестирования и стилю проекта скелета.
Добавление других классов
Если у вас есть другие классы, которые вы хотите добавить в контейнер, с 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';
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, но оно выполняет свою задачу с теми же преимуществами!
Смотрите также
- Установка — Структура скелета и где находится
services.php. - Автозагрузка — Пространства имён
App\и регистр папок. - Расширение Flight — Узнайте, как добавить внедрение зависимостей в ваши собственные классы, расширяя фреймворк.
- Конфигурация — Узнайте, как настроить Flight для вашего приложения.
- Маршрутизация — Узнайте, как определять маршруты для вашего приложения и как внедрение зависимостей работает с контроллерами.
- Промежуточное ПО — Узнайте, как создавать промежуточное ПО для вашего приложения и как внедрение зависимостей работает с ним.
- Модульное тестирование — Почему внедрение через конструктор лучше глобальных
Flight::. - ИИ и опыт разработчика — Единый шаблон DI для людей и агентов.
- SimplePdo — Предпочтительный помощник для работы с базой данных при внедрении.
Устранение неполадок
- Если у вас возникают проблемы с контейнером, убедитесь, что вы передаёте в контейнер правильные имена классов.
- Контроллеры, которые указывают тип
Engine, но получают «пустое» приложение: добавьте подстановку Engine (см. выше). Dice не должен создавать второй Engine черезnew. - Класс не найден для
App\Controller\…: проверьте регистр папки вapp/Controller/— см. Автозагрузка. - Обработчик должен возвращать созданный объект из
registerContainerHandler(не вызывайтеFlight::make()безreturn).
Журнал изменений
- Документация — Описание скелета Dice + подстановки Engine, SimplePdo и структуры
App\Controllerдля проектов, дружественных к ИИ. - v3.7.0 — Добавлена возможность регистрации обработчика DIC во Flight.