← Назад к заметкам

Заметки

Повторный вызов обработчика CRM зациклился

Почему обработчик 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',
        ]
    );
}

Такой подход часто лучше простого флага, потому что он защищает не только от повторного входа, но и от лишних обновлений.

Источники