Что такое контейнер в Joomla
В документации Joomla используется название Dependency Injection Container, или сокращённо DIC. Технически это хранилище сервисов, где каждый сервис имеет ключ и значение либо функцию, которая знает, как создать это значение.
В качестве ключа чаще всего выступает полное имя PHP-класса или интерфейса:
Joomla\Database\DatabaseInterface::class
Контейнер может хранить уже созданный объект или callback, который создаст объект только тогда, когда он впервые понадобится.
Упрощённо контейнер можно представить в виде таблицы:
| Ключ |
Что возвращает контейнер |
DatabaseInterface::class |
Объект для работы с базой данных |
SomeFactoryInterface::class |
Конкретную реализацию фабрики |
MyServiceInterface::class |
Сервис, зарегистрированный расширением |
Контейнер не следует понимать как место, где физически заранее созданы все объекты Joomla. Для многих сервисов хранится правило создания, а сам объект появляется только после обращения к нему.
Зачем Joomla использует Dependency Injection
Без Dependency Injection класс часто самостоятельно создаёт всё, что ему нужно:
final class ProductService
{
public function __construct()
{
$this->repository = new ProductRepository();
}
}
В таком коде ProductService жёстко связан с конкретным ProductRepository. Заменить реализацию, передать тестовый объект или изменить способ создания репозитория становится сложнее.
При использовании Dependency Injection зависимость передаётся классу извне:
final class ProductService
{
public function __construct(
private ProductRepositoryInterface $repository
) {
}
}
Теперь ProductService не решает, как создать репозиторий. Он только сообщает, что ему нужен объект, реализующий ProductRepositoryInterface.
Именно на этом уровне появляется контейнер: он может знать, какую конкретную реализацию необходимо создать для определённого интерфейса.
Это даёт несколько практических преимуществ:
- классы меньше зависят от конкретных реализаций;
- объекты проще тестировать;
- создание сложных зависимостей концентрируется в одном месте;
- один сервис можно заменить другой реализацией;
- общие объекты не нужно вручную создавать в разных частях расширения.
Главный контейнер Joomla
Во время запуска Joomla создаёт основной DI-контейнер и регистрирует в нём системные сервисы. В исходном коде Joomla контейнер используется уже на этапе запуска приложения.
Получить главный контейнер можно через:
use Joomla\CMS\Factory;
$container = Factory::getContainer();
Например, объект базы данных можно получить по интерфейсу DatabaseInterface:
use Joomla\CMS\Factory;
use Joomla\Database\DatabaseInterface;
$db = Factory::getContainer()->get(DatabaseInterface::class);
Контейнер находит сервис, зарегистрированный для DatabaseInterface::class, при необходимости создаёт его и возвращает готовый объект.
Технически Factory::getContainer()->get() позволяет получить сервис практически из любого места. Но постоянно обращаться к глобальному контейнеру внутри бизнес-классов не следует. В собственных классах лучше явно передавать необходимые зависимости через конструктор или setter.
Как работает get()
Основной метод получения сервиса — get():
$service = $container->get(ServiceInterface::class);
Когда вызывается get(), контейнер:
- находит запись по переданному ключу;
- определяет, нужно ли создавать объект;
- выполняет зарегистрированный callback, если сервис ещё не создан;
- возвращает полученный объект;
- для shared-сервиса сохраняет созданный экземпляр и в дальнейшем возвращает его повторно.
Перед получением необязательного сервиса можно проверить его наличие:
if ($container->has(MyServiceInterface::class)) {
$service = $container->get(MyServiceInterface::class);
}
set() и share(): регистрация сервисов
Метод set() добавляет ресурс в контейнер. Например, у нас есть собственный сервис:
namespace Joomla\Component\PavlukShop\Administrator\Service;
final class PriceFormatter
{
public function format(float $price): string
{
return number_format($price, 2, '.', ' ');
}
}
Его можно зарегистрировать по имени класса:
use Joomla\Component\PavlukShop\Administrator\Service\PriceFormatter;
use Joomla\DI\Container;
$container->set(
PriceFormatter::class,
static function (Container $container): PriceFormatter {
return new PriceFormatter();
}
);
После этого:
$formatter = $container->get(PriceFormatter::class);
Метод share() используется, когда контейнер должен создать объект один раз и в дальнейшем возвращать тот же экземпляр:
$container->share(
PriceFormatter::class,
static function (Container $container): PriceFormatter {
return new PriceFormatter();
}
);
Это существенное различие: для shared-сервиса повторный вызов get() не должен создавать новый объект.
Почему сервисы часто регистрируют по интерфейсу
В реальном расширении лучше, чтобы код зависел от контракта, а не от конкретного класса.
Например:
namespace Joomla\Component\PavlukShop\Administrator\Service;
interface PriceFormatterInterface
{
public function format(float $price): string;
}
Реализация:
namespace Joomla\Component\PavlukShop\Administrator\Service;
final class PriceFormatter implements PriceFormatterInterface
{
public function format(float $price): string
{
return number_format($price, 2, '.', ' ');
}
}
В контейнере сервис можно зарегистрировать по интерфейсу:
$container->share(
PriceFormatterInterface::class,
static function (Container $container): PriceFormatterInterface {
return new PriceFormatter();
}
);
Класс, использующий форматирование цены, теперь зависит только от интерфейса:
final class ProductPriceService
{
public function __construct(
private PriceFormatterInterface $formatter
) {
}
public function getFormattedPrice(float $price): string
{
return $this->formatter->format($price);
}
}
Контейнер Joomla также поддерживает aliases — альтернативные ключи для уже зарегистрированного ресурса:
$container->alias(
PriceFormatter::class,
PriceFormatterInterface::class
);
В самой Joomla aliases активно используются для связывания разных имён сервисов и интерфейсов с одной реализацией.
Что такое services/provider.php
Для расширений Joomla основным местом настройки DI-контейнера является файл services/provider.php.
Он возвращает объект, реализующий Joomla\DI\ServiceProviderInterface:
<?php
defined('_JEXEC') or die;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
return new class implements ServiceProviderInterface
{
public function register(Container $container): void
{
// Register extension services here.
}
};
Joomla загружает этот файл и вызывает метод register(). Именно здесь расширение сообщает контейнеру, какие сервисы оно предоставляет и как их создавать.
Например, собственный сервис компонента можно зарегистрировать так:
<?php
defined('_JEXEC') or die;
use Joomla\Component\PavlukShop\Administrator\Service\PriceFormatter;
use Joomla\Component\PavlukShop\Administrator\Service\PriceFormatterInterface;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
return new class implements ServiceProviderInterface
{
public function register(Container $container): void
{
$container->share(
PriceFormatterInterface::class,
static function (Container $container): PriceFormatterInterface {
return new PriceFormatter();
}
);
}
};
Как передавать зависимости в собственные классы
Предположим, сервису компонента одновременно нужны база данных и форматировщик цены:
namespace Joomla\Component\PavlukShop\Administrator\Service;
use Joomla\Database\DatabaseInterface;
final class ProductService
{
public function __construct(
private DatabaseInterface $db,
private PriceFormatterInterface $formatter
) {
}
}
Сам ProductService не обращается к Factory и не создаёт зависимости через new. Все необходимые объекты передаются ему через конструктор.
Их можно собрать в provider:
use Joomla\Component\PavlukShop\Administrator\Service\PriceFormatterInterface;
use Joomla\Component\PavlukShop\Administrator\Service\ProductService;
use Joomla\Database\DatabaseInterface;
use Joomla\DI\Container;
$container->share(
ProductService::class,
static function (Container $container): ProductService {
return new ProductService(
$container->get(DatabaseInterface::class),
$container->get(PriceFormatterInterface::class)
);
}
);
Здесь хорошо видна роль контейнера: он отвечает за создание и связывание объектов, а ProductService отвечает только за свою прикладную логику.
Именно services/provider.php является нормальным местом для вызовов $container->get(), необходимых для построения объекта. Размещать такие вызовы непосредственно внутри каждого бизнес-класса обычно нет необходимости.
Дочерние контейнеры расширений
Архитектура Joomla не ограничивается одним глобальным контейнером. Во время загрузки расширения Joomla создаёт для него дочерний DI-контейнер.
Это позволяет расширению регистрировать собственные сервисы отдельно от главного контейнера Joomla.
При этом дочерний контейнер связан с родительским:
set() регистрирует сервис в контейнере расширения;
get() сначала ищет сервис в дочернем контейнере;
- если сервис там не найден, поиск переходит к главному контейнеру Joomla.
Благодаря этому services/provider.php компонента может получить системный сервис:
$container->get(DatabaseInterface::class);
даже если DatabaseInterface регистрировался не самим компонентом, а Joomla.
Service Provider в компонентах Joomla
В компонентах services/provider.php используется не только для собственных прикладных сервисов. Через него Joomla регистрирует основной Extension-класс компонента и его инфраструктурные зависимости.
Типичный provider компонента содержит регистрацию MVC Factory и Component Dispatcher Factory:
use Joomla\CMS\Dispatcher\ComponentDispatcherFactoryInterface;
use Joomla\CMS\Extension\ComponentInterface;
use Joomla\CMS\Extension\MVCComponent;
use Joomla\CMS\Extension\Service\Provider\ComponentDispatcherFactory;
use Joomla\CMS\Extension\Service\Provider\MVCFactory;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
return new class implements ServiceProviderInterface
{
public function register(Container $container): void
{
$container->registerServiceProvider(
new MVCFactory('\\Acme\\Component\\Shop')
);
$container->registerServiceProvider(
new ComponentDispatcherFactory('\\Acme\\Component\\Shop')
);
$container->set(
ComponentInterface::class,
static function (Container $container): ComponentInterface {
$component = new MVCComponent(
$container->get(ComponentDispatcherFactoryInterface::class)
);
$component->setMVCFactory(
$container->get(MVCFactoryInterface::class)
);
return $component;
}
);
}
};
Метод registerServiceProvider() позволяет подключить другой Service Provider. Фактически Joomla вызывает его метод register(), а тот добавляет необходимые сервисы в контейнер.
Поэтому файлы services/provider.php, которые встречаются в компонентах, модулях и плагинах Joomla, — это не просто шаблонный код. Они формируют связи между основными объектами расширения.
Container и Dependency Injection — не одно и то же
Важно различать две похожие конструкции.
Первый вариант:
final class ProductService
{
public function execute(): void
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
}
}
Класс самостоятельно обращается к контейнеру и ищет свою зависимость. Это подход, известный как Service Locator.
Второй вариант:
final class ProductService
{
public function __construct(
private DatabaseInterface $db
) {
}
}
Класс явно объявляет свою зависимость, а контейнер используется извне для создания ProductService. Это уже Dependency Injection.
Для архитектуры собственного расширения второй вариант обычно лучше: зависимости класса видны из конструктора, класс проще тестировать, а его код не зависит от глобального состояния Joomla.
Практические правила работы с контейнером
- Используйте
services/provider.php для регистрации сервисов расширения.
- По возможности зависьте от интерфейсов, а не от конкретных реализаций.
- Передавайте зависимости в классы через конструктор или предусмотренные Joomla setter/aware-механизмы.
- Не создавайте вручную системные объекты Joomla, если для них уже существует сервис или фабрика.
- Используйте
share(), если в пределах контейнера нужен один общий экземпляр сервиса.
- Не используйте контейнер как глобальное хранилище произвольных данных.
- Не добавляйте
Factory::getContainer() в каждый класс только потому, что это более быстрый способ получить зависимость.
Не следует считать, что любой класс Joomla автоматически можно получить через $container->get(SomeClass::class). В контейнере доступны только зарегистрированные ресурсы и их aliases. Для части API Joomla правильным способом получения объекта является специальная Factory или API самого Application.
Контейнер Joomla вкратце
Dependency Injection Container отвечает не за бизнес-логику, а за создание и связывание объектов. Joomla регистрирует в нём системные сервисы, а расширения могут добавлять собственные через services/provider.php.
На практике схема выглядит так:
Interface
↓
services/provider.php
↓
Container
↓
Concrete implementation
↓
Constructor injection
↓
Your service
Если классу нужна база данных, фабрика, dispatcher или собственный сервис, класс не должен обязательно знать, как создать этот объект. Эту задачу можно вынести в DI-контейнер, а готовую зависимость передать классу во время его создания.
Использованные источники:
Joomla Programmers Documentation — Dependency Injection
Joomla Programmers Documentation — The Dependency Injection Container
Joomla Programmers Documentation — Extensions and Child Containers
Joomla Programmers Documentation — Plugin Methods and Arguments
Joomla Framework — DI Package
Joomla CMS — official source code