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

Заметки

Повторный вызов обработчика 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',
        ]
    );
}

Проверка данных часто полезнее одного флага: она убирает лишние обновления и делает поведение обработчика понятнее.