Простой 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,
]);
$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 )

Добавить комментарий

 Латинские или кириллические буквы, не меньше 3 и не больше 30 символов.
 E-mail никто не увидит.