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

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

Описание

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

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

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

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

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

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

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

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

Подготовка

В данном разделе речь идёт о сервисах для 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-2021, «1С-Битрикс», 2021
Наверх