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