Изменение логики Смарт Процессов в 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 продолжит функционировать в обычном режиме, но с наследуемыми классами. Реализовывать нужно по принципу нашел — подменяем, нет — пропускаем, другими словами изменения должны влиять только на наш код.
В перекрытом методе мы должны сделать следующие действия:
- Проверить что подменяем наш сервис, не наш подменять не нужно
- Проверить на наличие объекта, вдруг мы уже подменили и это повторное обращение
- Создать объект фабрики, запомнить его и вернуть
Полный текст метода 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, аналог событий.
Чтобы добавить свою логику, вам нужно:
- В фабрике переопределить метод в зависимости от этапа на котором нужно поменять логику:
getAddOperationдля кастомизации при созданииgetUpdateOperationдля кастомизации при обновленииgetDeleteOperationдля кастомизации при удалении
- Добавить свой
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;
}
}

