Що таке контейнер у 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