Перейти к содержимому

Контейнер зависимостей в Joomla 6: как работает Dependency Injection

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

Связаться со мной

Оставьте свои контакты, и я свяжусь с вами в ближайшее время.