Заметки
Повторный вызов обработчика CRM зациклился
Почему обработчик CRM может повторно вызвать сам себя после обновления сущности и как остановить такой цикл.
Если обработчик CRM внутри события снова обновляет ту же сущность, событие может сработать повторно. Без защиты это легко превращается в цикл.
Проблема
Обработчик CRM может зациклиться, если после изменения сущности он сам снова изменяет эту же сущность. Например, событие сработало по сделке, а внутри обработчика снова выполняется обновление этой сделки.
Повторный вызов события — нормальная ситуация. Проблема появляется тогда, когда обработчик не проверяет, нужно ли ему выполнять обновление повторно.
Цепочка обычно такая:
- сделка изменилась;
- сработал обработчик;
- в обработчике обновили эту же сделку;
- из-за обновления снова сработал тот же обработчик;
- логика пошла по кругу.
Поэтому перед техническим обновлением нужно добавить условие остановки.
Чаще всего такое возникает в обработчиках событий CRM:
- после изменения сделки;
- после изменения лида;
- после изменения контакта или компании;
- после изменения дела;
- при автоматическом заполнении полей;
- при синхронизации данных между сущностями;
- при техническом обновлении карточки сразу после сохранения.
Если обработчик меняет ту же сущность, по которой был вызван, защита от повторного входа нужна сразу.
Защита
Обычно хватает трёх приёмов: статический флаг, сброс флага через finally
и проверка текущих данных перед обновлением.
Самый простой вариант — поставить флаг выполнения. Если обработчик уже запущен, повторный вход сразу останавливается.
<?php
use Bitrix\Main\Loader;
Loader::includeModule('crm');
/**
* Обрабатывает изменение сделки.
*/
function handleDealUpdate(array &$deal_fields): void
{
static $is_handler_running = false;
if ($is_handler_running) {
return;
}
$is_handler_running = true;
$deal_id = (int)($deal_fields['ID'] ?? 0);
if ($deal_id <= 0) {
$is_handler_running = false;
return;
}
updateDealMarker($deal_id);
$is_handler_running = false;
}
/**
* Обновляет служебный маркер сделки.
*/
function updateDealMarker(int $deal_id): void
{
$deal = new CCrmDeal(false);
$deal->Update(
$deal_id,
[
'UF_CRM_HANDLER_UPDATED' => 'Y',
]
);
} Такой флаг защищает от повторного входа в рамках одного выполнения скрипта.
try/finally
Если внутри обработчика может быть ошибка, флаг лучше сбрасывать через finally.
Иначе код может упасть до строки, где флаг возвращается обратно.
<?php
use Bitrix\Main\Loader;
Loader::includeModule('crm');
/**
* Обрабатывает изменение сделки с защитой от повторного входа.
*/
function handleDealUpdate(array &$deal_fields): void
{
static $is_handler_running = false;
if ($is_handler_running) {
return;
}
$is_handler_running = true;
try {
$deal_id = (int)($deal_fields['ID'] ?? 0);
if ($deal_id <= 0) {
return;
}
updateDealMarker($deal_id);
} finally {
$is_handler_running = false;
}
}
/**
* Обновляет служебный маркер сделки.
*/
function updateDealMarker(int $deal_id): void
{
$deal = new CCrmDeal(false);
$deal->Update(
$deal_id,
[
'UF_CRM_HANDLER_UPDATED' => 'Y',
]
);
} Этот вариант надёжнее простого флага, потому что флаг сбрасывается даже при ошибке.
Лучше не только ставить флаг, но и проверять, нужно ли вообще обновлять сущность. Если поле уже содержит нужное значение, повторное обновление не нужно.
<?php
use Bitrix\Main\Loader;
Loader::includeModule('crm');
/**
* Обрабатывает изменение сделки.
*/
function handleDealUpdate(array &$deal_fields): void
{
$deal_id = (int)($deal_fields['ID'] ?? 0);
if ($deal_id <= 0) {
return;
}
$deal_data = fetchDealData($deal_id);
if (hasActualMarker($deal_data)) {
return;
}
updateDealMarker($deal_id);
}
/**
* Получает данные сделки.
*/
function fetchDealData(int $deal_id): array
{
$deal_result = CCrmDeal::GetListEx(
[],
['ID' => $deal_id],
false,
false,
['ID', 'UF_CRM_HANDLER_UPDATED']
);
$deal_data = $deal_result->Fetch();
return is_array($deal_data) ? $deal_data : [];
}
/**
* Проверяет, что служебный маркер уже установлен.
*/
function hasActualMarker(array $deal_data): bool
{
return ($deal_data['UF_CRM_HANDLER_UPDATED'] ?? '') === 'Y';
}
/**
* Обновляет служебный маркер сделки.
*/
function updateDealMarker(int $deal_id): void
{
$deal = new CCrmDeal(false);
$deal->Update(
$deal_id,
[
'UF_CRM_HANDLER_UPDATED' => 'Y',
]
);
} Проверка данных часто полезнее одного флага: она убирает лишние обновления и делает поведение обработчика понятнее.