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
- Последнее обновление: .