Простой DI контейнер с поддержкой автовайринга
Контейнер внедрения зависимостей используется практически во всех современных архитектурных веб-приложениях, написанных на PHP, вне зависимости используется фреймворк или приложение собрано из компонентов. Реализаций написано не мало, самыми популярными являются контейнеры популярных фреймворков, а также «PHP-DI» и «Pimple».
Все популярные контейнеры безусловно классные и много чего умеют, но они «монструозные» и в большинстве проектов используется лишь малая часть их функционала. С другой стороны есть простой «Pimple», но в нем нет автовайринга, и приходится все оборачивать анонимными функциями.
$container = new Container([
Application::class => function (ContainerInterface $container): Application {
return new Application(
$container->get(RouterInterface::class),
$container->get(EmitterInterface::class)
);
},
RouterInterface::class => function (ContainerInterface $container): RouterInterface {
return new Router();
},
EmitterInterface::class => function (ContainerInterface $container): EmitterInterface {
return new Emitter();
},
]);
// Получаем экземпляр приложения
$container->get(Application::class);
На практике я часто сталкиваюсь с разработкой небольших REST API, и в подавляющем большинстве нужен именно простой контейнер, но хотелось бы с автовайрингом, чтобы можно было делать просто так:
$container = new Container([
RouterInterface::class => Router::class,
EmitterInterface::class => Emitter::class,
]);
// Получаем экземпляр приложения
$container->get(Application::class);
После не очень продолжительных поисков было принято решение написать собственную реализацию «PSR-11 Container Interface». Перед написанием статьи протестировал его на реальных боевых проектах — полет нормальный, вот исходный код на GitHub.
Возможности контейнера
Контейнер получился простым и легковесным, помимо всех встроенных типов данных в качестве значения, которое кладется в контейнер может быть:
- анонимные функции, принимающие аргументом сам контейнер;
$container->set(Application::class, function (ContainerInterface $container): Application {
return new Application(
$container->get(Router::class),
$container->get(EmitterInterface::class)
);
});
- имена классов, объекты и зависимости которых будут созданы при запросе;
$container = new Container([
Router::class => Router::class,
EmitterInterface::class => Emitter::class,
]);
- имена классов фабрик, реализующих интерфейс «Devanych\Di\FactoryInterface».
$container->setMultiple([
Router::class => RouterFactory::class,
EmitterInterface::class => EmitterFactory::class,
]);
Фабрики нужны для более сложных процессов создания объектов и являются заменой анонимных функций, тем самым делают более читаемым код установки зависимостей.
Интерфейс фабрики содержит всего один метод:
public function create(ContainerInterface $container): object;
Метод create()
так же как и анонимная функция принимает аргументом сам контейнер, но в отличие от функции всегда возвращает объект, а функция может возвращать все что угодно. Если переписать пример создания приложения с анонимной функции на реализацию фабрики, то получится вот такой класс:
final class ApplicationFactory implements FactoryInterface
{
public function create(ContainerInterface $container): Application
{
return new Application(
$container->get(Router::class),
$container->get(EmitterInterface::class)
);
}
}
Конечно, это простейший пример для понимания, на практике логика создания объекта может быть гораздо сложнее. Также в фабриках никто не мешает принимать дополнительные параметры в конструктор, так как при создани фабрики также используется автовайринг. Если в качестве значения зависимости указано имя класса фабрики, то сначала будет создана фабрика, а затем вызван метод create()
.
$container->setMultiple([
Application::class => ApplicationFactory::class,
Router::class => RouterFactory::class,
EmitterInterface::class => EmitterFactory::class,
]);
$container->get(Application::class); // Application instance
Если для создания фабрики не нужен автовайринг и важна производительность, например в продакшене, то лучше обернуть ее создание в анонимную функцию.
$container = new Container([
Application::class => fn() => new ApplicationFactory(),
Router::class => fn() => new RouterFactory(),
EmitterInterface::class => fn() => new EmitterFactory(),
]);
Важно запомнить, что фабрика, как значение зависимости, при создании всегда будет возвращать не собственный объект, а объект, создаваемый методом
create()
.
Публичные методы
Для установки зависимостей доступны следующие сеттеры:
/**
* Устанавливает зависимость в конструктор.
*
* @param string $id
* @param mixed $definition
*/
$container->set($id, $definition);
/**
* Устанавливает сразу несколько зависимостей.
*
* @param array<string, mixed> $definitions
*/
$container->setMultiple($definitions);
Также метод setMultiple()
вызывается при создании экземпляра контейнера внутри конструктора. Можно создать пустой контейнер, а можно сразу передать массив зависимостей.
public function __construct(array $definitions = [])
При повторной установке зависимости с уже существующем ключом, старая зависимость будет перезаписана новой.
Для получения зависимостей доступны следующие геттеры:
/**
* Возвращает `true`, если зависимость с данным идентификатором
* была установлена, в противном случае возвращает `false`.
*
* @param string $id
* @return bool
*/
$container->has($id);
/**
* Возвращает зависимость или результат ее выполнения из контейнера по идентификатору.
*
* @param string $id
* @return mixed
* @throws Devanych\Di\Exception\NotFoundException если определение не найдено в контейнере.
* @throws Devanych\Di\Exception\ContainerException если не удается создать экземпляр.
*/
$container->get($id);
/**
* Возвращает всегда новый экземпляр зависимости из контейнера по идентификатору.
*
* @param string $id
* @return mixed
* @throws Devanych\Di\Exception\NotFoundException если определение не найдено в контейнере.
* @throws Devanych\Di\Exception\ContainerException если не удается создать экземпляр.
*/
$container->getNew($id);
/**
* Возвращает исходное значение зависимости из контейнера по идентификатору.
*
* @param string $id
* @return mixed
* @throws Devanych\Di\Exception\NotFoundException если определение не найдено в контейнере.
*/
$container->getDefinition($id);
Если зависимость является анонимной функцией или именем класса, метод get() выполнит функцию и создаст экземпляр класса только в первый раз, а последующие вызовы get()
вернут уже созданный результат. Если вам нужно выполнять функцию и создавать экземпляр класса каждый раз, используйте метод getNew()
.
Если зависимость является именем класса и в его конструкторе имеются зависимости, то при вызове методов get()
и getNew()
, во время создания экземпляра класса, контейнер рекурсивно обойдет все зависимости и попытается их разрешить.
Если переданный параметр
$id
методамget()
иgetNew()
является именем класса и ранее не был установлен методомset()
, то объект этого класса все равно будет создан так, как если бы он был установлен.Если
$id
не является именем класса и не был установлен ранее методомset()
, то будет вызвано исключениеDevanych\Di\Exception\NotFoundException
.
Примеры использования смотрите на странице описания пакета «devanych/di-container».
Коментарии ( 0 )