Dependency Injection Containers

  • Сергей Толкачёв

Как и для чего использовать Dependency Injection Containers в Joomla.

Введение

Понятие контейнеров внедрения зависимостей (DI-контейнеры, DIC) появилось в Joomla 4. На самом деле, DI-контейнеры уже давно существуют в экосистеме PHP для поддержки целей внедрения зависимостей. Например, Symfony представила эту концепцию в 2009 году.

Есть несколько причин, по которым пришло время внедрить их в Joomla:

  • Тестирование — одной из тем Joomla 3 были глючные релизы. Необходимо иметь возможность тестировать классы и компоненты более простым способом. Внедрение зависимостей позволяет значительно упростить внедрение классов Mock, что позволит сократить количество ошибок.
  • Уменьшить количество "магии" в Joomla - Joomla имеет большое количество "волшебных" файлов, названия которых нужно угадывать. Это увеличивает количество времени, которое люди, плохо знакомые с Joomla, тратят на изучение соглашений по именованию файлов. Предоставление конкретного класса в расширениях позволяет нам легко тестировать совместимость расширений с другими расширениями (например, категориями и ассоциациями).

Глобальный контейнер

Внедрение глобального контейнера зависимостей очень условно заменяет класс Factory, поэтому не надо воспринимать контейнер как прямую замену классу Factory.

Так, например, в контроллерах CMS вместо

\Joomla\CMS\Factory::getDocument();

правильнее использовать

$this->app->getDocument();

При этом используется внедряемое приложение, что позволяет упростить тестирование.

Создание объекта в контейнере

Чтобы поместить что-то в глобальный DI-контейнер Joomla проще всего передать анонимную функцию. Пример для логгера ниже:

// Здесь предполагаем, что у нас уже есть экземпляр Joomla Container
$container->share(
    LoggerInterface::class,
    function (Container $container)
    {
        return \Joomla\CMS\Log\Log::createDelegatedLogger();
    },
    true
);

Функция share принимает два обязательных параметра и необязательный третий параметр:

Параметр Описание
$key Имя сервиса (dataStore key) - почти всегда является именем класса, который вы создаете.
$value Анонимная функция принимает единственный параметр — экземпляр контейнера (это позволяет вам получать любые зависимости из контейнера). Возвращаемое значение — это сервис, который мы хотим поместить в контейнер.
$protected Необязательный булев параметр, определяет, защищён ли сервис от перезаписи (т. е. разрешено ли кому-либо еще переопределять ее в контейнере). Как правило, для основных служб Joomla, таких как объекты сессии (Session), это true.

Теперь рассмотрим более сложный пример:

$container->alias('AmazingApiRouter', Joomla\CMS\Router\ApiRouter::class)
    ->share(
    \Joomla\CMS\Router\ApiRouter::class,
    function (Container $container)
    {
        return new \Joomla\CMS\Router\ApiRouter($container->get(\Joomla\CMS\Application\ApiApplication::class));
    },
    true
);

Здесь видно, что мы добавили две вещи — начали использовать зависимости (роутер API получает приложение API из контейнера) и мы также создали алиас для ApiRouter. Это означает, что контейнер создает экземпляр ApiRouter тогда, когда распознает использование класса. Зато в нашем коде для простоты мы сможем запустить следующий вызов, чтобы получить наш роутер:

Factory::getContainer()->get('AmazingApiRouter');

В то время как в Joomla провайдеры могут выглядеть более сложными, потому что логика создания объектов внутри анонимной функции более сложна - все они следуют этой базовой идее.

Провайдеры

Провайдеры в Joomla — это способ регистрации зависимости в сервис-контейнере. Для этого необходимо создать класс, реализующий Joomla\DI\ServiceProviderInterface.

Таким образом мы получаем доступ к методу регистрации, который содержит контейнер. Затем мы можем снова использовать метод share, чтобы добавить любое количество объектов в контейнер. Далее мы можем зарегистрировать их в контейнере с помощью \Joomla\DI\Container::registerServiceProvider.

Пример того, как в Joomla регистрируются все сервис-провайдеры, можно посмотреть в методе \Joomla\CMS\Factory::createContainer:

// libraries/src/Factory.php

 /**
 * Create a container object
 *
 * @return  Container
 *
 * @since   4.0.0
 */
protected static function createContainer(): Container
{
	$container = (new Container())
		->registerServiceProvider(new \Joomla\CMS\Service\Provider\Application())
		->registerServiceProvider(new \Joomla\CMS\Service\Provider\Authentication())
		->registerServiceProvider(new \Joomla\CMS\Service\Provider\CacheController())
		->registerServiceProvider(new \Joomla\CMS\Service\Provider\Config())
		->registerServiceProvider(new \Joomla\CMS\Service\Provider\Console())
		->registerServiceProvider(new \Joomla\CMS\Service\Provider\Database())
		->registerServiceProvider(new \Joomla\CMS\Service\Provider\Dispatcher())
		->registerServiceProvider(new \Joomla\CMS\Service\Provider\Document())
		->registerServiceProvider(new \Joomla\CMS\Service\Provider\Form())
		->registerServiceProvider(new \Joomla\CMS\Service\Provider\Logger())
		->registerServiceProvider(new \Joomla\CMS\Service\Provider\Language())
		->registerServiceProvider(new \Joomla\CMS\Service\Provider\Menu())
		->registerServiceProvider(new \Joomla\CMS\Service\Provider\Pathway())
		->registerServiceProvider(new \Joomla\CMS\Service\Provider\HTMLRegistry())
		->registerServiceProvider(new \Joomla\CMS\Service\Provider\Session())
		->registerServiceProvider(new \Joomla\CMS\Service\Provider\Toolbar())
		->registerServiceProvider(new \Joomla\CMS\Service\Provider\WebAssetRegistry())
		->registerServiceProvider(new \Joomla\CMS\Service\Provider\Router())
		->registerServiceProvider(new \Joomla\CMS\Service\Provider\User());

	return $container;
}

Контейнер компонента

Каждый компонент также имеет свой собственный контейнер (который размещается в разделе administrator). Однако этот контейнер не подвергается воздействию. Он нужен только для того, чтобы получить системные зависимости и позволить классу представлять ваше расширение. Этот класс является классом Extension и как минимум должен реализовывать интерфейс соответствующего типа расширения. Например, компонент должен реализовывать \Joomla\CMS\Extension\ComponentInterface (libraries/src/Extension/ComponentInterface.php).

На примере ниже мы видим, как регистрируются провайдеры компонента com_content, и как объект компонента помещается в контейнер:

// administrator/components/com_content/services/provider.php

/**
 * The content service provider.
 *
 * @since  4.0.0
 */
return new class implements ServiceProviderInterface
{
    /**
     * Registers the service provider with a DI container.
     *
     * @param   Container  $container  The DI container.
     *
     * @return  void
     *
     * @since   4.0.0
     */
    public function register(Container $container)
    {
        $container->set(AssociationExtensionInterface::class, new AssociationsHelper());

        $container->registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Content'));
        $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Content'));
        $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Content'));
        $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Content'));

        $container->set(
            ComponentInterface::class,
            function (Container $container) {
                $component = new ContentComponent($container->get(ComponentDispatcherFactoryInterface::class));

                $component->setRegistry($container->get(Registry::class));
                $component->setMVCFactory($container->get(MVCFactoryInterface::class));
                $component->setCategoryFactory($container->get(CategoryFactoryInterface::class));
                $component->setAssociationExtension($container->get(AssociationExtensionInterface::class));
                $component->setRouterFactory($container->get(RouterFactoryInterface::class));

                return $component;
            }
        );
    }
};

Использование контейнера компонента в другом расширении

Мы можем легко получить контейнер другого расширения через объект CMSApplication. Например:

Factory::getApplication()->bootComponent('com_content')->getMVCFactory()->createModel('Articles', 'Site');

Код выше получает контейнер com_content, получает MVC Factory и получает ArticlesModel фронтенда Joomla. И это будет работать в любом расширении во фронтенде, бэкэнде или API Joomla (в отличие от старого метода LegacyModel::getInstance()).

Дополнительно

В документации Joomla Framework есть отличный пример того, почему внедрение зависимостей полезно для вашего приложения и как DIC помогает его структурировать. Читать на GitHub.

Factory Application CMSApplication DIC

  • Последнее обновление: .

© Joomla для профессионалов. Все права защищены.