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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Привет, Хабр! Меня зовут Алексей Яриков, я ведущий разработчик в команде внешних сайтов НЛМК. Мы занимаемся разработкой и поддержкой веб-платформ компании на 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. Оно позволяет поддерживать актуальность цен на сайте, минимизируя риски ошибок. Разделение процесса на два агента — периодического для получения данных и разового для их обработки — повысило стабильность работы и упростило масштабирование системы. В результате компания получает надёжный и гибкий механизм обновления данных, который снижает вероятность потерь и повышает уровень удовлетворённости клиентов.

Источник: https://habr.com/ru/companies/nlmk/articles/913274/


Интересные статьи

Интересные статьи

Предположим вам надо написать десктопное приложение, где будет свое состояние с набором коллекций и других свойств. Объекты для отображения могут храниться древовидно, содержать различные свойства со ...
Решение проблемы при проведении практических работ в образовательной организации СПО.Привет Хабр! Наконец я решил высказаться о том, как я решил проблему организации практических работ в колледже где ...
Одни предприниматели стремятся к полной независимости своего дела, другие – встраиваются в денежный поток крупных игроков и зарабатывают миллионы на дополнениях для чужого продукта...
Я давно знаком с Битрикс24, ещё дольше с 1С-Битрикс и, конечно же, неоднократно имел дела с интернет-магазинами которые работают на нём. Да, конечно это дорого, долго, местами неуклюже...
Маркетплейс – это сервис от 1С-Битрикс, который позволяет разработчикам делиться своими решениями с широкой аудиторией, состоящей из клиентов и других разработчиков.