Двусторонняя синхронизация статусов CMS и CRM в 1C-Битрикс
Как связать статусы заказов и сделок в Битрикс без циклов: OnSaleOrderSaved, OnAfterCrmDealUpdate, маппинг статусов, проверка расхождений. Готовые примеры кода.
Требования
- 1C-Битрикс (коробочная версия с модулями Интернет-магазин и CRM)
- PHP 7.4+
- Понимание событий Bitrix (AddEventHandler, EventManager)
- Доступ к файловой системе сервера
Двусторонняя синхронизация статусов CMS и CRM в 1C-Битрикс
Проблема: заказы создаются в CMS, сделки ведутся в CRM, а бизнесу нужна синхронизация в обе стороны. Меняют статус в одном месте — он должен подтянуться в другом, без ручного дублирования и без зацикливания.
Решение: разделение на три сценария (CMS → CRM, CRM → CMS, оплата), проверка расхождений перед обновлением, использование корректных событий из документации. Готовые примеры кода для коробочной версии.
В чём проблема: почему нельзя делать «в лоб»
Если навесить обработчики на события без учёта источника изменения:
1. CMS меняет статус заказа → CRM обновляет сделку
2. CRM меняет стадию сделки → CMS обновляет заказ
3. 🔁 Возникает цикл: обновление тянет за собой следующее
4. Растёт нагрузка на БД, статусы «прыгают», данные перестают быть консистентными
Причина: обработчик не различает, кто инициировал изменение — пользователь или другой обработчик.
Решение: явно разделять сценарии и обновлять вторую сторону только когда значения реально различаются.
Архитектура решения: три сценария
Логику разносим на три независимых сценария:
Сценарий 1: CMS → CRM
- Триггер: сохранение заказа в CMS (
OnSaleOrderSaved) - Действие: обновить стадию сделки в CRM
- Защита от цикла: проверять, что статус заказа изменился
Сценарий 2: CRM → CMS
- Триггер: обновление сделки в CRM (
OnAfterCrmDealUpdate) - Действие: обновить статус заказа в CMS
- Защита от цикла: проверять, что стадя сделки изменилась
Сценарий 3: Оплата (приоритет)
- Триггер: смена статуса заказа на оплаченный
- Действие: сразу обновить сделку в CRM
- Особенность: без лишних проверок, минимальная задержка
Где размещать код
Коробка 1C-Битрикс: CMS и CRM на одном ядре
В типовой установке один сервер, одно ядро, но два приложения:
- CMS (сайт магазина) —
/ext_www/<domain>/public_html - CRM-портал —
/ext_www/crm.<domain>/
У каждого свой DOCUMENT_ROOT, поэтому код синхронизации разносим по двум местам.
Структура файлов
CMS (сайт):
/home/bitrix/ext_www/<domain>/public_html/local/php_interface/
├── init.php
└── lib/
└── SyncCmsOrderToDeal.php
CRM (портал):
/home/bitrix/ext_www/crm.<domain>/local/php_interface/
├── init.php
├── lib/
│ ├── SyncCrmDealToOrder.php
│ └── SyncCrmOrderToDeal.php
Рабочее решение: пошаговая реализация
Шаг 1: Подключение обработчиков в CMS
Файл /local/php_interface/init.php (CMS):
<?php
// Подключение синхронизации CMS → CRM
$path_order_to_deal = $_SERVER["DOCUMENT_ROOT"] . "/local/php_interface/lib/SyncCmsOrderToDeal.php";
if (file_exists($path_order_to_deal)) {
require_once $path_order_to_deal;
// Основной триггер — создание и сохранение заказа
\Bitrix\Main\EventManager::getInstance()->addEventHandler(
'sale',
'OnSaleOrderSaved',
[\Local\Sync\SyncCmsOrderToDeal::class, 'onOrderSaved']
);
}
Шаг 2: Подключение обработчиков в CRM
Файл /local/php_interface/init.php (CRM):
<?php
// Подключение синхронизации CRM ↔ CMS
$sync_deal_to_order = $_SERVER["DOCUMENT_ROOT"] . "/local/php_interface/lib/SyncCrmDealToOrder.php";
$path_order_to_deal = $_SERVER["DOCUMENT_ROOT"] . "/local/php_interface/lib/SyncCrmOrderToDeal.php";
if (file_exists($sync_deal_to_order)) {
require_once $sync_deal_to_order;
}
if (file_exists($path_order_to_deal)) {
require_once $path_order_to_deal;
}
// CRM → CMS (изменение стадии сделки)
AddEventHandler("crm", "OnAfterCrmDealUpdate", [\Local\Sync\SyncCrmDealToOrder::class, "onDealUpdate"]);
// CMS → CRM (изменение статуса заказа)
AddEventHandler("sale", "OnSaleStatusOrder", [\Local\Sync\SyncCrmOrderToDeal::class, "onOrderStatusChange"]);
Шаг 3: Обработчик CMS → CRM
Файл /local/php_interface/lib/SyncCmsOrderToDeal.php:
<?php
namespace Local\Sync;
use Bitrix\Main\Event;
use Bitrix\Main\EventResult;
class SyncCmsOrderToDeal
{
public static function onOrderSaved(Event $event): void
{
if (!\CModule::IncludeModule('crm')) {
return;
}
$order = $event->getParameter('ENTITY');
if (!$order || !method_exists($order, 'getId')) {
return;
}
$orderId = $order->getId();
$status = (string) $order->getField('STATUS_ID');
// Найти связанную сделку
$dealId = self::getLinkedDealId($orderId);
if (!$dealId) {
return;
}
// Смаппить статус заказа в стадию сделки
$targetStage = self::mapOrderStatusToDeal($status);
// Проверить текущую стадию (защита от цикла)
$currentStage = self::getDealStage($dealId);
if ($currentStage === $targetStage) {
return; // Не обновляем, если статусы совпадают
}
// Обновить сделку
self::updateDealStage($dealId, $targetStage);
}
private static function getLinkedDealId(int $orderId): ?int
{
$res = \CCrmDeal::GetList(
[],
['UF_CRM_ORDER_ID' => $orderId],
false,
['nTopCount' => 1],
['ID']
);
$row = $res ? $res->Fetch() : null;
return $row ? (int) $row['ID'] : null;
}
private static function getDealStage(int $dealId): ?string
{
$deal = \CCrmDeal::GetByID($dealId);
return $deal ? ($deal['STAGE_ID'] ?? null) : null;
}
private static function mapOrderStatusToDeal(string $orderStatus): string
{
$map = [
'N' => 'NEW', // Новый
'P' => 'PREPARATION', // Принят / в работе
'F' => 'WON', // Выполнен
'C' => 'LOSE', // Отменён
];
return $map[$orderStatus] ?? 'NEW';
}
private static function updateDealStage(int $dealId, string $stageId): void
{
$deal = new \CCrmDeal(false);
$deal->Update($dealId, ['STAGE_ID' => $stageId]);
}
}
Шаг 4: Обработчик CRM → CMS
Файл /local/php_interface/lib/SyncCrmDealToOrder.php:
<?php
namespace Local\Sync;
class SyncCrmDealToOrder
{
/**
* Обработчик OnAfterCrmDealUpdate: параметры ($id, &$arFields)
*/
public static function onDealUpdate($id, &$arFields): void
{
$id = (int) $id;
if ($id <= 0) {
return;
}
if (!\CModule::IncludeModule('sale')) {
return;
}
$deal = \CCrmDeal::GetByID($id);
if (!$deal) {
return;
}
// Найти связанный заказ
$orderId = (int) ($deal['UF_CRM_ORDER_ID'] ?? 0);
if ($orderId <= 0) {
return;
}
// Смаппить стадию сделки в статус заказа
$newStage = (string) ($deal['STAGE_ID'] ?? '');
$targetOrderStatus = self::mapDealStageToOrder($newStage);
// Проверить текущий статус (защита от цикла)
$currentStatus = self::getOrderStatus($orderId);
if ($currentStatus === $targetOrderStatus) {
return; // Не обновляем, если статусы совпадают
}
// Обновить заказ
self::updateOrderStatus($orderId, $targetOrderStatus);
}
private static function getOrderStatus(int $orderId): ?string
{
$order = \Bitrix\Sale\Order::load($orderId);
return $order ? $order->getField('STATUS_ID') : null;
}
private static function mapDealStageToOrder(string $stageId): string
{
$map = [
'NEW' => 'N',
'PREPARATION' => 'P',
'WON' => 'F',
'LOSE' => 'C',
];
return $map[$stageId] ?? 'N';
}
private static function updateOrderStatus(int $orderId, string $statusId): void
{
$order = \Bitrix\Sale\Order::load($orderId);
if (!$order) {
return;
}
$order->setField('STATUS_ID', $statusId);
$order->save();
}
}
Шаг 5: Обработчик смены статуса заказа в CRM (OnSaleStatusOrder)
Событие OnSaleStatusOrder вызывается после изменения статуса заказа. Параметры по документации Битрикс: $ID — идентификатор заказа, $val — новый идентификатор статуса. Обработчик размещается в CRM (там же, где висит событие).
Файл /local/php_interface/lib/SyncCrmOrderToDeal.php (на портале CRM):
<?php
namespace Local\Sync;
/**
* Синхронизация: смена статуса заказа (CMS) → обновление стадии сделки (CRM).
* Обработчик события OnSaleStatusOrder (параметры по документации: ID заказа, val — код статуса).
* @see https://dev.1c-bitrix.ru/api_help/sale/events/events_status_order.php
*/
class SyncCrmOrderToDeal
{
/**
* Вызывается после изменения статуса заказа. Параметры: $ID, $val (документация Sale).
* @param int $ID Идентификатор заказа
* @param string $val Новый идентификатор статуса (N, P, F, C и т.д.)
*/
public static function onOrderStatusChange($ID, $val): void
{
$orderId = (int) $ID;
$status = (string) $val;
if ($orderId <= 0) {
return;
}
if (!\CModule::IncludeModule('crm')) {
return;
}
$dealId = self::getLinkedDealId($orderId);
if (!$dealId) {
return;
}
$targetStage = self::mapOrderStatusToDeal($status);
$currentStage = self::getDealStage($dealId);
if ($currentStage === $targetStage) {
return;
}
self::updateDealStage($dealId, $targetStage);
}
private static function getLinkedDealId(int $orderId): ?int
{
$res = \CCrmDeal::GetList(
[],
['UF_CRM_ORDER_ID' => $orderId],
false,
['nTopCount' => 1],
['ID']
);
$row = $res ? $res->Fetch() : null;
return $row ? (int) $row['ID'] : null;
}
private static function getDealStage(int $dealId): ?string
{
$deal = \CCrmDeal::GetByID($dealId);
return $deal ? ($deal['STAGE_ID'] ?? null) : null;
}
private static function mapOrderStatusToDeal(string $orderStatus): string
{
$map = [
'N' => 'NEW',
'P' => 'PREPARATION',
'F' => 'WON',
'C' => 'LOSE',
];
return $map[$orderStatus] ?? 'NEW';
}
private static function updateDealStage(int $dealId, string $stageId): void
{
$deal = new \CCrmDeal(false);
$deal->Update($dealId, ['STAGE_ID' => $stageId]);
}
}
Важно: код поля связи сделки с заказом (UF_CRM_ORDER_ID) и коды стадий могут отличаться в вашей конфигурации — проверьте в настройках CRM (Настройки → Сделки → Поля сделки) и в документации CCrmDeal::GetList, Update.
Как связать заказ и сделку
Связь «заказ ↔ сделка» делается через пользовательские поля:
- У сделки — поле с ID заказа (часто
UF_CRM_ORDER_ID) - У заказа — свойство с ID сделки (опционально)
Получить ID связанной сделки по ID заказа
function getLinkedDealId(int $orderId): ?int
{
$res = \CCrmDeal::GetList(
[],
['UF_CRM_ORDER_ID' => $orderId],
false,
['nTopCount' => 1],
['ID', 'STAGE_ID']
);
$row = $res ? $res->Fetch() : null;
return $row ? (int) $row['ID'] : null;
}
Получить ID связанного заказа по ID сделки
function getLinkedOrderId(int $dealId): ?int
{
$deal = \CCrmDeal::GetByID($dealId);
if (!$deal) {
return null;
}
$orderId = $deal['UF_CRM_ORDER_ID'] ?? null;
return $orderId ? (int) $orderId : null;
}
Важно: код поля связи (UF_CRM_ORDER_ID) может отличаться в вашей конфигурации — смотрите свойства сделки в настройках CRM.
Маппинг статусов
Коды статусов заказа и коды стадий сделки не совпадают, поэтому нужны две функции перевода:
function mapOrderStatusToDeal(string $orderStatus): string
{
$mapping = [
'N' => 'NEW', // Новый
'P' => 'PREPARATION', // Принят / в работе
'F' => 'WON', // Выполнен
'C' => 'LOSE', // Отменён
];
return $mapping[$orderStatus] ?? 'NEW';
}
function mapDealStageToOrder(string $dealStage): string
{
$mapping = [
'NEW' => 'N',
'PREPARATION' => 'P',
'WON' => 'F',
'LOSE' => 'C',
];
return $mapping[$dealStage] ?? 'N';
}
Важно: маппинг должен быть симметричным — иначе часть переходов «потеряется».
Проверка результата
1. Проверка связи заказ-сделка
// В init.php временно включить логирование
define('LOCAL_SYNC_DEBUG', true);
// Создать тестовый заказ в CMS
// Проверить лог: /local/log/sync_status.log
2. Проверка синхронизации CMS → CRM
# 1. Изменить статус заказа в CMS
# 2. Открыть сделку в CRM
# 3. Проверить, что стадия обновилась
3. Проверка синхронизации CRM → CMS
# 1. Изменить стадию сделки в CRM
# 2. Открыть заказ в CMS
# 3. Проверить, что статус обновился
4. Проверка оплаты
# 1. Оплатить заказ в CMS
# 2. Проверить сделку в CRM (должна обновиться за доли секунды)
5. Проверка отсутствия циклов
# 1. Включить логирование
# 2. Изменить статус заказа
# 3. Проверить лог: должно быть 1-2 обновления, не больше
Типичные ошибки
❌ Ошибка: обработчик не срабатывает
Причина: неверное имя события или путь к файлу.
Решение:
// Проверить подключение в init.php
require_once $_SERVER["DOCUMENT_ROOT"] . "/local/php_interface/lib/SyncCmsOrderToDeal.php";
// Проверить имя события
AddEventHandler('sale', 'OnSaleOrderSaved', [...]); // Не OnSaleOrderSave!
❌ Ошибка: зацикливание обновлений
Причина: нет проверки расхождений перед обновлением.
Решение: всегда проверять текущее значение:
if ($currentStage === $targetStage) {
return; // Не обновлять, если статусы совпадают
}
❌ Ошибка: связь заказ-сделка не находится
Причина: неверный код поля связи (UF_CRM_ORDER_ID).
Решение: проверить в настройках CRM: Настройки → Настройки продукта → Сделки → Поля сделки.
❌ Ошибка: после обновления ядра синхронизация пропала
Причина: код лежит в /bitrix/ вместо /local/.
Решение: всегда размещать код в /local/php_interface/ — эта папка не затрагивается при обновлениях.
Отладка и логирование
Минимальный лог в обработчике
// В классе SyncCmsOrderToDeal
private static function log(string $message, array $context = []): void
{
if (!defined('LOCAL_SYNC_DEBUG') || !LOCAL_SYNC_DEBUG) {
return;
}
$line = date('Y-m-d H:i:s') . ' [Sync] ' . $message;
if ($context !== []) {
$line .= ' ' . json_encode($context, JSON_UNESCAPED_UNICODE);
}
file_put_contents(
$_SERVER['DOCUMENT_ROOT'] . '/local/log/sync_status.log',
$line . PHP_EOL,
FILE_APPEND | LOCK_EX
);
}
// Использование
self::log('updating deal', ['dealId' => $dealId, 'stage' => $targetStage]);
Включение отладки
// В init.php временно включить
define('LOCAL_SYNC_DEBUG', true);
// После отладки выключить
// define('LOCAL_SYNC_DEBUG', false);
Где применять
- Интернет-магазины на Битрикс: синхронизация заказов и сделок
- CRM-интеграции: двусторонний обмен статусами
- Автоматизация: оплата, отгрузка, уведомления
Связанные статьи:
- Структура проекта Bitrix: local folder
- Простой модуль Bitrix с компонентом
- Kint в Bitrix CMS: дебаг без боли
Документация:
- События, связанные с изменением состояния заказов (OnSaleStatusOrder)
- События модуля Sale (D7)
- CCrmDeal::GetList, GetByID, Update
- EventManager::addEventHandler



Комментарии