Поведение CRM расширяют обработчиками событий и дополнительными действиями операций. Обработчик подходит для реакции на определенное событие модуля. Действие операции работает с объектом Item и может вернуть ошибку, которая остановит дальнейшее выполнение. Чтобы добавлять действие ко всем операциям определенного типа, расширьте его фабрику.
Перед обращением к классам CRM подключите модуль crm через Loader::includeModule(). Модель Container → Factory → Item → Operation описана в статье Схема работы CRM и основные объекты API.
Если объединяете несколько фрагментов в одном PHP-файле, соберите импорты use в начале файла без повторов. Код выполнения операции размещайте в отдельном серверном обработчике. В init.php добавляйте только регистрацию обработчиков или подмены фабрики.
Выбрать точку расширения
Область действия доработки зависит от способа подключения. Дополнительное действие принадлежит конкретному объекту операции, а обработчик события получает все соответствующие вызовы в тех PHP-запросах, где он зарегистрирован.
|
|| Задача | Точка расширения | Область действия ||
|| Проверить или изменить поля сделки до записи | OnBeforeCrmDealAdd, OnBeforeCrmDealUpdate | Вызовы, которые отправляют эти события ||
|| Отреагировать на изменение готового типа | Событие OnAfterCrm… конкретного типа | Вызовы соответствующего события CRM ||
|| Отреагировать на изменение смарт-процесса | Событие onCrmDynamicItem… | Динамические элементы с отбором по типу ||
|| Добавить проверку в собственный сценарий записи | Действие Operation::ACTION_BEFORE_SAVE | Только операция, которой добавлено действие ||
|| Выполнить действие после записи и получить состояние до сохранения | Действие Operation::ACTION_AFTER_SAVE | Только операция, которой добавлено действие ||
|| Применять правило при каждом получении операции через фабрику | Наследник фабрики и регистрация сервиса | Последующие обращения к подмененному сервису ||
|#
Для локального события используйте Bitrix\Main\EventManager. У классических событий аргументы передаются напрямую, например массив полей по ссылке. У событий динамических элементов обработчик получает Bitrix\Main\Event с именованными параметрами. Формат PHP-события не следует выводить из одноименного REST-события.
Сохраняйте элементы через операции фабрики. Прямая запись через ORM не запускает цепочку Operation, ее дополнительные действия и события, которые они отправляют. Фабрика заказа также не предоставляет обычные операции добавления, изменения и удаления элемента. Для заказа используйте профильный API CRM и модуля sale.
Обработать события готовых типов
У сделок, лидов, контактов, компаний и коммерческих предложений есть события до и после изменения. Через события сделки можно проверить поля перед сохранением или выполнить действие после записи. Для других типов используйте соответствующее имя, например OnBeforeCrmContactUpdate для контакта. Состав полей зависит от типа и способа вызова API.
Проверить данные перед сохранением сделки
События OnBeforeCrmDealAdd и OnBeforeCrmDealUpdate передают массив $fields по ссылке. Обработчик может изменить значения в этом массиве. Чтобы отменить действие, запишите пояснение в RESULT_MESSAGE и верните строго false.
При обновлении массив не следует считать полной карточкой. Проверяйте наличие нужного ключа через array_key_exists(). Отсутствие поля означает, что обработчик не получил его значение, а не то, что пользователь очистил поле. Для правила, которое зависит от нескольких текущих и изменяемых полей, используйте действие операции с объектом Item.
Пример. Обработчик удаляет пробелы по краям переданного названия сделки и отклоняет название, которое состоит только из пробелов. Разместите код регистрации в /local/php_interface/init.php, добавив его в существующий PHP-файл. Метод addEventHandlerCompatible() сохраняет формат аргументов классического события.
use Bitrix\Main\EventManager;
$normalizeDealTitle = static function (array &$fields): bool
{
if (!array_key_exists('TITLE', $fields))
{
return true;
}
$title = trim((string)$fields['TITLE']);
if ($title === '')
{
$fields['RESULT_MESSAGE'] = 'Укажите непустое название сделки';
return false;
}
$fields['TITLE'] = $title;
return true;
};
$eventManager = EventManager::getInstance();
$eventManager->addEventHandlerCompatible(
'crm',
'OnBeforeCrmDealAdd',
$normalizeDealTitle
);
$eventManager->addEventHandlerCompatible(
'crm',
'OnBeforeCrmDealUpdate',
$normalizeDealTitle
);
После успешного сохранения перечитайте название. Оно должно совпасть с результатом trim(). При отказе операция фабрики вернет ошибку в Result. Классический вызов CCrmDeal::Add() или CCrmDeal::Update() возвращает false и сохраняет сообщение в LAST_ERROR объекта API.
Меняйте переданный массив до сохранения. Повторный вызов Update() из этого обработчика снова запустит событие и может создать рекурсию. Возврат true продолжает обработку, но не гарантирует итогового успеха — следующая проверка или другой обработчик еще могут остановить операцию.
Отреагировать после записи или при удалении
События после записи подходят для реакции на выполненное изменение. Они не служат проверками, которые должны запретить сохранение.
|
|| Событие | Когда срабатывает | Аргумент обработчика | Как влияет возвращаемое значение ||
|| OnAfterCrmDealAdd | При обработке результата добавления сделки после записи | Массив полей с ID сделки | Не отменяет добавление ||
|| OnAfterCrmDealUpdate | При обработке результата изменения сделки после записи | Массив полей с ID сделки | Не отменяет изменение ||
|| OnBeforeCrmDealDelete | При обработке удаления до удаления сделки | ID сделки | Строго false останавливает удаление ||
|| OnAfterCrmDealDelete | При обработке удаления после удаления сделки | ID удаленной сделки | Не отменяет удаление ||
|#
Для этих событий также используйте addEventHandlerCompatible(). У обработчика добавления или обновления извлеките ID из $fields['ID'] и проверьте, что он положительный. Поля, необходимые для последующего действия, при необходимости прочитайте через фабрику. У обработчика удаления аргумент уже содержит ID, а загрузить удаленный элемент после операции нельзя.
Чтобы сообщить причину отказа в OnBeforeCrmDealDelete, вызовите $APPLICATION->ThrowException('Причина отказа') и верните false. Переменную $APPLICATION получите через global $APPLICATION. При удалении обработчик не получает массив полей, поэтому записать сообщение в RESULT_MESSAGE нельзя.
В операциях фабрики события OnAfterCrmDealAdd и OnAfterCrmDealUpdate входят в действия после сохранения. Они могут выполниться до последующей ошибки операции. Изменение массива в таком обработчике не сохраняет новые поля сделки.
Выбрать способ регистрации
Методы addEventHandler() и addEventHandlerCompatible() регистрируют обработчик в текущем PHP-запросе. Код в init.php выполняет такую регистрацию при подключении общего окружения сайта. Не добавляйте регистрацию внутри самого обработчика.
Для расширения в собственном модуле вынесите обработчик в автозагружаемый класс. В установщике вызовите registerEventHandlerCompatible() для классического события или registerEventHandler() для события с объектом Event. Передайте модуль-источник crm, имя события, ID своего модуля, класс и метод. При удалении модуля вызовите unRegisterEventHandler() с теми же данными. Подробности регистрации приведены в статье События.
Выберите один способ подключения обработчика. Регистрация одной логики через установщик и через init.php приведет к повторному выполнению. Для временной подписки сохраните идентификатор, который вернул addEventHandler() или addEventHandlerCompatible(), и передайте его в removeEventHandler() вместе с модулем и именем события.
Обработать событие смарт-процесса
Фабрика динамических элементов отправляет события после добавления, изменения и удаления. Обработчик получает объект Bitrix\Main\Event.
|
|| Событие | Когда срабатывает ||
|| onCrmDynamicItemAdd | При выполнении действий после добавления динамического элемента ||
|| onCrmDynamicItemUpdate | При выполнении действий после изменения динамического элемента ||
|| onCrmDynamicItemDelete | При выполнении действий после удаления динамического элемента ||
|#
Каждое событие содержит параметр item с объектом Bitrix\Crm\Item и параметр id с ID элемента. При удалении берите ID из параметра id. Отправитель сохраняет его из состояния до удаления, если текущий объект уже не возвращает ID.
После общего события фабрика отправляет событие для конкретного типа. Его имя состоит из общего имени, символа _ и entityTypeId, например onCrmDynamicItemUpdate_128. Число в примере нужно заменить ID типа из настроек своего смарт-процесса. Не используйте вместо него ID отдельного элемента или ID записи настроек типа. Различие идентификаторов объясняет статья Смарт-процессы.
Пример. Подпишитесь только на обновление элементов выбранного смарт-процесса. Код в /local/php_interface/init.php проверяет параметры и записывает тип и ID измененного элемента в журнал PHP. Содержимое полей в журнал не попадает.
use Bitrix\Crm\Item;
use Bitrix\Main\Event;
use Bitrix\Main\EventManager;
$entityTypeId = 128; // Замените идентификатором своего типа
EventManager::getInstance()->addEventHandler(
'crm',
'onCrmDynamicItemUpdate_' . $entityTypeId,
static function (Event $event) use ($entityTypeId): void
{
$item = $event->getParameter('item');
$itemId = (int)$event->getParameter('id');
if (!$item instanceof Item || $itemId <= 0)
{
return;
}
if ($item->getEntityTypeId() !== $entityTypeId)
{
return;
}
error_log('CRM item updated: ' . $entityTypeId . '/' . $itemId);
}
);
Функция error_log() пишет в журнал, выбранный конфигурацией PHP. После изменения элемента найдите запись с нужной парой «тип — ID». Обновление элемента другого типа не должно вызвать этот обработчик.
Если регистрируете обработчик в собственном модуле на каждом хите, разместите регистрацию в include.php модуля. Для постоянной подписки при установке модуля выберите способ регистрации.
События входят в действия после сохранения. Если эти действия отключены у операции или предыдущее действие вернуло ошибку, операция не отправляет событие.
Для нескольких типов можно подписаться на общее событие и отбирать нужные элементы по getEntityTypeId(). Не подключайте одно действие одновременно к общему и типовому событию, если оно должно выполниться один раз.
Отправитель этих событий не анализирует результаты обработчиков. Возврат false или EventResult::ERROR не отменит запись. Для обязательной проверки до сохранения добавьте действие операции. Не рассчитывайте на имя вроде onBeforeCrmDynamicItemUpdate как на симметричную пару — фабрика Dynamic не отправляет такое событие.
Добавить действие операции
Класс Bitrix\Crm\Service\Operation\Action задает метод process(Item $item): Result. Операция передает ему текущий элемент и контекст. Действие возвращает Bitrix\Main\Result, а для отказа добавляет в результат Bitrix\Main\Error.
Метод Operation::addAction($actionPlacement, $action, $sort = 500) добавляет действие и возвращает текущую операцию. Аргумент $actionPlacement задает момент выполнения, $action содержит экземпляр действия, $sort определяет порядок внутри этого момента. Меньшее значение выполняется раньше. Если значение уже занято, операция увеличивает его до свободного. Учитывайте действия, которые добавила сама фабрика.
|
|| Момент | Состояние элемента | Что означает ошибка действия ||
|| Operation::ACTION_BEFORE_SAVE | Включенные проверки операции уже пройдены, основная запись еще не выполнена | Останавливает операцию до основной записи ||
|| Operation::ACTION_AFTER_SAVE | Основная запись и предыдущие успешные этапы уже выполнены | Останавливает дальнейшую цепочку, но не откатывает выполненную запись ||
|#
Операция прекращает текущую цепочку на первом неуспешном результате действия. Для ожидаемого отказа возвращайте Result с ошибкой. Исключение не является заменой такого результата и может прервать вызывающий PHP-код.
Проверить и нормализовать название до записи
Действие до сохранения подходит, если проверке нужен объект Item, а не массив полей события. В базовой операции к этому моменту уже выполнены включенные проверки лимитов, прав, бизнес-процессов и полей. Изменяя поля внутри действия, самостоятельно проверьте допустимость новых значений. Повторного полного прохода этих проверок после ACTION_BEFORE_SAVE нет.
Если после подготовки полей операция обновления не обнаружила изменений, она завершается до выполнения действий. Не используйте действие как обработчик каждого нажатия кнопки сохранения.
Создайте файл /local/php_interface/lib/Crm/NormalizeDealTitle.php. Класс проверяет название и меняет только текущий объект в памяти, не вызывая вложенную операцию:
<?php
namespace Example\Crm;
use Bitrix\Crm\Item;
use Bitrix\Crm\Service\Operation\Action;
use Bitrix\Main\Error;
use Bitrix\Main\Result;
use CCrmOwnerType;
final class NormalizeDealTitle extends Action
{
public function process(Item $item): Result
{
$result = new Result();
if ($item->getEntityTypeId() !== CCrmOwnerType::Deal)
{
return $result;
}
$title = (string)$item->get(Item::FIELD_NAME_TITLE);
$normalizedTitle = trim($title);
if ($normalizedTitle === '')
{
$result->addError(new Error(
'Укажите непустое название сделки',
'EMPTY_DEAL_TITLE'
));
return $result;
}
if ($normalizedTitle !== $title)
{
$item->set(Item::FIELD_NAME_TITLE, $normalizedTitle);
}
return $result;
}
}
Это альтернативная реализация того же правила. Выберите одно решение — добавьте действие операции или проверьте данные в обработчике события. Иначе название будут проверять обе реализации.
Пример. Добавьте действие к одной операции обновления сделки. Код выполняется в доверенном серверном обработчике после пролога. Переменная $userId содержит положительный ID пользователя, от имени которого нужно выполнить изменение. Подставьте ID существующей сделки вместо 123. Модуль crm и файл класса подключаются до создания действия.
use Bitrix\Crm\Item;
use Bitrix\Crm\Service\Container;
use Bitrix\Crm\Service\Context;
use Bitrix\Crm\Service\Operation;
use Bitrix\Main\Loader;
use Example\Crm\NormalizeDealTitle;
if (!Loader::includeModule('crm'))
{
throw new RuntimeException('Не удалось подключить модуль CRM');
}
require_once $_SERVER['DOCUMENT_ROOT'] . '/local/php_interface/lib/Crm/NormalizeDealTitle.php';
$factory = Container::getInstance()->getFactory(CCrmOwnerType::Deal);
if ($factory === null)
{
throw new RuntimeException('Фабрика сделок недоступна');
}
$dealId = 123;
$item = $factory->getItem($dealId);
if ($item === null)
{
throw new RuntimeException('Сделка не найдена');
}
$item->set(Item::FIELD_NAME_TITLE, ' Название после проверки ');
$context = (new Context())->setUserId($userId);
$operation = $factory->getUpdateOperation($item, $context);
$operation->addAction(Operation::ACTION_BEFORE_SAVE, new NormalizeDealTitle());
$result = $operation->launch();
if (!$result->isSuccess())
{
throw new RuntimeException(implode('; ', $result->getErrorMessages()));
}
Операция проверяет право пользователя на изменение сделки. Метод getItem() сам не проверяет право чтения, поэтому не используйте этот фрагмент как готовый обработчик произвольного ID из HTTP-запроса. Правила чтения по пользовательскому запросу приведены в статье Работа с элементами CRM.
После успешного вызова перечитайте сделку. Название должно быть без пробелов по краям. Код EMPTY_DEAL_TITLE отличает отказ собственного действия от других ошибок. Если штатная проверка отклонит данные раньше, операция вернет ее ошибку, а действие не запустится.
Сравнить значения после сохранения
В действии ACTION_AFTER_SAVE метод getItemBeforeSave() возвращает копию элемента, которую операция сделала перед основной записью. Копия уже содержит подготовленные изменения. Чтобы получить исходное сохраненное значение поля, используйте remindActual(), а не обычный get() этой копии. Для нового элемента прежней записи нет, поэтому сравнение предназначено для обновления.
Пример. Добавьте действие к $operation из предыдущего примера перед единственным вызовом launch(). Оно запишет ID сделки в журнал PHP, только если сохранение изменило название. Здесь $operation — операция обновления, а Item и Operation уже импортированы.
use Bitrix\Main\Result;
$operation->addAction(
Operation::ACTION_AFTER_SAVE,
new class extends Operation\Action
{
public function process(Item $item): Result
{
$before = $this->getItemBeforeSave();
if ($before === null)
{
return new Result();
}
$oldTitle = (string)$before->remindActual(Item::FIELD_NAME_TITLE);
$newTitle = (string)$item->get(Item::FIELD_NAME_TITLE);
if ($oldTitle !== $newTitle)
{
error_log('CRM deal title changed: ' . $item->getId());
}
return new Result();
}
}
);
Метод getContext() внутри действия возвращает контекст текущей операции. Например, getContext()->getUserId() дает ID пользователя, от имени которого операция проверяет права. Не подменяйте его случайным текущим пользователем фонового процесса.
Изменение $item внутри действия после сохранения само по себе не записывает новые значения. Если нужно нормализовать поле текущего элемента, перенесите изменение в действие до сохранения. Если нужен отдельный последующий шаг, запускайте его с собственными условиями повторной обработки.
Подменить фабрику для общего правила
Действие, добавленное к одной операции в своем скрипте, не появится автоматически в других операциях CRM. Если правило должно действовать при получении операции обновления сделки через контейнер, расширьте Bitrix\Crm\Service\Factory\Deal.
Метод наследника должен вызвать parent::getUpdateOperation() и дополнить полученную операцию. Так сохраняются штатные действия фабрики, включая события. Создание новой пустой операции вместо вызова родителя потребовало бы самостоятельно воспроизвести их состав.
Создайте файл /local/php_interface/lib/Crm/DealFactory.php. Класс добавляет действие нормализации к операциям обновления:
<?php
namespace Example\Crm;
use Bitrix\Crm\Item;
use Bitrix\Crm\Service\Context;
use Bitrix\Crm\Service\Factory\Deal;
use Bitrix\Crm\Service\Operation;
final class DealFactory extends Deal
{
public function getUpdateOperation(Item $item, Context $context = null): Operation\Update
{
$operation = parent::getUpdateOperation($item, $context);
$operation->addAction(
Operation::ACTION_BEFORE_SAVE,
new NormalizeDealTitle()
);
return $operation;
}
}
Для сделок контейнер получает фабрику по ключу crm.service.factory.deal из сервис-локатора. Зарегистрируйте наследник до того, как код получит фабрику и создаст операции. Подключите модуль crm и оба файла классов в /local/php_interface/init.php:
use Bitrix\Main\DI\ServiceLocator;
use Bitrix\Main\Loader;
use Example\Crm\DealFactory;
if (Loader::includeModule('crm'))
{
require_once __DIR__ . '/lib/Crm/NormalizeDealTitle.php';
require_once __DIR__ . '/lib/Crm/DealFactory.php';
ServiceLocator::getInstance()->addInstance(
'crm.service.factory.deal',
new DealFactory()
);
}
Пример меняет только операции обновления сделок. Для добавления нужно отдельно расширить getAddOperation(), для удаления — getDeleteOperation(), сохранив сигнатуры и вызов родителя. Если используете подмену фабрики, уберите ручное добавление NormalizeDealTitle из собственного скрипта.
Подмена влияет на код, который получает этот сервис. Она не заменит фабрику в уже сохраненных переменных и не изменит созданные ранее операции. Прямое создание new Deal() обходит сервис-локатор. Классический API может использовать другой путь выполнения, поэтому проверяйте реальные точки записи, которые есть в вашем проекте.
Изменить поля одного смарт-процесса
Для собственного смарт-процесса расширьте Bitrix\Crm\Service\Factory\Dynamic. Его конструктор принимает объект настроек Type. Контейнер получает фабрику каждого динамического типа по отдельному ключу crm.service.factory.dynamic.<entityTypeId>, поэтому подмена одного ключа не меняет фабрики других типов.
Метод getFieldsSettings() задает описания системных полей. Вызовите метод родителя и дополните ATTRIBUTES нужного поля, сохранив остальные настройки. Для пользовательских полей используйте настройки пользовательского поля, а не добавляйте UF_* в этот массив.
Пример. Сделайте внешний идентификатор XML_ID обязательным при создании и неизменяемым после сохранения. Такое правило подходит для типа, элементы которого создает интеграция с заранее известным внешним идентификатором. До подключения правила заполните XML_ID у существующих элементов. Создайте файл /local/php_interface/lib/Crm/DynamicFactory.php.
<?php
namespace Example\Crm;
use Bitrix\Crm\Item;
use Bitrix\Crm\Service\Factory\Dynamic;
final class DynamicFactory extends Dynamic
{
public function getFieldsSettings(): array
{
$fields = parent::getFieldsSettings();
$fields[Item::FIELD_NAME_XML_ID]['ATTRIBUTES'][] = \CCrmFieldInfoAttr::Required;
$fields[Item::FIELD_NAME_XML_ID]['ATTRIBUTES'][] = \CCrmFieldInfoAttr::Immutable;
return $fields;
}
}
Зарегистрируйте фабрику в /local/php_interface/init.php до первого получения фабрики этого типа. Переменная $entityTypeId должна содержать идентификатор собственного смарт-процесса из настроек вашего модуля. Не подставляйте ID записи Type или служебного типа счетов и документов.
use Bitrix\Crm\Service\Container;
use Bitrix\Crm\Service\Factory\Dynamic;
use Bitrix\Main\DI\ServiceLocator;
use Bitrix\Main\Loader;
use Example\Crm\DynamicFactory;
if (Loader::includeModule('crm'))
{
require_once __DIR__ . '/lib/Crm/DynamicFactory.php';
$type = Container::getInstance()->getTypeByEntityTypeId($entityTypeId);
if ($type !== null)
{
$serviceId = Container::getIdentifierByClassName(Dynamic::class, [$entityTypeId]);
ServiceLocator::getInstance()->addInstance($serviceId, new DynamicFactory($type));
}
}
Если тип не найден, пример пропускает регистрацию. После подключения проверьте создание с пустым и заполненным XML_ID, попытку изменить его у существующего элемента и сохранение элемента другого типа. При штатных настройках операции пустой идентификатор вызовет ошибку проверки, а попытка изменить сохраненный идентификатор восстановит прежнее значение без отдельной ошибки. Признаки поля не проверяют уникальность XML_ID.
Выбирайте атрибут по требуемому результату.
|
|| Атрибут | Поведение ||
|| Required | Включает поле в проверку обязательных значений операции. Учитываются настройки проверки и доступность поля ||
|| ReadOnly | При обработке поля отменяет переданное изменение. У нового элемента подставляет значение по умолчанию. Редактор помечает поле как недоступное для изменения ||
|| Immutable | При обработке поля отменяет изменение у существующего элемента. При создании значение можно задать ||
|| NotDisplayed | Исключает поле из набора полей редактора карточки. Сам по себе не запрещает запись из серверного кода ||
|| Hidden | Контроллер элементов исключает поле из входных значений. Для скрытия поля в редакторе используется NotDisplayed ||
|#
Проверка обязательности и обработка атрибутов работают в операции. Не отключайте их в сценариях, где рассчитываете на эти правила. Подмена фабрики не защищает от прямой записи через ORM. Если изменение нужно отклонить с понятной пользователю ошибкой, используйте действие до сохранения вместо атрибута Immutable. При совместном использовании атрибут восстановит прежнее значение до запуска действия.
Когда нужен другой сервис или контейнер
Для одного типа достаточно подменить его фабрику. Если требуется изменить сам выбор фабрик или несколько общих сервисов CRM, рассмотрите наследник Bitrix\Crm\Service\Container. Метод Container::getInstance() получает его через ключ crm.service.container. Такая замена затрагивает больше вызовов, поэтому сначала определите, нельзя ли заменить один сервис.
Смарт-процесс использует фабрику Factory\Dynamic, которой нужен объект настроек типа. Контейнер хранит фабрики динамических типов отдельно. Счет SmartInvoice и специализированные типы используют своих наследников, поэтому не заменяйте все динамические фабрики одной реализацией. Устройство типов описано в статье Смарт-процессы.
Подмена одного сервисного ключа не объединяет расширения разных модулей. Метод addInstance() заменяет экземпляр по ключу, поэтому последняя регистрация может убрать предыдущую доработку. Согласуйте единую фабрику расширения и состав ее действий.
Отключить доработку и проверить обновление
Для отключения уберите регистрацию подмены и начните новый PHP-запрос. Уже полученные объекты в длительно работающем процессе останутся прежними — такой процесс нужно перезапустить. Файлы ядра менять не требуется.
После обновления CRM сверьте сигнатуры переопределенных методов и проверьте сценарии, которые расширяет фабрика. Сохраняйте вызов родителя, чтобы новые штатные действия попадали в операцию. Отдельно проверьте сохранение через карточку и собственные фоновые задания, права пользователя и результат отключения подмены.
Учесть связанные действия
Сохранение элемента — один из этапов операции. В базовой цепочке после него идут обработка полей, обновление служебных данных, статистика, история, таймлайн и действия ACTION_AFTER_SAVE. Далее операция обрабатывает уведомление интерфейса и при соответствующих условиях запускает бизнес-процессы и автоматизацию. Конкретный состав зависит от типа элемента и настроек операции.
Событие после сохранения не подтверждает завершение всей цепочки, фиксацию внешней транзакции или успешную доставку во внешнюю систему. Не отправляйте из него необратимое внешнее действие без механизма учета и повторов. Неуспешный Result тоже не доказывает, что запись отсутствует. Перед повтором проверьте сохраненные данные и незавершенные действия по статье Производительность и типовые ошибки CRM.
Настройте конкретную операцию до вызова launch(). В таблице перечислены методы, которые управляют событиями и связанными действиями.
|
|| Метод | Что отключает | Что нужно учесть ||
|| disableAutomation() | Запуск автоматизации этой операцией | Не отключает произвольные обработчики событий ||
|| disableBizProc() | Запуск бизнес-процессов этой операцией | Не равен отключению проверки уже работающих процессов ||
|| disableSaveToHistory() | Штатную запись операции в историю | Не отключает собственный журнал обработчика ||
|| disableSaveToTimeline() | Штатную запись операции в таймлайн | Не отменяет самостоятельные записи других обработчиков ||
|| disableBeforeSaveActions() | Всю группу дополнительных действий до сохранения | Может убрать проверки и события, которые добавила фабрика ||
|| disableAfterSaveActions() | Всю группу дополнительных действий после сохранения | Может убрать события, очистку кеша и другие действия фабрики ||
|#
В базовых настройках эти возможности включены. Настройки фабрики и вызывающий код могут изменить их. Чтобы отключить собственное действие, уберите его добавление к операции. Метод removeAction($actionPlacement, $actionClassName) удаляет действия указанного точного класса из выбранной группы. Он не снимает регистрацию обработчиков EventManager.
Не отключайте всю группу действий ради защиты от рекурсии одного обработчика. Ограничьте сам обработчик условием изменения нужного поля. Проверки доступа и полей сохраняйте включенными. Отключение автоматизации также не гарантирует, что другой обработчик не запустит отдельную операцию с собственными настройками.
Защитить обработчик от повторов
Разделяйте вложенный вызов в одном PHP-процессе и повтор события в другом запросе. Для них нужны разные меры.
-
Сначала проверьте, изменилось ли поле, от которого зависит действие. В обработчике до записи меняйте входные данные, а в действии до сохранения — текущий
Item. Это позволяет обойтись без повторной операции над тем же элементом. -
Если вложенное изменение неизбежно, ограничьте его парой «тип — ID» и признаком выполнения собственного шага. Защиту в памяти устанавливайте перед вложенным вызовом и снимайте в
finally, чтобы исключение не оставило блокировку до конца процесса. -
Для повторов между запросами храните устойчивый ключ задания и состояние обработки в постоянном хранилище. Один только ID элемента не различает несколько последовательных изменений. Добавьте вид действия и идентификатор изменения или версию задания.
-
Защитите захват задания от одновременного выполнения. Проверка «записи еще нет», за которой отдельно идет вставка, не исключает гонку двух процессов. Используйте уникальность ключа и атомарный переход состояния в своем хранилище.
-
Вынесите длительную обработку и сетевые обращения в фоновое задание. Обработчик выполняется синхронно с запросом CRM, поэтому ожидание внешнего сервиса задерживает сохранение. Фиксируйте задание так, чтобы можно было восстановить пропущенную обработку и проверить фактическое состояние элемента.
Статический флаг помогает остановить вложенный вызов только в текущем процессе. Он не защищает от повторного запроса, другого PHP-процесса или повторного запуска задания. Сопоставление записей и повторную доставку организуйте в сервисе интеграции. Обработчик CRM передает ему данные изменения, а сервис хранит результат и определяет, какой шаг нужно повторить. Для обмена с внешними ключами, подтверждением доставки и восстановлением после сбоя используйте статью Синхронизация CRM с внешней системой.
Проверить расширение
Перед включением общего правила проверьте его на тестовой установке. Выбирайте те способы записи, которыми пользуется ваш проект.
|
|| Проверка | Ожидаемый результат ||
|| Изменить название сделки с пробелами по краям | Сохранено нормализованное название ||
|| Передать название из пробелов | Запись отклонена до сохранения, вызывающий код получил сообщение ||
|| Изменить другое поле без передачи TITLE в классическое событие | Обработчик не принимает отсутствие TITLE за пустое название ||
|| Запустить обновление без изменений | Собственное действие не используется как обязательное подтверждение вызова ||
|| Изменить элемент без необходимых прав | Операция вернула отказ, собственная логика не обошла проверку доступа ||
|| Обновить смарт-процесс другого типа | Обработчик типового события не выполнил действие ||
|| Получить ошибку после записи | Перед повтором проверены фактические поля и уже выполненные действия ||
|| Повторить или одновременно запустить внешнее задание | Постоянный ключ и состояние обработки исключили повтор одного шага ||
|| Отключить подмену фабрики | В новом запросе контейнер вернул штатную фабрику ||
|#
Сопоставляйте результат launch(), сохраненные данные и результат собственного действия отдельно. Так можно различить отказ до записи, ошибку после сохранения и сбой последующей интеграции.