Полный цикл в digital

Изменение логики Смарт Процессов в crm Битрикс24

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

Любая работа будь то операция создания или получение списка элементов со смарт-процессами или новым API начинается с получения фабрики этого типа. Если вы не знакомы с паттернами проектирования, то рекомендую сначала почитать про фабрики.

Шаг 1. Подмена фабрики crm

Для подмены фабрики, создадим класс отвечающий за саму фабрику /local/modules/hmarketing.rest/lib/Factory/OrderFactory.php с содержимым:

/local/modules/hmarketing.rest/lib/Factory/OrderFactory.php<?php

namespace Hmarketing\Rest\Factory;

\Bitrix\Main\Loader::requireModule('crm');
class OrderFactory extends \Bitrix\Crm\Service\Factory\Dynamic
{

}

Шаг 2. Подмена контейнера crm

Все взаимодействие со смарт-процессами осуществляется через контейнер \Bitrix\Crm\Service\Container, получить который можно следующим кодом:

$container = \Bitrix\Crm\Service\Container::getInstance();;

Внутри себя метод getInstance() представляет не что иное, как обращение к Bitrix\Main\DI\ServiceLocator, подробнее в документации, если мы заглянем внутрь этого метода, то увидим получение crm.service.container сервиса:

public static function getInstance(): Container {
    return ServiceLocator::getInstance()->get('crm.service.container');
}

Воспользовавшись возможностями Bitrix\Main\DI\ServiceLocator мы можем подменить возвращаемый результат на своего наследника.

Определим контейнер в соответсвующее пространство имен, своего модуля, пусть это будет Hmarketing\Rest\Container. Создадим файл с нашим новым контейнером /local/modules/hmarketing.rest/lib/Container/OrderContainer.php:

/local/modules/hmarketing.rest/lib/Container/OrderContainer.php<?php

namespace Hmarketing\Rest\Container;

\Bitrix\Main\Loader::requireModule('crm');

class OrderContainer extends \Bitrix\Crm\Service\Container
{

}

Если сейчас мы попытаемся что-то сделать в CRM, ничего не произойдет. Мы создали класс-наследник, но он ничего не делает и нигде не участвует. Даже если мы впишем ему методы модуль CRM не будет его использовать.

Для того, что-бы система увидела новый класс, следующим шагом нужно подменить сервис, для подмены добавим следующий код:

/local/php_interface/init.php<?php

// подключаем кастомный модуль где лежат все файлы hmarketing.rest
\Bitrix\Main\Loader::includeModule('hmarketing.rest');

\Bitrix\Main\DI\ServiceLocator::getInstance()->addInstanceLazy('crm.service.container', [
    'className' => '\\Hmarketing\\Rest\\Container\\OrderContainer',
]);

Теперь, мы можем открыть php-консоль в административном интерфейсе, или выполнить код в любом файле с подключением пролога и эпилога:

test.php<?php

require_once($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/include/prolog_before.php");

\Bitrix\Main\Loader::IncludeModule('crm');

if (\Bitrix\Crm\Service\Container::getInstance() instanceof Hmarketing\Rest\Container\OrderContainer) {
    echo "Класс подменен";
} else {
    echo "Класс не подменен";
}

require_once($_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/epilog_after.php');

Мы увидим вывод Класс подменен, значит контейнер успешно подменили.

Теперь изменим контейнер чтобы он возвращал нашу фабрику. Согласно документации, для получения фабрики какой-то сущности, необходимо передать методу getFactory идентификатор сущности чью фабрику мы хотим получить. Именно этот метод нам нужно перекрыть в нашем контейнере.

Для начала необходимо перекрыть код самого метода, для этого добавим класс контейнера в файл /local/modules/hmarketing.rest/lib/Container/OrderContainer.php следующий код:

/local/modules/hmarketing.rest/lib/Container/OrderContainer.php<?php

namespace Hmarketing\Rest\Container;

\Bitrix\Main\Loader::requireModule('crm');

class OrderContainer extends \Bitrix\Crm\Service\Container
{
    public function getFactory(int $entityTypeId): ?\Bitrix\Crm\Service\Factory
    {
        // если убрать, будет подмена контейнера
        die("Overridden");
        // подключаем родительский метод и передаем в него ID
        return parent::getFactory($entityTypeId);
    }
}

Теперь когда мы выполним нижеследующий код в консоли или файле:

test.php<?php

require_once($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/include/prolog_before.php");

// смарт процесс с идентификатором 1036
$entityTypeId = 1036;

\Bitrix\Main\Loader::IncludeModule('crm');

\Bitrix\Crm\Service\Container::getInstance()->getFactory($entityTypeId);

require_once($_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/epilog_after.php');

Мы увидим на экране надпись Overridden:

Если мы удалим die("Overridden");, CRM продолжит функционировать в обычном режиме, но с наследуемыми классами. Реализовывать нужно по принципу нашел — подменяем, нет — пропускаем, другими словами изменения должны влиять только на наш код.

В перекрытом методе мы должны сделать следующие действия:

  1. Проверить что подменяем наш сервис, не наш подменять не нужно
  2. Проверить на наличие объекта, вдруг мы уже подменили и это повторное обращение
  3. Создать объект фабрики, запомнить его и вернуть

Полный текст метода getFactory с комментариями:

/local/modules/hmarketing.rest/lib/Container/OrderContainer.php<?php

namespace Hmarketing\Rest\Container;

\Bitrix\Main\Loader::requireModule('crm');

class OrderContainer extends \Bitrix\Crm\Service\Container
{
    // идентификатор типа смарт-процесса
    protected int $entityTypeId = 1036;

    public function getFactory(int $entityTypeId): ?\Bitrix\Crm\Service\Factory
    {
        // если наш тип, подменяем
        if ($entityTypeId == $this->entityTypeId) {
            // сгенерируем название сервиса
            $identifier = static::getIdentifierByClassName(static::$dynamicFactoriesClassName, [$entityTypeId]);

            // проверим, вдруг уже есть объект класса
            if (\Bitrix\Main\DI\ServiceLocator::getInstance()->has($identifier)) {
                return \Bitrix\Main\DI\ServiceLocator::getInstance()->get($identifier);
            }

            // если объекта нет, получим объект смарт-процесса
            $type = $this->getTypeByEntityTypeId($entityTypeId);
            if (!$type) {
                // не получилось, смарт-процесс удален
                return null;
            }

            // создадим фабрику, запомним ее
            $factory = new \Hmarketing\Rest\Factory\OrderFactory($type);
            \Bitrix\Main\DI\ServiceLocator::getInstance()->addInstance(
                $identifier,
                $factory
            );

            // вернем подмененную фабрику
            return $factory;
        }

        // если тип не наш, передаем в родительский метод и не подменяим фабрику
        return parent::getFactory($entityTypeId);
    }
}

Теперь когда мы выполнили нижеследующий код в консоли или файле:

test.php<?php

require_once($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/include/prolog_before.php");

// идентификатор типа смарт-процесса
$entityTypeId = 1036;

\Bitrix\Main\Loader::IncludeModule('crm');

$factory = \Bitrix\Crm\Service\Container::getInstance()->getFactory($entityTypeId);

if ( $factory instanceof Hmarketing\Rest\Factory\OrderFactory )
{
    echo "Класс подменен";
}
else
{
    echo "Класс не подменен";
}

require_once($_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/epilog_after.php');

Мы увидим на экране надпись Класс подменен, мы подменили фабрику через подмену контейнера.

Шаг 3. изменение логики

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

Запрет на редактирование свойства

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

В файле /local/modules/hmarketing.rest/lib/Factory/OrderFactory.php необходимо изменить метод getUserFieldsInfo, который согласно документации возвращает описание пользовательских полей, изменяим следующим образом:

/local/modules/hmarketing.rest/lib/Factory/OrderFactory.php<?php

namespace Hmarketing\Rest\Factory;

\Bitrix\Main\Loader::requireModule('crm');

class OrderFactory extends \Bitrix\Crm\Service\Factory\Dynamic
{
    public function getUserFieldsInfo(): array
    {
        $fields = parent::getUserFieldsInfo();
        $fields['UF_CRM_3_1766692211591']['ATTRIBUTES'][] = \CCrmFieldInfoAttr::Immutable;

        return $fields;
    }
}

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

Подмена операции редактирования

Обычные бизнес требования могут подразумевать различное поведение элементов в системе в зависимости от выполняемых действий над элементом. В старом ядре подобный механизм основывался на событийной модели. При работе со смарт-процессами подобные влияниям можно осуществить через действия Action, аналог событий.

Чтобы добавить свою логику, вам нужно:

  1. В фабрике переопределить метод в зависимости от этапа на котором нужно поменять логику:
    1. getAddOperation для кастомизации при создании
    2. getUpdateOperation для кастомизации при обновлении
    3. getDeleteOperation для кастомизации при удалении
  2. Добавить свой Action в нужную операцию с помощью addAction(), указав этап выполнения ACTION_BEFORE_SAVE или ACTION_AFTER_SAVE

Метод addAction() принимает два ключевых параметра:

  • Operation::ACTION_BEFORE_SAVE до сохранения элемента в БД
  • Operation::ACTION_AFTER_SAVE после успешного сохранения элемента в БД

Из документации мы знаем, что любое действие является реализацией абстрактного класса \Bitrix\Crm\Service\Operation\Action. Создадим свое действие, для этого создадим класс OrderAction, который будет реализовывать это действие:

/local/modules/hmarketing.rest/lib/Action/OrderAction.php<?php

namespace Hmarketing\Rest\Action;

class OrderAction extends \Bitrix\Crm\Service\Operation\Action
{
    public function process(\Bitrix\Crm\Item $item): \Bitrix\Main\Result
    {
        $result = new \Bitrix\Main\Result();

        // для примера, проверим созданное свойство сумма
        if ($item->get('UF_CRM_3_1766692211591') < 0)
        {
            $result->addError(new \Bitrix\Main\Error("Сумма заказа не может быть отрицательной!"));
        }

        // возвращаем результат операции
        return $result;
    }
}

Теперь когда мы реализовали действие, необходимо добавить его к операции редактирования getUpdateOperation. Для этого нам нужно расширить метод getUpdateOperation в нашей подмененной фабрике:

/local/modules/hmarketing.rest/lib/Factory/OrderFactory.php<?php

namespace Hmarketing\Rest\Factory;

\Bitrix\Main\Loader::requireModule('crm');

class OrderFactory extends \Bitrix\Crm\Service\Factory\Dynamic
{
    // переопределяем метод получения операции обновления
    public function getUpdateOperation(\Bitrix\Crm\Item $item, \Bitrix\Crm\Service\Context $context = null): \Bitrix\Crm\Service\Operation\Update
    {
        // 1. получаем стандартную операцию обновления
        $operation = parent::getUpdateOperation($item, $context);

        // 2. добавляем свой Action на этап "ПЕРЕД сохранением"
        $operation->addAction(
            \Bitrix\Crm\Service\Operation::ACTION_BEFORE_SAVE,
            new \Hmarketing\Rest\Action\OrderAction()
        );

        return $operation;
    }
}

Заполните форму уже сегодня!
Для начала сотрудничества необходимо заполнить заявку или заказать обратный звонок. В ответ получите коммерческое предложение, которое будет содержать индивидуальную стратегию с учетом требований и поставленных задач
Работаем по будням с 9:00 до 18:00. Заявки, отправленные в выходные, обрабатываем в первый рабочий день до 12:00.
Спасибо, ваш запрос принят и будет обработан!