Как стать автором
Обновить
167.02
НЛМК ИТ
Группа НЛМК

Простая интеграция в CMS Bitrix из XML-файла на FTP-сервере с использованием агентов

Время на прочтение6 мин
Количество просмотров622

Привет, Хабр! Меня зовут Алексей Яриков, я ведущий разработчик в команде внешних сайтов НЛМК. Мы занимаемся разработкой и поддержкой веб-платформ компании на Bitrix, обеспечивая их стабильность, производительность и удобство для пользователей.

Актуальность данных является ключевым фактором для успешного функционирования различных аспектов бизнеса — от интернет-магазинов до сложных корпоративных систем. Информация может изменяться по ряду причин, таких как обновление данных поставщиками, изменения цен, актуализация описаний товаров или услуг и многое другое. Несвоевременное обновление данных может привести к ошибкам в обработке заказов, недостоверной информации для пользователей и возможным финансовым потерям.

Во многих наших проектах мы используем CMS Bitrix. Довольно часто требуются небольшие интеграции, позволяющие точно и оперативно предоставлять критически важную информацию. Для эффективного управления данными и синхронизации с внешними источниками важно настроить автоматизированный процесс, который будет регулярно обновлять информацию из внешних источников. Один из эффективных способов реализации такого решения — использование агентов Bitrix.

Обычно для автоматизации периодических задач на веб-сайтах используют Cron-задачи. Однако настройка и поддержка крон-задач могут требовать дополнительных усилий, особенно в условиях ограниченного доступа к серверной части хостинга. У Bitrix есть встроенный функционал агентов, который отлично подходит для мелких и простых интеграций, таких как обновление данных. Агенты Bitrix предоставляют готовый механизм автоматического выполнения задач, который легко настроить и поддерживать в пределах самой CMS без дополнительных внешних инструментов.

Проблема при интеграции XML-файлов с FTP

Сами по себе XML-файлы, содержащие актуальные данные, могут иметь значительный объем, включая информацию о тысячах товаров или других объектах. При подходе к задаче мы не знали ни размеров файлов, ни их количества. XML-файлы периодически выгружались в определенное место на FTP-сервер, могли быть изменены, использовались не только нашей интеграцией, и поэтому не удалялись. Далее выяснилось, что могли быть выгружены файлы с датой актуальности на несколько дней вперёд, которые загружать не нужно.

Настройка агентов Bitrix для интеграции XML

Для эффективного решения данной задачи используются два агента Bitrix, каждый из которых отвечает за свою часть процесса:

Периодический агент для регулярного опроса FTP-сервера и скачивания XML-файлов. Этот агент автоматически запускается через заданные интервалы времени и выполняет последовательность действий по подключению к FTP-серверу, проверке наличия новых файлов и их скачиванию.

Разовый агент для обработки скачанных XML-файлов и обновления данных в базе данных Bitrix. Этот агент ставится в очередь после успешного скачивания файла, запускается один раз и отвечает за процесс парсинга XML, обновления данных в системе, удаление скачанных обработанных файлов. Во время парсинга агент проверяет даты прайс-листов в файле и пропускает его, если дата ещё не наступила. Важно, чтобы агент не ставился в очередь повторно, если он уже там.

Для решения проблемы с настройками интеграции, управления и мониторинга ею было решено обернуть агенты в модуль. Вынесение параметров интеграции в настройки модуля позволило указать данные для подключения к FTP-серверу, вывести краткую информацию по последней запущенной интеграции и разместить дополнительные настройки для типов цен и прочее. Также настройки в таком формате позволят вносить коррективы в процесс импорта без изменения кода.

Осталось решить последнюю проблему — определить, какие файлы уже загружены, а какие ещё нет, поскольку список XML-файлов на FTP-сервере очищается редко. Было решено создать highload-блок, в котором хранится информация об импортируемых файлах. В частности, название файла, дату и время его модификации, дату импорта. Название файла и его дата модификации являются идентификатором файла. Хранение имени файла и даты его модификации в HL-блоке позволяет точно определить, были ли данные обновлены на FTP. Если файл с таким же именем, но с более поздней датой модификации появляется на сервере, он снова загружается и импортируется.

С учетом выше сказанного процесс импорта выглядит следующим образом:

Для мониторинга процесса импорта и отладки все этапы сопровождаются логированием с разными уровнями детализации. Уровень логирования указан в настройках модуля. Переключение уровня логирования позволяет в любой момент увидеть, что произошло с процессом, если что-то пошло не так.

Пример кода периодического агента

class PriceImport
{
    public static function importFiles(): string
    {
        $hasNewPrices = false;
        $origin = new DateTime();
 
        try {
            $hasNewPrices = (new PriceImporter())->execute();
        } catch (Throwable $e) {
            Logger::getInstance()->error(
                Loc::getMessage('PRICE_IMPORT_ERROR'),
                [
                    'MODULE_ID' => Options::getModuleId(),
                    'CODE'      => LoggerCodes::INTEGRATION_PROCESS_ERROR->name,
                    'ITEM_ID'   => __METHOD__,
                    'ERROR'     => $e->getMessage(),
                    'TRACE'     => current($e->getTrace())
                ]
            );
        } finally {
            Logger::getInstance()->notice(
                Loc::getMessage('PRICE_IMPORT_COMPLETED'),
                [
                    'MODULE_ID'    => Options::getModuleId(),
                    'CODE'         => LoggerCodes::INTEGRATION_PROCESS->name,
                    'ITEM_ID'      => __METHOD__,
                    'PROCESS_TIME' => $origin->diff(new DateTime())->format(Loc::getMessage('PRICE_IMPORT_PROCESS_TIME_FORMAT')),
                ]
            );
        }
 
        $updateAgent = sprintf('\\%s::updateElements();', PriceUpdate::class);
 
        if ($hasNewPrices && !Agent::exist($updateAgent)) {
            CAgent::AddAgent($updateAgent, Options::getModuleId(), 'Y', 3600);
        }
 
        return '\\' . __METHOD__ . '();';
    }
}

Пример кода одноразового агента

class PriceUpdate
{
    public static function updateElements(): void
    {
        $origin = new DateTime();
 
        try {
            (new PriceUpdater())->execute();
        } catch (Throwable $e) {
            Logger::getInstance()->error(
                Loc::getMessage('PRICE_UPDATE_ERROR'),
                [
                    'MODULE_ID' => Options::getModuleId(),
                    'CODE'      => LoggerCodes::INTEGRATION_PROCESS_ERROR->name,
                    'ITEM_ID'   => __METHOD__,
                    'ERROR'     => $e->getMessage(),
                    'TRACE'     => current($e->getTrace())
                ]
            );
        } finally {
            Logger::getInstance()->notice(
                Loc::getMessage('PRICE_UPDATE_COMPLETED'),
                [
                    'MODULE_ID'    => Options::getModuleId(),
                    'CODE'         => LoggerCodes::INTEGRATION_PROCESS->name,
                    'ITEM_ID'      => __METHOD__,
                    'PROCESS_TIME' => $origin->diff(new DateTime())->format(Loc::getMessage('PRICE_UPDATE_PROCESS_TIME_FORMAT')),
                ]
            );
        }
    }
}

Пример кода чтения файлов на FTP-сервере

protected function readExternalDir(): static
{
    try {
        $this->externalDirList = $this->ftp->dirList($this->externalPath);
    } catch (ArgumentNullException|SystemException $e) {
        $this->ftp->close();
        Logger::getInstance()->error(
            Loc::getMessage('PRICE_IMPORTER_CONNECTION_ERROR'),
            [
                'MODULE_ID' => ModuleOptions::getModuleId(),
                'CODE'      => LoggerCodes::INTEGRATION_FTP_ERROR->name,
                'ITEM_ID'   => __METHOD__,
                'ERROR'     => $e->getMessage(),
                'TRACE'     => current($e->getTrace())
            ]
        );
    }
 
    if (empty($this->externalDirList)) {
        $this->externalDirList = [];
        $this->ftp->close();
        Logger::getInstance()->error(
            Loc::getMessage('PRICE_IMPORTER_EXTERNAL_DIR_ERROR', ['#DIR#' => $this->externalPath]),
            [
                'MODULE_ID' => ModuleOptions::getModuleId(),
                'CODE'      => LoggerCodes::INTEGRATION_IMPORT_ERROR->name,
                'ITEM_ID'   => __METHOD__,
            ]
        );
    }
 
    return $this;
}

Пример кода получения файлов

protected function receiveFiles(): void
{
    foreach ($this->externalFilesInfo as $arInfo) {
        try {
            $success = $this->ftp->receive(
                $this->localPath . $arInfo['name'],
                $this->externalPath . $arInfo['name'],
            );
 
            if ($success) {
                $this->receivedFiles[] = $arInfo['name'];
            } else {
                Logger::getInstance()->error(
                    Loc::getMessage('PRICE_IMPORTER_SAVE_FILE_ERROR', ['#FILE#' => $arInfo['name']]),
                    [
                        'MODULE_ID'     => ModuleOptions::getModuleId(),
                        'CODE'          => LoggerCodes::INTEGRATION_IMPORT_ERROR->name,
                        'ITEM_ID'       => __METHOD__,
                        'EXTERNAL_FILE' => $this->externalPath . $arInfo['name'],
                    ]
                );
            }
        } catch (ArgumentNullException|SystemException $e) {
            Logger::getInstance()->error(
                Loc::getMessage('PRICE_IMPORTER_RECEIVE_FILE_ERROR'),
                [
                    'MODULE_ID'     => ModuleOptions::getModuleId(),
                    'CODE'          => LoggerCodes::INTEGRATION_FTP_ERROR->name,
                    'ITEM_ID'       => __METHOD__,
                    'EXTERNAL_FILE' => $this->externalPath . $arInfo['name'],
                    'ERROR'         => $e->getMessage(),
                    'TRACE'         => current($e->getTrace())
                ]
            );
        }
    }
 
    $this->ftp->close();
    Option::set(
        ModuleOptions::getModuleId(),
        'last_import_date',
        date(ModuleOptions::OPTIONS_DATETIME_FORMAT)
    );
    Logger::getInstance()->notice(
        Loc::getMessage('PRICE_IMPORTER_RECEIVE_SUCCESS'),
        [
            'MODULE_ID'      => ModuleOptions::getModuleId(),
            'CODE'           => LoggerCodes::ECOTECH_INTEGRATION_PROCESS->name,
            'ITEM_ID'        => __METHOD__,
            'RECEIVED_FILES' => $this->receivedFiles,
        ]
    );
}

Заключение

Таким образом, мы разработали простое в настройке и удобное в использовании решение для интеграции данных, основанное на штатных возможностях CMS Bitrix. Оно позволяет поддерживать актуальность цен на сайте, минимизируя риски ошибок. Разделение процесса на два агента — периодического для получения данных и разового для их обработки — повысило стабильность работы и упростило масштабирование системы. В результате компания получает надёжный и гибкий механизм обновления данных, который снижает вероятность потерь и повышает уровень удовлетворённости клиентов.

Теги:
Хабы:
+12
Комментарии5

Публикации

Информация

Сайт
nlmk.com
Дата регистрации
Дата основания
2013
Численность
свыше 10 000 человек
Местоположение
Россия