Документация для разработчиков
Темная тема

Подмена сервисов

Описание

Один из плюсов использования концепции сервисов в том, что экземпляр сервиса довольно легко подменить.

Подмена сервиса даёт разработчику высокий уровень контроля приложения без необходимости вмешиваться в исходный код.

Однако высокий уровень контроля требует тщательного подхода и высокого уровня понимания и ответственности за свои действия.

Компания Битрикс, как разработчик программного обеспечения, гарантирует обратную совместимость на уровне исходных кодов.

Однако поддержка обратной совместимости иногда является существенным препятствием в развитии продукта.

В связи с этим, подменяя исходные сервисы на свою реализацию, следует руководствоваться следующими правилами:

  • Внедряемые изменения не должны нарушать существующую логику приложения. Чем более сложные доработки внедряются с помощью подмены сервисов, тем сложнее будет их поддерживать в будущем.
  • Нежелательно вносить изменения в сигнатуру методов на уровне добавления новых аргументов.
  • Изменения должны быть внедрены таким образом, чтобы в любой момент времени от них было достаточно просто и безболезненно отказаться (с целью отладки / проверки гипотез).

Особенно актуальными эти правила являются на раннем этапе развития сервисов. Чем моложе код, тем активней он будет изменяться.

Подготовка

В данном разделе речь идёт о сервисах для CRM. Т.к. для подмены необходимо наличие исходного класса, произвести замену получится только после подключения модуля crm.

В идеальном случае следовало бы подписаться на событие, возникающее при подключении модуля crm, и в нем произвести замену.

Сейчас такого события нет, поэтому придется на каждый хит подключать модуль crm и делать подмену сервисов.


Все представленные ниже сниппеты кода являются только примерами. Не обязательно воспроизводить их полностью.

Файл /local/php_interface/include/crm_services.php

if (\Bitrix\Main\Loader::includeModule('crm'))
{
	// here we can use crm module api
}

Файл /local/php_interface/init.php

define('CRM_USE_CUSTOM_SERVICES', true);

if (defined('CRM_USE_CUSTOM_SERVICES') && CRM_USE_CUSTOM_SERVICES === true)
{
	$fileName = __DIR__ . '/include/crm_services.php';
	if (file_exists($fileName))
	{
		require_once ($fileName);
	}
}

Все примеры ниже будут касаться содержимого файла crm_services.php без учета первой ветки с условием.

Теперь отключить кастомизацию сервисов можно будет в любой момент, закомментировав объявление константы.


Ниже примеры с подменой сервисов сделаны через анонимные классы. Делать именно так не обязательно, лучше каждый наследник поместить в отдельный файл и подключать внутри crm_services.php.

Простой пример замены сервиса

В данном примере подменяется сервис локализации.

use Bitrix\Crm\Service;
use Bitrix\Main\DI;

$localization = new class extends Service\Localization {
	public function loadMessages(): array
	{
		$messages = parent::loadMessages();
		$messages['CRM_MY_NEW_COMMON_MESSAGE'] = 'Some Messages';
        
		return $messages;
	}
};

DI\ServiceLocator::getInstance()->addInstance(
	'crm.service.localization' ,
	$localization
);

В этом примере переопределенный сервис в явном виде помещается в DI\ServiceLocator, откуда Service\Contianer получит его.

В большинстве случаев этого будет недостаточно, и понадобится заменить Service\Container.

Подмена контейнера

Подменяем Service\Container.

use Bitrix\Crm\Service;
use Bitrix\Main\DI;

$container = new class extends Service\Container {
    
};

DI\ServiceLocator::getInstance()->addInstance('crm.service.container', $container);

Тут ничего сложного. Всё, как в предыдущем примере, но в DI\ServiceLocator помещается контейнер.

Теперь можно переопределить любой метод контейнера, вернув на запрос сервиса собственную реализацию.

Все дальнейшие примеры подразумевают, что у вас уже есть код замены контейнера. Ниже все примеры приведены так, будто ваша реализация контейнера находится в отдельном файле.

Подмена фабрики

Для выполнения сложных замен рано или поздно потребуется подменить фабрику Service\Factory.

При этом есть потребность заменить фабрику не для всех смарт-процессов, а только для одного.

Допустим, этот смарт-процесс был создан заранее, а его идентификатор занесен в константу:

define('SUPER_ENTITY_TYPE_ID', 150);

Ниже пример кода, когда фабрика заменяется только для смарт-процесса с этим идентификатором:

use Bitrix\Crm\Service;

class MyContainer extends Service\Container {
	public function getFactory(int $entityTypeId): ?Service\Factory
	{
		if (defined('SUPER_ENTITY_TYPE_ID') && $entityTypeId === SUPER_ENTITY_TYPE_ID)
		{
			$type = $this->getTypeByEntityTypeId($entityTypeId);
			$factory = new class($type) extends Service\Factory\Dynamic {
				// here some additional logic
			};
		return $factory;
		}
        
		return parent::getFactory($entityTypeId);
	}
};

Сделать поле только для чтения

Представим, что реализация фабрики для отдельного смарт-процесса находится в отдельном файле.

Теперь необходимо в этой фабрике добавить какую-то свою бизнес-логику.

Например, есть пользовательское поле с кодом UF_CRM_150_STRING, которое должно быть доступно только для чтения (оно будет меняться только через API).

use Bitrix\Crm\Service;

class MyFactory extends Service\Factory\Dynamic 
{
	public function getUserFieldsInfo(): array
	{
		$fields = parent::getUserFieldsInfo();
		$fields['UF_CRM_150_STRING']['ATTRIBUTES'][] = \CCrmFieldInfoAttr::Immutable;
        
		return $fields;
	}
}

Добавление атрибута \CCrmFieldInfoAttr::Immutable не позволяет изменять это поле через интерфейс пользователем.

По аналогии можно было бы добавить атрибуты:

  • \CCrmFieldInfoAttr::NotDisplayed - скроет поле из детальной карточки.
  • \CCrmFieldInfoAttr::Required - сделает поле обязательным независимо от настроек.

Подмена операции

Типичный кейс - вклиниться в выполнение операции над элементом.

Механизм событий позволяет вклиниться в процесс до сохранения и после сохранения изменений.

Добавление через фабрику дополнительных действий - аналог событий.

В старых сущностях старые обработчики событий реализованы через эти же дополнительные действия. Но будет лучше воспользоваться этим механизмом напрямую.


Смарт-процессы пока не поддерживают события аналогичные таким в старых сущностях.

Допустим, мы хотим логгировать удаление элементов смарт-процессов:

use Bitrix\Main\Result;
use Bitrix\Crm\Item;
use Bitrix\Crm\Service;
use Bitrix\Crm\Service\Operation;

class MyFactory extends Service\Factory\Dynamic 
{
	public function getDeleteOperation(Item $item, Service\Context $context = null): Operation\Delete
	{
		$operation = parent::getDeleteOperation($item, $context);
        
		return $operation->addAction(
			Operation::ACTION_AFTER_SAVE,
			new class extends Operation\Action {
				public function process(Item $item): Result
				{
					$userId = Service\Container::getInstance()->getContext()->getUserId();
					\AddMessage2Log(Json::encode([
						'userId' => $userId,
						'entityTypeId' => $item->getEntityTypeId(),
						'id' => $item->getId(),
					]));
                    
				return new Result();
				}
			}
		);
	}
}

Запретить менять стадию определенному пользователю

Чуть более сложный кейс. Допустим, мы хотим запретить переносить элемент со стадии D150_3:PREPARATION на стадию D150_3:CLIENT пользователю с идентификатором 222.

use Bitrix\Main\Error;
use Bitrix\Main\Result;
use Bitrix\Crm\Item;
use Bitrix\Crm\Service;
use Bitrix\Crm\Service\Operation;

class MyFactory extends Service\Factory\Dynamic 
{
	public function getUpdateOperation(Item $item, Context $context = null): Operation\Update
	{
		$operation = parent::getUpdateOperation($item, $context);
        
		return $operation->addAction(
			Operation::ACTION_BEFORE_SAVE,
			new class extends Operation\Action {
				public function process(Item $item): Result
				{
					$result = new Result();
					$userId = Service\Container::getInstance()->getContext()->getUserId();
                    
				if (
					$userId === 222
						&& $item->isChangedStageId()
						&& $item->getStageId() === 'D150_3:CLIENT'
						&& $item->remindActual(Item::FIELD_NAME_STAGE_ID) === 'D150_3:PREPARATION'
				)	{
					$result->addError(new Error('Change stage is prohibited'));
					}
                    
					return $result;
				}
			}
		);
	}
}

© «Битрикс», 2001-2024, «1С-Битрикс», 2024