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

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

Описание

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

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

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

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

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

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

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

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

Подготовка

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