Pull to refresh

Учимся использовать Dependency Injection Containers (DI контейнеры) в Joomla 5

Level of difficultyMedium
Reading time9 min
Views540

У меня есть библиотека для работы с АПИ Сдэка. Для работы с ней надо создать экземпляр класса WishboxCdekSDK2\CdekClientV2 и вызвать нужный метод.

$apiClient = new CdekClientV2($account, $secure, $timeout);
$apiClient->getCities();

Цель данной статьи получать АПИ-клиент в основном компоненте и плагинах следующим образом:

$apiClient = $this->getCdekClientV2();

или в любом другом расширении:

$apiClient = Joomla\CMS\Factory::getApplication()
  ->bootComponent('com_wishboxcdek')->getCdekClientV2();
$apiClient = Joomla\CMS\Factory::getApplication()
  ->bootPlugin('wishboxcdek', 'console')->getCdekClientV2();

И в этом нам помогут DI контейнеры.

Давайте пойдём от обратного.

Шаг 1: трейт WishboxCdekSDK2\Trait\CdekClientV2ServiceTrait

Трейт — реализует методы getCdekClientV2 и setCdekClientV2.

Root/libraries/wishboxcdek/src/Trait/CdekClientV2ServiceTrait.php:

<?php
/**
 * @copyright   (c) 2013-2025 Nekrasov Vitaliy <nekrasov_vitaliy@list.ru>
 * @license     GNU General Public License version 2 or later;
 */
namespace WishboxCdekSDK2\Trait;

use UnexpectedValueException;
use WishboxCdekSDK2\CdekClientV2;
use WishboxCdekSDK2\CdekClientV2Interface;
use function defined;

// phpcs:disable PSR1.Files.SideEffects
defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects

/**
 * Defines the trait for the CdekClientV2 service class.
 *
 * @since  1.0.0
 */
trait CdekClientV2ServiceTrait
{
	/**
	 * The API client.
	 *
	 * @var CdekClientV2|null
	 *
	 * @since 1.0.0
	 */
	private ?CdekClientV2Interface $cdekClientV2 = null;

	/**
	 * Get the API client.
	 *
	 * @return  CdekClientV2Interface
	 *
	 * @since   1.0.0
	 */
	public function getCdekClientV2(): CdekClientV2Interface
	{
		if (!$this->cdekClientV2)
		{
			throw new UnexpectedValueException('CdekClientV2 not set in ' . __CLASS__);
		}

		return $this->cdekClientV2;
	}

	/**
	 * The API client.
	 *
	 * @param   CdekClientV2Interface  $cdekClientV2  CdekClientV2
	 *
	 * @return  void
	 *
	 * @since  1.0.0
	 */
	public function setCdekClientV2(CdekClientV2Interface $cdekClientV2): void
	{
		$this->cdekClientV2 = $cdekClientV2;
	}
}

Подключим его к классам компонента Root/administrator/components/com_wishboxcdek/src/Extension/WishboxcdekComponent.php и плагина Root/plugins/console/wishboxcdek/src/Extension/Wishboxcdek.php .

Шаг 2: класс сервис-провайдера WishboxCdekSDK2\Service\ProviderCdekClientV2

Сервис-провайдер — реализует интерфейс Joomla\DI\ServiceProviderInterface, в методе register которого в контейнере регистрируется функция, которая создаёт экземпляр АПИ-клиента.

В каждом компоненте или плагине есть файл provider.php с анонимным классом, реализующим интерфейс Joomla\DI\ServiceProviderInterface, используемый для регистрации сервис-провайдеров.

Значит нам нужен сервис-провайдер для нашего класса WishboxCdekSDK2\CdekClientV2.

По аналогии с сервис-провайдерами классов ядра Joomla назовём класс точно так же как называется класс который он будет регистрировать — WishboxCdekSDK2\Service\ProviderCdekClientV2.

Root/libraries/wishboxcdek/src/Service/Provider/CdekClientV2.php

<?php
/**
 * @copyright   (c) 2013-2025 Nekrasov Vitaliy <nekrasov_vitaliy@list.ru>
 * @license     GNU General Public License version 2 or later
 */
namespace WishboxCdekSDK2\Service\Provider;

use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use function defined;

// phpcs:disable PSR1.Files.SideEffects
defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects

/**
 * Service provider for the service MVC factory.
 *
 * @since  1.0.0
 */
class CdekClientV2 implements ServiceProviderInterface
{
	/**
	 * Аккаунт сервиса интеграции.
	 *
	 * @var string
	 *
	 * @since 1.0.0
	 */
	private string $account;

	/**
	 * Секретный пароль сервиса интеграции.
	 *
	 * @var string
	 *
	 * @since 1.0.0
	 */
	private string $secure;

	/**
	 * Timeout
	 *
	 * @var float|null
	 *
	 * @since 1.0.0
	 */
	private ?float $timeout;

	/**
	 * @param   string       $account  Account
	 * @param   string|null  $secure   Secure
	 * @param   float|null   $timeout  Timeout
	 *
	 * @since   1.0.0
	 */
	public function __construct(string $account, ?string $secure = null, ?float $timeout = 10.0)
	{
		$this->account = $account;
		$this->secure = $secure;
		$this->timeout = $timeout;
	}

	/**
	 * Registers the service provider with a DI container.
	 *
	 * @param   Container  $container  The DI container.
	 *
	 * @return  void
	 *
	 * @since   1.0.0
	 *
	 * @noinspection PhpUnusedParameterInspection
	 */
	public function register(Container $container): void
	{
		$container->set(
			\WishboxCdekSDK2\CdekClientV2Interface::class,
			function (Container $container)
			{
				return new \WishboxCdekSDK2\CdekClientV2(
					$this->account,
					$this->secure,
					$this->timeout
				);
			}
		);
	}
}

Теперь перейдём в сервис-провайдер компонента строки 49-59 и 71.

Root/administrator/components/com_wishboxcdek/services/provider.php

<?php
/**
 * @copyright   (c) 2013-2025 Nekrasov Vitaliy <nekrasov_vitaliy@list.ru>
 * @license     GNU General Public License version 2 or later;
 */

use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Component\Router\RouterFactoryInterface;
use Joomla\CMS\Dispatcher\ComponentDispatcherFactoryInterface;
use Joomla\CMS\Extension\ComponentInterface;
use Joomla\CMS\Extension\Service\Provider\ComponentDispatcherFactory;
use Joomla\CMS\Extension\Service\Provider\RouterFactory;
use Joomla\CMS\HTML\Registry;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\Component\Wishboxcdek\Administrator\Extension\WishboxcdekComponent;
use Joomla\Component\Wishboxcdek\Site\CMS\Extension\Service\Provider\MVCFactory;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use WishboxCdekSDK2\Service\Provider\CdekClientV2;

// phpcs:disable PSR1.Files.SideEffects
defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects

/**
 * The Wishboxcdek service provider.
 *
 * @since  1.0.0
 */
return new class implements ServiceProviderInterface
{
	/**
	 * Registers the service provider with a DI container.
	 *
	 * @param   Container  $container  The DI container.
	 *
	 * @return  void
	 *
	 * @since   1.0.0
	 *
	 * @noinspection PhpMissingReturnTypeInspection
	 */
	public function register(Container $container)
	{
		$container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Wishboxcdek'));
		$container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Wishboxcdek'));
		$container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Wishboxcdek'));

        // Получаем параметры компонента
		$componentParams = ComponentHelper::getParams('com_wishboxcdek');

        // Регистрируем класс сервис-провайдера АПИ клиента в контейнере
		$container->registerServiceProvider(
			new CdekClientV2(
				$componentParams->get('account', ''),
				$componentParams->get('secure', ''),
				60.0
			)
		);

		$container->set(
			ComponentInterface::class,
			function (Container $container)
			{
				$component = new WishboxcdekComponent($container->get(ComponentDispatcherFactoryInterface::class));
				$component->setRegistry($container->get(Registry::class));
				$component->setMVCFactory($container->get(MVCFactoryInterface::class));
				$component->setRouterFactory($container->get(RouterFactoryInterface::class));

                // Устанавливаем АПИ-клиент в класс компонента
                $component->setCdekClientV2($container->get(\WishboxCdekSDK2\CdekClientV2Interface::class));

				return $component;
			}
		);
	}
};

И аналогично зарегистрируем сервис-провайдер в плагине Root/plugins/console/wishboxcdek/services/provider.php.

Следующие способы получения АПИ-клиента уже должны работать.

$apiClient = Joomla\CMS\Factory::getApplication()
 ->bootComponent('com_wishboxcdek')->getCdekClientV2();
 $apiClient = Joomla\CMS\Factory::getApplication()
 ->bootPlugin('wishboxcdek', 'console')->getCdekClientV2();

Но наша цель $apiClient = $this->getCdekClientV2(); в моделях компонента.

Шаг 3: Интерфейс WishboxCdekSDK2\Interface\CdekClientV2AwareInterface и трейт WishboxCdekSDK2\TraitCdekClientV2AwareTrait

Так же пойдём от обратного, напишем для модели интерфейс, который описывает метод для установки АПИ-клиента setCdekClientV2.

Root/libraries/wishboxcdek/src/Interface/CdekClientV2AwareInterface.php

<?php
/**
 * @copyright   (c) 2013-2025 Nekrasov Vitaliy <nekrasov_vitaliy@list.ru>
 * @license     GNU General Public License version 2 or later
 */
namespace WishboxCdekSDK2\Interface;

// phpcs:disable PSR1.Files.SideEffects
use WishboxCdekSDK2\CdekClientV2Interface;
use function defined;

defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects

/**
 * Interface to be implemented by classes depending on a form factory.
 *
 * @since  1.0.0
 */
interface CdekClientV2AwareInterface
{
	/**
	 * Set the form factory to use.
	 *
	 * @param   CdekClientV2  $cdekClientV2  The API client to use.
	 *
	 * @return  CdekClientV2AwareInterface  This method is chainable.
	 *
	 * @since   1.0.0
	 */
	public function setCdekClientV2(CdekClientV2Interface $cdekClientV2): CdekClientV2AwareInterface;
}

И трейт, который реализует методы setCdekClientV2 и getCdekClientV2.

Root/libraries/wishboxcdek/src/Trait/CdekClientV2AwareTrait.php

<?php
/**
 * @copyright   (c) 2013-2025 Nekrasov Vitaliy <nekrasov_vitaliy@list.ru>
 * @license     GNU General Public License version 2 or later;
 */
namespace WishboxCdekSDK2\Trait;

use UnexpectedValueException;
use WishboxCdekSDK2\CdekClientV2;
use WishboxCdekSDK2\Interface\CdekClientV2AwareInterface;
use function defined;

// phpcs:disable PSR1.Files.SideEffects
defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects

/**
 * Defines the trait for the CdekClientV2 service class.
 *
 * @since  1.0.0
 */
trait CdekClientV2AwareTrait
{
	/**
	 * The API client.
	 *
	 * @var CdekClientV2|null
	 *
	 * @since 1.0.0
	 */
	private ?CdekClientV2 $cdekClientV2 = null;

	/**
	 * Get the API client.
	 *
	 * @return  CdekClientV2
	 *
	 * @throws  UnexpectedValueException May be thrown if the factory has not been set.
	 *
	 * @since   1.0.0
	 */
	public function getCdekClientV2(): CdekClientV2Interface
	{
		if (!$this->cdekClientV2)
		{
			throw new UnexpectedValueException('CdekClientV2 not set in ' . __CLASS__);
		}

		return $this->cdekClientV2;
	}

	/**
	 * The API client.
	 *
	 * @param   CdekClientV2  $cdekClientV2  CdekClientV2
	 *
	 * @return  CdekClientV2AwareInterface
	 *
	 * @since  1.0.0
	 */
	public function setCdekClientV2(CdekClientV2Interface $cdekClientV2): CdekClientV2AwareInterface
	{
		$this->cdekClientV2 = $cdekClientV2;

		return $this;
	}
}

Подключаем интерфейс и трейт к классу модели, теперь она поддерживает методы setCdekClientV2 и getCdekClientV2.

Но кто устанавливает АПИ-клиент в модель?. - Правильно, MVC-фабрика. Давайте вспомним пост Использование своего класса MVC фабрики в компоненте Joomla 5​.

Открываем переопределённый класс MVC-фабрики, находим метод createModel и в конце добвляем следующие строки:

if ($model instanceof CdekClientV2AwareInterface)
{
    try
    {
        $model->setCdekClientV2($this->getCdekClientV2());
    }
    catch (UnexpectedValueException $e)
    {
        // Ignore it
    }
}

Мы проверяем наличие у модели интерфейса и если есть, устанавляваем свой АПИ-клиент.

Значит и сам класс MVC-фабрики должен использовать эти интерфейс и трейт.

А в фабрику экземпляр АПИ-клиента при создании устанавливает её сервис-провайдер.

Root/administrator/components/com_wishboxcdek/src/Extension/Service/Provider/MVCFactory.php

<?php
/**
 * @copyright   (c) 2013-2025 Nekrasov Vitaliy <nekrasov_vitaliy@list.ru>
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */
namespace Joomla\Component\Wishboxcdek\Administrator\Extension\Service\Provider;

use Joomla\CMS\Cache\CacheControllerFactoryInterface;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\FormFactoryInterface;
use Joomla\CMS\Mail\MailerFactoryInterface;
use Joomla\CMS\MVC\Factory\ApiMVCFactory;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\CMS\Router\SiteRouter;
use Joomla\CMS\User\UserFactoryInterface;
use Joomla\Database\DatabaseInterface;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use WishboxCdekSDK2\CdekClientV2Interface;
use function defined;

// phpcs:disable PSR1.Files.SideEffects
defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects

/**
 * Service provider for the service MVC factory.
 *
 * @since  1.0.0
 */
class MVCFactory extends \Joomla\CMS\Extension\Service\Provider\MVCFactory implements ServiceProviderInterface
{
	/**
	 * The extension namespace
	 *
	 * @var  string
	 *
	 * @since   1.0.0
	 */
	private $namespace;

	/**
	 * MVCFactory constructor.
	 *
	 * @param   string  $namespace  The namespace
	 *
	 * @since   1.0.0
	 */
	public function __construct(string $namespace)
	{
		parent::__construct($namespace);

		$this->namespace = $namespace;
	}

	/**
	 * Registers the service provider with a DI container.
	 *
	 * @param   Container  $container  The DI container.
	 *
	 * @return  void
	 *
	 * @since   1.0.0
	 */
	public function register(Container $container): void
	{
		$container->set(
			MVCFactoryInterface::class,
			function (Container $container)
			{
				if (Factory::getApplication()->isClient('api'))
				{
					$factory = new ApiMVCFactory($this->namespace);
				}
				else
				{
					$factory = new \Joomla\Component\Wishboxcdek\Administrator\MVC\Factory\MVCFactory($this->namespace);
				}

				$factory->setFormFactory($container->get(FormFactoryInterface::class));
				$factory->setDispatcher($container->get(DispatcherInterface::class));
				$factory->setDatabase($container->get(DatabaseInterface::class));
				$factory->setSiteRouter($container->get(SiteRouter::class));
				$factory->setCacheControllerFactory($container->get(CacheControllerFactoryInterface::class));
				$factory->setUserFactory($container->get(UserFactoryInterface::class));
				$factory->setMailerFactory($container->get(MailerFactoryInterface::class));
				$factory->setCdekClientV2($container->get(CdekClientV2Interface::class));

				return $factory;
			}
		);
	}
}

Готово! Теперь в модели работает код $apiClient = $this->getCdekClientV2();.

Tags:
Hubs:
Total votes 1: ↑1 and ↓0+1
Comments2

Articles