Подмена сервисов
Описание
Один из плюсов использования концепции сервисов в том, что экземпляр сервиса довольно легко подменить.
Подмена сервиса даёт разработчику высокий уровень контроля приложения без необходимости вмешиваться в исходный код.
Однако высокий уровень контроля требует тщательного подхода и высокого уровня понимания и ответственности за свои действия.
Компания Битрикс, как разработчик программного обеспечения, гарантирует обратную совместимость на уровне исходных кодов.
Однако поддержка обратной совместимости иногда является существенным препятствием в развитии продукта.
В связи с этим, подменяя исходные сервисы на свою реализацию, следует руководствоваться следующими правилами:
- Внедряемые изменения не должны нарушать существующую логику приложения. Чем более сложные доработки внедряются с помощью подмены сервисов, тем сложнее будет их поддерживать в будущем.
- Нежелательно вносить изменения в сигнатуру методов на уровне добавления новых аргументов.
- Изменения должны быть внедрены таким образом, чтобы в любой момент времени от них было достаточно просто и безболезненно отказаться (с целью отладки / проверки гипотез).
Особенно актуальными эти правила являются на раннем этапе развития сервисов. Чем моложе код, тем активней он будет изменяться.
Подготовка
В данном разделе речь идёт о сервисах для 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;
}
}
);
}
}