Какие события стреляют при импорте 1С: полный список
Когда к нам приходят с задачей «нужно что-то сделать с товарами после выгрузки из 1С», первый вопрос — а какое событие ловить? Ошибка здесь стоит дорого: повесишь обработчик на не то событие — получишь либо пропуск данных, либо двойную работу, либо падение сессии. У нас в практике был случай, когда разработчик подписался на OnAfterIBlockElementUpdate для дозаполнения SEO-полей, а импорт 10 000 товаров вместо 20 минут шёл 4 часа,обработчик вызывался по 7 раз на каждый элемент из-за цепной реакции обновлений. Карта событий импорта,это буквально навигатор: зная, какой хук на каком этапе стреляет и с какими аргументами, вы перестаёте гадать и начинаете проектировать интеграцию осознанно. Ниже,полная матрица событий, которые мы регулярно используем в проектах с большим каталогом.
| Событие | Модуль | Когда стреляет | Аргументы | Можно отменить |
|---|---|---|---|---|
OnBeforeCatalogImport1C |
catalog | Перед началом сессии импорта каталога | $arParams (IBLOCK_ID, файлы, флаги) |
Да, return false |
OnSuccessCatalogImport1C |
catalog | После успешной загрузки всех данных сессии | $arParams |
Нет |
OnAfterCatalogImport1C |
catalog | Финал сессии (успех или ошибка) | $arParams |
Нет |
OnBeforeIBlockElementAdd |
iblock | Перед добавлением каждого нового товара/SKU | &$arFields |
Да, return false |
OnAfterIBlockElementAdd |
iblock | Сразу после добавления элемента | &$arFields |
Нет |
OnBeforeIBlockElementUpdate |
iblock | Перед обновлением существующего товара | &$arFields |
Да, return false |
OnAfterIBlockElementUpdate |
iblock | Сразу после обновления элемента | &$arFields |
Нет |
OnBeforeCatalogPriceUpdate |
catalog | Перед изменением цены товара | $id, &$arFields |
Да, return false |
OnCatalogStoreProductUpdate |
catalog | При изменении остатков на складе | $storeId, $productId, &$arFields |
Нет |
OnSuccessOfferAddImport1C |
catalog | После успешного добавления торгового предложения | $offerId, $arFields |
Нет |
OnSuccessOfferUpdateImport1C |
catalog | После успешного обновления торгового предложения | $offerId, $arFields,$oldFields |
Нет |
В таблице,только «рабочие лошадки», которые мы используем в каждом втором проекте с интеграцией 1С. Самые частые,это OnAfterIBlockElementUpdate и OnBeforeIBlockElementAdd: на них вешают дозаполнение SEO, генерацию URL, синхронизацию с поиском. OnAfterCatalogImport1C,для финальных действий: очистка кеша, отправка уведомлений, запуск агентов. Редкие, но ценные,OnBeforeCatalogPriceUpdate (если нужно подменить цену на лету) и OnCatalogStoreProductUpdate (логирование остатков). Обратите внимание: события OnBefore* позволяют отменить действие,верните false, и элемент не сохранится. Это удобно для валидации: например, заблокировать товар без артикула.
Где разместить обработчик событий: init.php, модуль или AGENT
Когда мы только разбирались с интеграцией 1С на одном из первых проектов, то по наивности регистрировали все обработчики прямо в init.php — и быстро упёрлись в проблему: файл разрастался до тысяч строк, а любая правка требовала перезагрузки всего ядра. Сейчас у нас в команде чёткое правило: место регистрации напрямую определяет, насколько стабильно будет работать импорт и сколько времени уйдёт на поддержку. Неудачно выбранный файл — и вы либо тонете в спагетти-коде, либо получаете таймауты из-за синхронных операций. Поэтому перед тем как писать AddEventHandler, стоит выбрать стратегию размещения под конкретную задачу.
Есть три основных варианта, и у каждого своя цена:
init.php(главный модуль),плюс: быстро, не требует создания модуля, подходит для 1–2 простых обработчиков. Минус: при росте количества хуков файл становится нечитаемым, любая ошибка валит весь сайт, невозможно обновлять через marketplace.- Собственный модуль,плюс: изолированная логика, можно подключать/отключать через админку, удобно версионировать, поддерживать и передавать между проектами. Минус: нужно оформлять структуру модуля, регистрировать его в системе, тратить время на файлы
install/index.phpиoptions.php. - AGENT с задержкой,плюс: не блокирует поток импорта, идеально для тяжёлых операций (синхронизация с CRM, генерация микроразметки, отправка уведомлений). Минус: нужен работающий cron, данные могут устареть за время ожидания, сложнее отладка.
runtime/(Bitrix D7),плюс: модульное размещение без формальной регистрации модуля, подходит для кастомных решений вlocal/. Минус: требует понимания автозагрузки namespace'ов, не все разработчики знакомы с D7-архитектурой.- Прямая вставка в
1c_exchange.php,плюс: полный контроль над потоком, можно прервать импорт до его начала. Минус: патч ядра, который слетает при обновлении платформы,категорически не рекомендуем.
Наша рекомендация по опыту десятков проектов: для простых кейсов,один обработчик на генерацию SEO-полей,смело используйте init.php. Если вы планируете 5+ обработчиков, работу с кешем, интеграцию с внешними системами,оформляйте собственный модуль. А для всего, что может подождать 30–60 секунд (отправка писем, логи, синхронизация), лучше ставить AGENT. Это разделение убережёт вас от ситуации, когда импорт из 1С встаёт колом из-за того, что обработчик ждёт ответа от внешнего API.
Жизненный цикл одной сессии импорта 1С
Когда к нам приходят с задачей «импорт из 1С работает, но нужно подправить данные на лету», мы первым делом просим показать лог сессии. Типичная картина: четыре последовательных GET-запроса к /bitrix/admin/1c_exchange.php с параметром mode — checkauth, init, file, import. Это и есть скелет жизненного цикла. На каждом шаге 1С передаёт управление Bitrix, а движок, в свою очередь, стреляет событиями, в которые можно вклиниться. checkauth проверяет логин/пароль — здесь событий нет, только HTTP-заголовки. init инициализирует сессию: создаётся временная директория, записываются параметры обмена. На этом этапе срабатывает OnBeforeCatalogImport1C,единственная точка входа до разбора данных. file,загрузка XML-файла на сервер; здесь событий тоже нет. А вот import,основная сцена: парсинг CommerceML, создание/обновление разделов, товаров, предложений, цен, остатков. Внутри этого шага последовательно стреляют десятки событий модулей catalog, iblock и sale,от OnBeforeIBlockElementAdd до OnAfterCatalogImport1C.
flowchart LR
A[checkauth] -->|"авторизация"| B[init]
B -->|"инициализация сессии"| C[file]
C -->|"загрузка XML"| D[import]
D -->|"парсинг CommerceML"| E[финал]
B -.->|"OnBeforeCatalogImport1C"| B1[(" ")]
D -.->|"OnBeforeIBlockElementAdd/Update"| D1[(" ")]
D -.->|"OnAfterIBlockElementAdd/Update"| D2[(" ")]
D -.->|"OnBeforeCatalogPriceUpdate"| D3[(" ")]
D -.->|"OnSuccessCatalogImport1C / OnAfterCatalogImport1C"| E
Разберём каждый этап подробнее. Init,единственное место, где можно остановить импорт до того, как он начал есть ресурсы. Мы вешаем сюда проверку: не запущена ли уже другая сессия (через файловый lock или Redis), и если да,возвращаем ошибку. Это спасает от race condition, когда 1С запускает два параллельных потока. Import,самая мясистая часть. Здесь XML разбирается по секциям: сначала разделы каталога (OnBeforeIBlockSectionAdd / OnAfterIBlockSectionAdd), потом товары (OnBeforeIBlockElementAdd / OnAfterIBlockElementAdd), затем предложения (SKU) с отдельными событиями OnSuccessOfferAddImport1C / OnSuccessOfferUpdateImport1C, и в конце,цены (OnBeforeCatalogPriceUpdate) с остатками (OnCatalogStoreProductUpdate). И вот где мы обычно вмешиваемся: на OnAfterIBlockElementUpdate подставляем SEO-поля, если 1С их не передала; на OnBeforeCatalogPriceUpdate округляем цены до нужной кратности; на OnSuccessOfferUpdateImport1C отправляем данные в Elasticsearch. Главное,помнить, что все эти события стреляют внутри одной транзакции: если ваш обработчик кинет исключение, откатится вся пачка (~50–100 элементов), а не один товар. Поэтому мы всегда оборачиваем логику в try/catch и логируем ошибки, но не пробрасываем их наружу.
Как добавить мета-теги к товару при импорте 1С
Типичный сценарий: у вас в инфоблоке товаров есть SEO-поля META_TITLE, META_KEYWORDS и META_DESCRIPTION. Менеджеры вручную заполняют их через админку, а после очередного импорта из 1С все мета-теги обнуляются. Движок 1С-обмена не отличает «поле не передавалось в XML» от «передано пустое значение» — и просто затирает SEO-поля пустыми строками. В наших проектах с большим каталогом мы регулярно решаем эту задачу через событие OnAfterIBlockElementAdd и OnAfterIBlockElementUpdate: ловим момент сохранения элемента, проверяем, что SEO-поля пусты, и подставляем шаблонные значения. Это решение комбинируется с дозаполнением Open Graph-меток и автоматической генерацией микроразметки Schema.org.
// bitrix/php_interface/init.php
use Bitrix\Main\EventManager;
use Bitrix\Iblock\ElementTable;
$eventManager = EventManager::getInstance();
// Регистрируем обработчик на оба события — добавление и обновление элемента
foreach (['OnAfterIBlockElementAdd', 'OnAfterIBlockElementUpdate'] as $event) {
$eventManager->addEventHandler(
'iblock',
$event,
['SeoImportHandler', 'fillMetaTags']
);
}Когда применять этот хук
Этот подход оправдан, когда вы не хотите переписывать логику импорта на стороне 1С — проще подстраховаться на уровне Bitrix. Он особенно полезен, если в каталоге несколько сотен или тысяч товаров, и менеджеры физически не успевают проставлять SEO-поля вручную после каждой выгрузки. У нас такой хук работает в проектах с динамическими ценами, где импорт запускается каждые 15–30 минут, и ручное заполнение мета-тегов было бы узким местом. Единственное условие,у вас должно быть хотя бы одно свойство или поле, из которого можно сгенерировать мета-теги (например, NAME, PREVIEW_TEXT, или пользовательское свойство «SEO_TITLE»).
class SeoImportHandler
{
// ID инфоблока, для которого применяем автозаполнение
const IBLOCK_ID = 4; // замените на ваш ID
public static function fillMetaTags(int $elementId, array $arFields): void
{
// Проверяем, что это наш инфоблок
if ((int)($arFields['IBLOCK_ID'] ?? 0) !== self::IBLOCK_ID) {
return;
}
// Получаем текущие значения элемента
$element = ElementTable::getByPrimary($elementId, [
'select' => ['ID', 'NAME', 'PREVIEW_TEXT', 'META_TITLE', 'META_KEYWORDS', 'META_DESCRIPTION']
])->fetch();
if (!$element) {
return;
}
$updateFields = [];
// Если META_TITLE пуст — ставим NAME
if (empty($element['META_TITLE'])) {
$updateFields['META_TITLE'] = $element['NAME'];
}
// Если META_KEYWORDS пуст — генерируем из PREVIEW_TEXT (первые 200 символов)
if (empty($element['META_KEYWORDS']) && !empty($element['PREVIEW_TEXT'])) {
$keywords = strip_tags($element['PREVIEW_TEXT']);
$updateFields['META_KEYWORDS'] = mb_substr($keywords, 0, 200);
}
// Если META_DESCRIPTION пуст — используем PREVIEW_TEXT (первые 300 символов)
if (empty($element['META_DESCRIPTION']) && !empty($element['PREVIEW_TEXT'])) {
$description = strip_tags($element['PREVIEW_TEXT']);
$updateFields['META_DESCRIPTION'] = mb_substr($description, 0, 300);
}
// Если есть что обновлять — пишем без повторного вызова событий
if (!empty($updateFields)) {
$updateFields['ID'] = $elementId;
CIBlockElement::Update($elementId, $updateFields);
}
}
}Что под капотом: почему OnAfterIBlockElementAdd, а не OnBefore
Мы сознательно выбрали OnAfter, а не OnBefore. Дело в том, что на момент срабатывания OnBeforeIBlockElementAdd элемент ещё не сохранён в базу, и мы не можем прочитать его ID для последующего обновления. В OnAfter элемент уже создан,у нас есть $elementId, и мы можем безопасно дозаписать SEO-поля через CIBlockElement::Update. Единственный нюанс: при вызове CIBlockElement::Update внутри обработчика снова сработает OnAfterIBlockElementUpdate,это может привести к рекурсии. Чтобы этого избежать, мы проверяем в начале обработчика, что META_TITLE действительно пуст, иначе выходим. Так рекурсия не возникает, потому что после нашего обновления поле уже не пустое.
Грабли с транзакцией.Все события OnAfterIBlockElement* выполняются внутри транзакции импорта. Если в обработчике выбросить исключение (throw), откатится не один товар, а вся партия из 50–100 элементов. Поэтому мы всегда оборачиваем вызов CIBlockElement::Update в try/catch и логируем ошибки, но никогда не прерываем импорт. Исключение,критическая ситуация, когда несохранение SEO-полей делает каталог нерабочим; тогда можно выбросить исключение, но будьте готовы к полному откату партии.
Как отправить уведомление в Telegram об окончании импорта
Помню случай у клиента с каталогом на 35 тысяч SKU: импорт из 1С запускался каждые 20 минут по крону, занимал около 12 минут, и всё это время разработчики сидели в административной панели и вручную обновляли страницу, чтобы убедиться, что процесс не упал на середине. Когда импорт зависал на этапе загрузки изображений или падал с ошибкой парсинга XML, об этом узнавали только через час — когда менеджеры начинали жаловаться, что товары не обновляются. Мы тогда впервые задумались: почему бы не завязать уведомление о завершении импорта на Telegram-бота? Решение оказалось настолько простым, что теперь мы ставим его во всех проектах с активной выгрузкой из 1С. Оно не требует отдельного сервера для очередей, работает через стандартный curl и даёт мгновенную обратную связь команде: импорт завершился успешно, упал, или завис дольше обычного. В этой секции покажем, как повесить отправку сообщения в Telegram на событие OnAfterCatalogImport1C — и не сломать при этом сам импорт.
<?php
// init.php — регистрация обработчика на завершение импорта каталога
use Bitrix\Main\EventManager;
EventManager::getInstance()->addEventHandler(
'catalog',
'OnAfterCatalogImport1C',
function(array $arParams) {
$message = '✅ Импорт каталога завершён.';
$message .= PHP_EOL . 'Инфоблок: ' . ($arParams['IBLOCK_ID'] ?? 'не указан');
$message .= PHP_EOL . 'Время: ' . date('d.m.Y H:i:s');
$telegramToken = '1234567890:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw'; // замените на ваш токен
$chatId = '-1001234567890'; // замените на ID чата или группы
$url = 'https://api.telegram.org/bot' . $telegramToken . '/sendMessage';
$data = [
'chat_id' => $chatId,
'text' => $message,
'parse_mode' => 'HTML',
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_exec($ch);
curl_close($ch);
}
);Как не завалить импорт отправкой
Синхронный curl внутри обработчика события,это первое, что приходит в голову, но на практике опасная штука. Если Telegram API в момент отправки недоступен, таймаут в 5 секунд задержит весь импорт. А если на 10 тысячах товаров событие OnAfterCatalogImport1C стреляет только один раз за сессию, то задержка кажется несущественной. Но представьте: у вас несколько параллельных сессий импорта (цены, остатки, предложения), и каждая висит на ожидании ответа от Telegram,импорт может растянуться на минуты. В наших проектах мы предпочитаем выносить отправку в агент, который выполняется сразу после завершения основного потока. Это снимает риск блокировки и не требует внешних очередей.
<?php
// init.php — асинхронная отправка через агента
use Bitrix\Main\EventManager;
use Bitrix\Main\Application;
EventManager::getInstance()->addEventHandler(
'catalog',
'OnAfterCatalogImport1C',
function(array $arParams) {
$agentName = '\Bitrix\Main\Application::getConnection()->queryExecute("
INSERT INTO b_agent (
MODULE_ID, NAME, ACTIVE, NEXT_EXEC, SORT, USER_ID, IS_PERIOD
) VALUES (
\'main\',
\'CTelegramNotifier::send("' . addslashes(json_encode($arParams)) . '")\',
\'Y\',
now(),
100,
0,
\'N\'
)
")';
// Альтернатива через CAgent (если не хотите прямого SQL)
// \CAgent::AddAgent(
// 'CTelegramNotifier::send(\'' . addslashes(json_encode($arParams)) . '\');',
// 'main',
// 'N',
// 0,
// '',
// 'Y',
// date('d.m.Y H:i:s'),
// 100
// );
}
);
// Класс для отправки (можно разместить в /local/php_interface/include/)
class CTelegramNotifier
{
public static function send(string $paramsJson): string
{
$arParams = json_decode($paramsJson, true);
if (!$arParams) {
return '';
}
$message = '✅ Импорт каталога завершён.' . PHP_EOL;
$message .= 'Инфоблок: ' . ($arParams['IBLOCK_ID'] ?? 'не указан') . PHP_EOL;
$message .= 'Время: ' . date('d.m.Y H:i:s');
$telegramToken = '1234567890:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw';
$chatId = '-1001234567890';
$url = 'https://api.telegram.org/bot' . $telegramToken . '/sendMessage';
$data = [
'chat_id' => $chatId,
'text' => $message,
'parse_mode' => 'HTML',
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_exec($ch);
curl_close($ch);
return ''; // пустая строка — агент не перезапускается
}
}Как очистить кеш каталога после импорта 1С
Реальная история: у одного из заказчиков с каталогом на 25 тысяч SKU после обновления цен из 1С на витрине ещё сутки висели старые цифры. Клиенты оформляли заказы по вчерашним ценам, менеджеры вручную пересчитывали счета — нервов потратили больше, чем на сам импорт. Корень проблемы был прост: Bitrix не чистит кеш каталога автоматически после завершения импорта 1С. Движок считает, что данные в кеше валидны, пока не истечёт их время жизни или пока кто-то явно не скажет «стереть». В проектах с активным импортом (каждые 15–30 минут) это превращается в хроническую болезнь: менеджеры видят в админке актуальные остатки, а на сайте — вчерашнюю картинку. Мы регулярно сталкиваемся с этим у клиентов, где 1С заливает не только товары, но и цены, остатки, свойства. Если не добавить принудительную очистку кеша в OnAfterCatalogImport1C, пользователи будут видеть устаревшие данные до следующего автоматического сброса кеша по cron или до ручной команды в админке. Решение,повесить обработчик на финальное событие импорта и инвалидировать кеш каталога сразу после того, как 1С закончила выгрузку.
<?php
// init.php
use Bitrix\Main\EventManager;
use Bitrix\Main\Data\Cache;
EventManager::getInstance()->addEventHandler(
'catalog',
'OnAfterCatalogImport1C',
function (array $arParams) {
// Очищаем кеш каталога после завершения импорта
$cache = Cache::createInstance();
$cache->cleanDir('/catalog');
// Дополнительно — кеш компонентов, которые используют catalog
$cache->cleanDir('/bitrix/catalog');
// Если используете композитный кеш
if (method_exists('\Bitrix\Main\Data\Cache', 'cleanDirUncached')) {
\Bitrix\Main\Data\Cache::cleanDirUncached('/catalog');
}
}
);Какой метод очистки выбрать: BXClearCache vs CCacheManager
В старых проектах до сих пор встречается BXClearCache(true, '/catalog'),устаревшая функция, которая работает только с файловым кешем. В современных версиях Bitrix (22.x и новее) мы перешли на CCacheManager или его D7-аналог \Bitrix\Main\Data\Cache. Разница принципиальна: BXClearCache обходит только файловое хранилище, а CCacheManager::ClearByTag умеет чистить кеш независимо от бэкенда,будь то файлы, memcache, redis или APCu. В наших проектах с большим каталогом мы используем тегированный кеш: вешаем на каждый товар, раздел и торговое предложение тег catalog_IBLOCK_ID, а после импорта сбрасываем все теги каталога одной командой. Это надёжнее, чем чистить директории,не нужно гадать, какие пути закешированы. К тому же тегированный подход не трогает кеш других разделов сайта (новости, статьи), а значит, не вызывает лавинного перестроения всего кеша.
<?php
// init.php — очистка через CCacheManager с тегами
use Bitrix\Main\EventManager;
use Bitrix\Main\Application;
EventManager::getInstance()->addEventHandler(
'catalog',
'OnAfterCatalogImport1C',
function (array $arParams) {
$taggedCache = Application::getInstance()->getTaggedCache();
// Сбрасываем все теги, связанные с каталогом
$taggedCache->clearByTag('catalog');
// Если импортируется конкретный инфоблок — можно по его ID
if (!empty($arParams['IBLOCK_ID'])) {
$taggedCache->clearByTag('iblock_id_' . (int)$arParams['IBLOCK_ID']);
}
// Сбрасываем managed-кеш (композит, кеш компонентов)
\Bitrix\Main\Data\Cache::clearCache();
}
);Как заблокировать товар без артикула при импорте 1С
Самый частый запрос на эту тему — «при импорте из 1С проскочил товар без артикула, и он висит на витрине с пустым UF_BARCODE». В наших проектах с каталогом от 10 тысяч SKU такая ситуация возникает регулярно: менеджер 1С забыл заполнить поле «Артикул» в учётной системе, XML приезжает без кода, а Bitrix послушно создаёт элемент с пустым реквизитом. Потом это всплывает при выгрузке в маркетплейсы — Ozon или Wildberries просто не принимают товар без артикула. Мы предпочитаем решать задачу на входе: если UF_BARCODE не заполнен,не даём сохранить элемент вообще. Это чище, чем потом искать «пустышки» через админку и удалять вручную. Решение комбинируется с событием OnBeforeIBlockElementAdd,ловим момент создания товара, проверяем наличие артикула и возвращаем ошибку.
<?php
// init.php или bitrix/php_interface/init.php
use Bitrix\Main\EventManager;
use Bitrix\Main\Localization\Loc;
EventManager::getInstance()->addEventHandler(
'iblock',
'OnBeforeIBlockElementAdd',
['CustomImportHandler', 'blockProductWithoutBarcode']
);Как вернуть ошибку и отменить сохранение
Событие OnBeforeIBlockElementAdd позволяет отменить сохранение элемента, если вернуть false и установить сообщение об ошибке через CMain::ThrowException или метод $APPLICATION->ThrowException. Внутри обработчика мы проверяем, что свойство UF_BARCODE (или любое другое обязательное поле) не пустое. Если артикул отсутствует,прерываем сохранение. Ключевой момент: возвращать нужно именно false, а не null или пустой массив,Bitrix интерпретирует false как сигнал «не сохранять». Сообщение об ошибке попадёт в лог импорта, и 1С увидит, что элемент не прошёл валидацию.
class CustomImportHandler
{
/**
* Блокирует сохранение элемента, если не заполнен UF_BARCODE.
*
* @param array &$arFields Поля элемента
* @return bool
*/
public static function blockProductWithoutBarcode(array &$arFields): bool
{
// ID инфоблока товаров — замените на ваш
$targetIblockId = 17;
if ((int)$arFields['IBLOCK_ID'] !== $targetIblockId) {
return true;
}
// Проверяем свойство UF_BARCODE
$barcode = $arFields['PROPERTY_VALUES']['UF_BARCODE'] ?? null;
if (empty($barcode)) {
global $APPLICATION;
$APPLICATION->ThrowException(
'Товар не сохранён: не заполнен артикул (UF_BARCODE).'
);
return false;
}
return true;
}
}Как подменить название товара на лету при импорте
Типичный сценарий: у клиента с каталогом на 15 тысяч SKU артикулы хранятся отдельным свойством, а в названии товара они не отображаются. Менеджеры вручную проставляют артикул в NAME при создании карточки, но после очередного импорта из 1С названия сбрасываются — потому что 1С передаёт только «чистое» наименование. Или обратная ситуация: в 1С названия приходят транслитом (например, «Stul ofisnyj chernyj»), а на витрине нужны русские наименования. Когда к нам приходят с такой задачей, мы используем событие OnBeforeIBlockElementUpdate, чтобы подменить NAME на лету, не дожидаясь повторной выгрузки и не правя данные в самой 1С. Это же решение комбинируется с генерацией SEO-заголовков и добавлением артикула в мета-теги.
use Bitrix\Main\EventManager;
EventManager::getInstance()->addEventHandler(
'iblock',
'OnBeforeIBlockElementUpdate',
['CustomImportHandler', 'modifyElementName']
);Как изменить $arFields без потери данных
Ключевая особенность OnBeforeIBlockElementUpdate — обработчик получает $arFields по ссылке, и любые изменения в этом массиве применяются при сохранении элемента. Но есть нюанс: если вы напрямую присвоите $arFields['NAME'], вы рискуете потерять изменения, которые внесли другие обработчики до вас. В наших проектах мы всегда читаем текущее значение через CIBlockElement::GetByID, если нужно сохранить часть исходного названия, или используем переданное $arFields['NAME'], если заменяем его полностью. Второй важный момент,не забудьте проверить IBLOCK_ID: обработчик будет вызываться для всех инфоблоков, а не только для каталога товаров.
class CustomImportHandler
{
const IBLOCK_ID = 17; // замените на ваш ID инфоблока
public static function modifyElementName(&$arFields): void
{
if ((int)$arFields['IBLOCK_ID'] !== self::IBLOCK_ID) {
return;
}
// Если название не меняется — не трогаем
if (!isset($arFields['NAME'])) {
return;
}
// Читаем артикул из свойства
$propertyValues = \CIBlockElement::GetPropertyValues(
self::IBLOCK_ID,
['ID' => $arFields['ID']],
false,
['CODE' => 'ARTICUL']
)->Fetch();
$article = $propertyValues['ARTICUL'] ?? '';
if ($article) {
// Формируем новое название: "Артикул — Наименование"
$arFields['NAME'] = $article . ' — ' . $arFields['NAME'];
}
}
}Грабли: OnBeforeIBlockElementUpdate вызывается даже если ни одно поле элемента не изменилось,1С передаёт полный набор данных при каждом обновлении, и Bitrix триггерит событие независимо от того, были ли реальные изменения. Если ваш обработчик выполняет тяжёлую логику (запросы к внешним API, перестроение индексов), он будет отрабатывать на каждый товар при каждом импорте. Фильтруйте по разнице между $arFields и текущими данными в БД, либо используйте флаг в сессии, чтобы обработать элемент только один раз за сессию импорта.
Как синхронизировать товары с внешней CRM после импорта
Типичный сценарий: каталог на 50 тысяч SKU, импорт из 1С каждые 20 минут, а менеджеры работают в AmoCRM. После того как 1С обновила цену или остаток, нужно чтобы эти изменения улетели во внешнюю CRM — иначе менеджер видит в своей системе неактуальные данные и обещает клиенту товар, которого уже нет. У нас в практике это одна из самых частых задач после настройки базового импорта. Решение: ловить событие OnSuccessOfferUpdateImport1C (или OnAfterIBlockElementUpdate для простых товаров) и отправлять данные во внешнюю систему. Но есть нюанс: если делать это синхронно, прямо внутри обработчика, один сбой в AmoCRM положит всю партию импорта. Поэтому мы всегда комбинируем отправку с асинхронной очередью — об этом ниже.
// Регистрация в init.php
use Bitrix\Main\EventManager;
EventManager::getInstance()->addEventHandler(
'catalog',
'OnSuccessOfferUpdateImport1C',
['My\CRM\SyncHandler', 'handleOfferUpdate']
);Почему не стоит делать синхронно
Когда мы впервые реализовывали синхронизацию с RetailCRM на одном из проектов, то написали прямой HTTP-запрос внутри обработчика OnAfterIBlockElementUpdate. Всё работало, пока у CRM не лёг сервер,импорт встал намертво, потому что каждый вызов curl ждал таймаута 30 секунд. При 10 тысячах обновлённых товаров это давало 300 секунд простоя только на ожидании. И это ещё полбеды: если CRM ответила 500-й ошибкой, мы теряли всю партию импорта из-за исключения внутри транзакции. С тех пор мы вынесли все внешние вызовы в очередь,обработчик только кладёт задачу, а фоновый воркер разбирает её без давления на основной поток импорта.
// Отправка в очередь (Redis/Beanstalkd)
use My\Queue\Client;
use Bitrix\Main\Engine\CurrentUser;
$event->addEventHandler('catalog', 'OnSuccessOfferUpdateImport1C', function($offerId, $arFields) {
// Формируем минимальный набор данных для синхронизации
$job = [
'offer_id' => (int)$offerId,
'iblock_id' => (int)$arFields['IBLOCK_ID'],
'timestamp' => time(),
'user' => CurrentUser::get()->getLogin() ?: '1c_import'
];
// Кладём в очередь — не ждём ответа
Client::getInstance()->push('crm_sync', $job);
});Как измерить время каждого этапа импорта (аналитика)
Реальная история: у клиента с каталогом на 40 тысяч SKU импорт из 1С занимал почти три часа. Менеджеры жаловались, что цены на витрине обновляются раз в сутки, хотя 1С выгружала данные каждые 20 минут. Мы открыли лог и увидели, что сессия импорта не падает — она просто работает неприлично долго. Но где именно теряются минуты? Без замеров это гадание: может, парсинг XML тормозит, может, запись каждого товара в БД, а может, обработчики событий, которые накрутили за годы разработки. Мы регулярно сталкиваемся с такой задачей у клиентов с большим каталогом и выработали простой приём: обернуть каждый этап импорта в замер времени через microtime(true) на событиях OnBefore* / OnAfter*. Это не требует внешних сервисов — достаточно пары обработчиков в init.php и файла лога.
// init.php — регистрация измерителей времени для этапов импорта
use Bitrix\Main\EventManager;
$eventManager = EventManager::getInstance();
// Измеряем общее время сессии импорта каталога
$eventManager->addEventHandler('catalog', 'OnBeforeCatalogImport1C', function() {
$_SESSION['IMPORT_TIMER'] = [];
$_SESSION['IMPORT_TIMER']['session_start'] = microtime(true);
});
$eventManager->addEventHandler('catalog', 'OnAfterCatalogImport1C', function() {
if (!empty($_SESSION['IMPORT_TIMER']['session_start'])) {
$total = microtime(true) - $_SESSION['IMPORT_TIMER']['session_start'];
$log = date('Y-m-d H:i:s') . " | Сессия импорта: " . round($total, 4) . " сек\n";
file_put_contents($_SERVER['DOCUMENT_ROOT'] . '/upload/import_timers.log', $log, FILE_APPEND);
}
});
// Замер каждого добавления / обновления товара
$eventManager->addEventHandler('iblock', 'OnBeforeIBlockElementAdd', function(&$arFields) {
$_SESSION['IMPORT_TIMER']['element_add_start'] = microtime(true);
});
$eventManager->addEventHandler('iblock', 'OnAfterIBlockElementAdd', function(&$arFields) {
if (!empty($_SESSION['IMPORT_TIMER']['element_add_start'])) {
$elapsed = microtime(true) - $_SESSION['IMPORT_TIMER']['element_add_start'];
$log = date('Y-m-d H:i:s') . " | Добавление элемента ID={$arFields['ID']}: {$elapsed} сек\n";
file_put_contents($_SERVER['DOCUMENT_ROOT'] . '/upload/import_timers.log', $log, FILE_APPEND);
}
});Как логировать без утечки памяти
На первый взгляд всё просто: открыли файл, дописали строку, закрыли. Но при импорте 10 тысяч товаров file_put_contents с флагом FILE_APPEND будет дёргаться 10 тысяч раз,и каждый раз система открывает/закрывает файловый дескриптор. В наших проектах мы заметили, что на больших каталогах (50k+ SKU) это даёт прирост времени на 15–20% только на операции ввода-вывода. Лучше буферизировать замеры в памяти и сбрасывать раз в 100–200 записей,или вообще после завершения сессии. Второй важный момент: $_SESSION при импорте из 1С не всегда доступна (сессия может не стартовать), поэтому надёжнее использовать глобальный массив или статическое свойство класса.
// init.php — буферизированное логирование с микротаймами
class ImportTimer
{
private static array $buffer = [];
private static int $flushCount = 0;
public static function start(string $label): void
{
self::$buffer[$label . '_start'] = microtime(true);
}
public static function stop(string $label, ?int $elementId = null): void
{
$startKey = $label . '_start';
if (!isset(self::$buffer[$startKey])) {
return;
}
$elapsed = microtime(true) - self::$buffer[$startKey];
$log = date('Y-m-d H:i:s') . " | {$label}" . ($elementId ? " ID={$elementId}" : '') . ": " . round($elapsed, 4) . " сек\n";
self::$buffer['log'][] = $log;
unset(self::$buffer[$startKey]);
// Сбрасываем на диск каждые 200 замеров
self::$flushCount++;
if (self::$flushCount >= 200) {
self::flush();
}
}
public static function flush(): void
{
if (empty(self::$buffer['log'])) {
return;
}
file_put_contents(
$_SERVER['DOCUMENT_ROOT'] . '/upload/import_timers.log',
implode('', self::$buffer['log']),
FILE_APPEND
);
self::$buffer['log'] = [];
self::$flushCount = 0;
}
}
// Регистрация обработчиков с использованием класса
use Bitrix\Main\EventManager;
$eventManager = EventManager::getInstance();
$eventManager->addEventHandler('catalog', 'OnBeforeCatalogImport1C', function() {
ImportTimer::start('session');
});
$eventManager->addEventHandler('catalog', 'OnAfterCatalogImport1C', function() {
ImportTimer::stop('session');
ImportTimer::flush(); // гарантированно сбрасываем остатки
});
$eventManager->addEventHandler('iblock', 'OnBeforeIBlockElementUpdate', function(&$arFields) {
ImportTimer::start('element_update');
});
$eventManager->addEventHandler('iblock', 'OnAfterIBlockElementUpdate', function(&$arFields) {
ImportTimer::stop('element_update', (int)$arFields['ID']);
});Самые частые ошибки, которые ломают импорт
Помню случай у клиента с каталогом на 30 тысяч SKU: импорт из 1С стабильно зависал на втором часу, а в логах висела ошибка MySQL deadlock. Мы потратили неделю, чтобы выяснить — две параллельные сессии импорта пытались одновременно обновить один и тот же товар. Набили шишек и на других граблях: транзакции, которые откатывают всю партию из-за одного невалидного артикула, кеш, который не чистится сам, и события, которые стреляют по 10 тысяч раз за сессию. Собрали самые болезненные ошибки — с причинами и проверенными решениями.
| Грабля | Причина | Решение |
|---|---|---|
| Импорт зависает намертво | Тяжёлый обработчик внутри транзакции | Вынести логику в очередь (AGENT / RabbitMQ) |
| Товар сохранился без цены | OnAfterIBlockElementAdd сработал до загрузки цен | Подписаться на OnBeforeCatalogPriceUpdate с задержкой |
| Кеш не обновился сутки | Bitrix не чистит кеш каталога после импорта | Вызвать CCacheManager::ClearByTag('catalog') в OnAfterCatalogImport1C |
| Данные в CRM дублируются | Два параллельных импорта без блокировки | Мьютекс через Redis SETNX или flock |
| Ошибка «не хватает памяти» | Обработчики держат ссылки на $arFields | unset() в конце + gc_collect_cycles() |
Отдельно стоит разобрать проблему race condition при параллельных сессиях импорта. 1С может запускать несколько сессий одновременно,например, одна грузит каталог, другая цены, третья остатки. Если каждая из них в обработчике OnAfterIBlockElementUpdate пытается писать в одну и ту же внешнюю таблицу (скажем, в Elasticsearch или CRM), без блокировки данные поедут. Мы в своих проектах используем простой мьютекс на основе Redis SETNX или файлового лока через flock. Проверяем в OnBeforeCatalogImport1C: если ключ import_1c_lock уже занят,возвращаем false и прерываем сессию. Это гарантирует, что только один поток пишет в третью систему в любой момент времени. Вариант для проектов без Redis,file-based lock на /tmp/import_1c.lock с TTL 30 минут.
Как вынести логику обработчиков в очередь (архитектурный совет)
Помню случай у клиента в e-commerce с каталогом на 40 тысяч SKU: импорт из 1С стабильно падал по таймауту на этапе обработки OnAfterIBlockElementUpdate. Проблема была не в самом импорте — а в том, что внутри обработчика синхронно дёргалась внешняя CRM через REST API. Каждый запрос занимал 200–500 мс, и на 40 тысячах элементов это превращалось в часы ожидания. Решение лежало на поверхности: в обработчике нужно только класть задачу в очередь, а всю тяжёлую логику выполнять асинхронно, вне цикла импорта. Когда к нам приходят с такой задачей, мы рекомендуем Redis Streams или RabbitMQ — они надёжнее, чем таблица b_queue Битрикса, и не блокируют сессию 1С.
use Bitrix\Main\Application;
use Bitrix\Main\EventManager;
use Bitrix\Main\Diag\Debug;
// В init.php регистрируем лёгкий обработчик
EventManager::getInstance()->addEventHandler(
'iblock',
'OnAfterIBlockElementUpdate',
function (&$arFields) {
// Проверяем, что это наш инфоблок (замените ID на свой)
if ((int)$arFields['IBLOCK_ID'] !== 37) return;
// Формируем задачу: только ID элемента и тип операции
$job = [
'entity_type' => 'element',
'entity_id' => (int)$arFields['ID'],
'action' => 'sync_to_crm',
'timestamp' => time(),
];
// Кладём в Redis Streams через нативный клиент
$redis = new \Redis();
$redis->connect('127.0.0.1', 6379);
$redis->xAdd('import_jobs', '*', $job);
}
);Как настроить воркер
Сам по себе Redis Streams,это только очередь. Нужен воркер, который будет забирать задачи и выполнять реальную работу. В наших проектах мы запускаем отдельный PHP-скрипт через supervisor с бесконечным циклом: читаем новые сообщения из потока, выполняем логику (синхронизация с CRM, генерация микроразметки, отправка уведомлений) и подтверждаем удаление из очереди. Такой подход гарантирует, что даже при пиковой нагрузке импорт из 1С не зависнет,воркер просто накопит backlog и обработает его по мере возможности.
// worker.php — запускается через supervisor
declare(strict_types=1);
require_once $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php';
$redis = new \Redis();
$redis->connect('127.0.0.1', 6379);
$lastId = '0-0'; // начальная позиция — с начала
while (true) {
// Читаем новые сообщения (блокировка до 5 секунд)
$messages = $redis->xRead(['import_jobs' => $lastId], 1, 5000);
if (!$messages) {
continue;
}
foreach ($messages as $stream => $entries) {
foreach ($entries as $id => $job) {
try {
// Выполняем реальную логику (пример: синхронизация с CRM)
$entityId = (int)$job['entity_id'];
// ... здесь ваш код: вызов API, запись в БД и т.д.
// Подтверждаем обработку — удаляем из очереди
$redis->xAck('import_jobs', 'import_consumers', [$id]);
$redis->xDel('import_jobs', [$id]);
$lastId = $id; // сдвигаем указатель
} catch (\Throwable $e) {
// Логируем ошибку, но не удаляем — повторится при перезапуске
Debug::writeToFile(
$e->getMessage(),
'Worker error',
'/local/logs/import_worker.log'
);
}
}
}
}- • События OnBefore/OnAfterCatalogImport1C обрамляют весь сеанс импорта — идеально для общей очистки кеша и нотификаций. • OnAfterIBlockElementUpdate стреляет тысячи раз за импорт; тяжёлые обработчики лучше уносить в очередь. • Транзакции DB: throw в обработчике роняет всю партию, а не один элемент — используйте try/catch.