Обновление цен и остатков в 1С-Битрикс через Push & Pull

8 мин чтения
Иногда необходимо, чтобы цена и остаток товара в каталоге 1С-Битрикс обновлялись прямо на открытой странице — без перезагрузки и дополнительного действия со стороны пользователя.

Например, такая задача возникает, если:

  • 1С регулярно выгружает новые цены и остатки;
  • товары активно резервируются другими покупателями;
  • стоимость меняется автоматически;
  • страница каталога долго остаётся открытой;
  • каталог используется как рабочий B2B-интерфейс.
Обычный вариант — обновить страницу и получить новые данные с сервера. Но если пользователю важно постоянно видеть актуальное состояние каталога, удобнее передавать изменения в браузер автоматически.

Для этого в 1С-Битрикс можно использовать модуль Push & Pull.

Как работает Push & Pull

Общая схема выглядит следующим образом:


PHP → Push-сервер → WebSocket → JavaScript → DOM

Последовательность работы:

  1. На сервере изменяется цена или остаток товара.
  2. PHP отправляет Push & Pull событие.
  3. Сервер очередей передаёт событие подключённым браузерам.
  4. JavaScript получает событие.
  5. На странице изменяется цена или остаток нужного товара.
В отличие от периодических AJAX-запросов, браузеру не требуется каждые несколько секунд спрашивать сервер, изменились ли данные.

Событие отправляется только тогда, когда действительно произошло изменение.

Подключаем модуль Push & Pull

На серверной стороне первым делом подключаем необходимые модули:


use Bitrix\Main\Loader;

Loader::includeModule('pull');
Loader::includeModule('catalog');

Далее создадим две функции: одну для изменения цены, вторую — для изменения остатка.

Отправляем событие при изменении цены

Для публичного каталога можно отправлять событие в общий канал:


function sendPriceUpdateEvent(
    int $productId,
    float $price,
    string $currency = 'RUB'
): void
{
    \CPullStack::AddShared([
        'module_id' => 'catalog_live',
        'command' => 'priceChanged',
        'params' => [
            'productId' => $productId,
            'price' => $price,
            'currency' => $currency,
            'timestamp' => time(),
        ],
    ]);
}

В событие передаются:

  • ID товара;
  • новая цена;
  • валюта;
  • время формирования события.

Отправляем событие при изменении остатка

Аналогично создаём событие для количества товара:


function sendStockUpdateEvent(
    int $productId,
    float $quantity
): void
{
    \CPullStack::AddShared([
        'module_id' => 'catalog_live',
        'command' => 'stockChanged',
        'params' => [
            'productId' => $productId,
            'quantity' => $quantity,
            'timestamp' => time(),
        ],
    ]);
}

Теперь необходимо вызвать эти функции в момент изменения данных каталога.

Отслеживаем изменение цены

Обработчики можно зарегистрировать, например, в:


/local/php_interface/init.php

Подключаем EventManager:


use Bitrix\Main\EventManager;

$eventManager = EventManager::getInstance();

И регистрируем обработчик изменения цены:


$eventManager->addEventHandler(
    'catalog',
    'Bitrix\Catalog\Model\Price::OnAfterUpdate',
    static function ($event) {

        $id = (int)$event->getParameter('id');
        $fields = (array)$event->getParameter('fields');

        if (
            $id > 0
            && !empty($fields['PRODUCT_ID'])
            && array_key_exists('PRICE', $fields)
        ) {
            sendPriceUpdateEvent(
                (int)$fields['PRODUCT_ID'],
                (float)$fields['PRICE'],
                (string)($fields['CURRENCY'] ?? 'RUB')
            );
        }
    }
);

После обновления цены Push & Pull получит событие с новыми данными товара.

Отслеживаем изменение остатка

Для количества товара можно добавить отдельный обработчик:


$eventManager->addEventHandler(
    'catalog',
    'Bitrix\Catalog\Model\Product::OnAfterUpdate',
    static function ($event) {

        $id = (int)$event->getParameter('id');
        $fields = (array)$event->getParameter('fields');

        if (
            $id > 0
            && array_key_exists('QUANTITY', $fields)
        ) {
            sendStockUpdateEvent(
                $id,
                (float)$fields['QUANTITY']
            );
        }
    }
);

Если на проекте используется складской учёт и требуется обновлять остатки отдельных складов, обработчик необходимо адаптировать под используемую модель хранения остатков.

Подготавливаем HTML карточки товара

JavaScript должен понимать, какой элемент на странице относится к изменившемуся товару.

Проще всего добавить ID товара в data-атрибут контейнера:


<div
    class="product-card"
    data-product-id="<?=$arItem['ID']?>"
>

    <span class="product-price">
        <?=$arItem['PRICE']?> руб.
    </span>

    <span class="product-stock in-stock">
        В наличии: <?=$arItem['QUANTITY']?> шт.
    </span>

</div>

Теперь при получении события можно найти нужную карточку через значение data-product-id.

Подписываемся на изменение цены в JavaScript

В файле script.js шаблона компонента добавляем подписку:


BX.ready(function () {

    BX.PULL.subscribe({
        moduleId: 'catalog_live',
        command: 'priceChanged',

        callback: function (params) {

            var card = document.querySelector(
                '[data-product-id="' + params.productId + '"]'
            );

            if (!card) {
                return;
            }

            var priceElement = card.querySelector('.product-price');

            if (!priceElement) {
                return;
            }

            priceElement.textContent = new Intl.NumberFormat(
                'ru-RU',
                {
                    style: 'currency',
                    currency: params.currency || 'RUB'
                }
            ).format(params.price);

            priceElement.classList.add('price-updated');

            setTimeout(function () {
                priceElement.classList.remove('price-updated');
            }, 2000);
        }
    });

});

Когда сервер отправит команду priceChanged, JavaScript найдёт карточку товара и заменит цену непосредственно в DOM.

Подписываемся на изменение остатка

Добавляем вторую подписку:


BX.PULL.subscribe({
    moduleId: 'catalog_live',
    command: 'stockChanged',

    callback: function (params) {

        var card = document.querySelector(
            '[data-product-id="' + params.productId + '"]'
        );

        if (!card) {
            return;
        }

        var stockElement = card.querySelector('.product-stock');

        if (!stockElement) {
            return;
        }

        if (params.quantity > 0) {

            stockElement.textContent =
                'В наличии: ' + params.quantity + ' шт.';

            stockElement.className =
                'product-stock in-stock';

        } else {

            stockElement.textContent =
                'Нет в наличии';

            stockElement.className =
                'product-stock out-of-stock';
        }
    }
});

Теперь остаток товара также меняется без перезагрузки страницы.

Добавляем визуальную подсветку изменения

Чтобы пользователь заметил, что цена только что обновилась, можно кратковременно подсветить её.

Например:


.price-updated {
    animation: priceFlash 0.6s ease-in-out 3;
}

@keyframes priceFlash {

    0%,
    100% {
        background-color: transparent;
    }

    50% {
        background-color: #fff3cd;
    }
}

.product-stock.in-stock {
    color: #28a745;
}

.product-stock.out-of-stock {
    color: #dc3545;
}

После изменения цена несколько раз подсветится, а остаток получит соответствующий класс.

Если необходимо обновить карточку целиком

Иногда изменение цены влияет не только на одно поле.

Например, необходимо одновременно обновить:

  • цену;
  • старую цену;
  • размер скидки;
  • остаток;
  • доступность кнопки «Купить»;
  • торговое предложение;
  • другие параметры карточки.
В этом случае через Push & Pull можно передать только сигнал о том, что товар изменился, а актуальные данные получить отдельным AJAX-запросом.

Например:


BX.PULL.subscribe({
    moduleId: 'catalog_live',
    command: 'priceChanged',

    callback: function (params) {

        BX.ajax.runComponentAction(
            'vendor:catalog.product',
            'getProductData',
            {
                mode: 'class',
                data: {
                    productId: params.productId
                }
            }
        ).then(function (response) {

            var card = document.querySelector(
                '[data-product-id="' + params.productId + '"]'
            );

            if (!card) {
                return;
            }

            var price = card.querySelector('.product-price');
            var stock = card.querySelector('.product-stock');

            if (price) {
                price.textContent =
                    response.data.price + ' руб.';
            }

            if (stock) {
                stock.textContent =
                    response.data.quantity > 0
                        ? 'В наличии: '
                            + response.data.quantity
                            + ' шт.'
                        : 'Нет в наличии';
            }
        });
    }
});

Название компонента и action в этом примере условные — серверный action необходимо реализовать в своём компоненте.

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

Почему нельзя доверять Push-событию при оформлении заказа

Данные в интерфейсе обновляются практически в реальном времени, но Push & Pull не должен становиться источником истины для критических операций.

Например, при добавлении товара в корзину и оформлении заказа сервер всё равно должен повторно проверить:

  • текущую цену;
  • доступное количество;
  • скидки;
  • правила корзины;
  • доступность конкретного торгового предложения.
Если браузер потерял соединение или пропустил событие, пользователь может временно видеть устаревшее значение.

Интерфейс должен обновляться через Push & Pull, но окончательное решение всегда остаётся за сервером.

Публичные и персональные цены

В приведённом выше примере используется:


\CPullStack::AddShared()

Этот метод отправляет событие в общий канал.

Поэтому его можно использовать только для данных, которые допустимо передавать всем подключённым пользователям.

Например:

  • публичная цена товара;
  • общий остаток;
  • статус наличия.
Если на B2B-сайте у каждого клиента собственные цены, скидки или условия поставки, общий канал использовать нельзя.

В таком случае необходимо отправлять события конкретному пользователю или группе пользователей.

Например:


\Bitrix\Pull\Event::add(
    $userId,
    [
        'module_id' => 'catalog_live',
        'command' => 'priceChanged',
        'params' => [
            'productId' => $productId,
            'price' => $price,
            'currency' => $currency,
        ],
    ]
);

Композитный режим

Если на сайте используется композитный кеш, первоначальный HTML страницы может быть закеширован.

При этом JavaScript всё равно может изменить уже загруженный DOM, однако сами динамические данные каталога лучше проектировать с учётом композита.

Цена, остаток, персональные скидки и другие быстро меняющиеся значения обычно должны находиться в динамической части страницы.

Куда размещать код

Один из вариантов структуры проекта:

  • /local/php_interface/init.php — регистрация обработчиков;
  • /local/php_interface/pull_events.php — функции отправки Push & Pull событий;
  • script.js шаблона компонента — JS-подписки;
  • template.php — data-атрибуты товаров;
  • style.css — оформление и анимация обновлений.
Например, функции можно вынести в отдельный файл:


/local/php_interface/pull_events.php

а в init.php подключить его:


require_once(
    $_SERVER['DOCUMENT_ROOT']
    . '/local/php_interface/pull_events.php'
);

Так обработчики и вспомогательная логика не будут собраны в одном большом init.php.

Проверяем работу Push & Pull

После установки необходимо проверить всю цепочку.

  1. Открыть страницу каталога.
  2. Открыть консоль браузера.
  3. Проверить наличие объекта BX.PULL.
  4. Изменить цену товара в административной части.
  5. Проверить, обновилась ли цена на открытой странице.
  6. Изменить остаток.
  7. Проверить изменение количества без перезагрузки.
В консоли браузера можно проверить объект:


console.log(BX.PULL);

Если Push & Pull не работает, необходимо отдельно проверить сервер очередей и настройки модуля.

Если события не приходят

В первую очередь стоит проверить:

  • подключён ли модуль pull;
  • инициализирован ли BX.PULL на странице;
  • работает ли сервер очередей;
  • совпадают ли moduleId и command на PHP- и JS-стороне;
  • срабатывает ли серверный обработчик изменения товара;
  • есть ли на странице элемент с нужным data-product-id;
  • нет ли ошибок JavaScript в консоли.
Для отладки Push & Pull также полезно проверить состояние подключения средствами самого модуля.

Где можно применять обновление данных в реальном времени

Push & Pull подходит не только для обычного каталога товаров.

Его можно использовать для:

  • обновления остатков после обмена с 1С;
  • показа изменений цены во время акции;
  • B2B-каталогов;
  • мониторинга наличия товаров;
  • аукционов;
  • динамического ценообразования;
  • резервирования товаров;
  • отображения изменения статусов;
  • различных внутренних панелей и дашбордов.

Итог

Push & Pull позволяет обновлять данные каталога 1С-Битрикс без постоянных AJAX-запросов и без перезагрузки страницы.

Общий принцип достаточно простой:

  1. Отслеживаем изменение данных на сервере.
  2. Отправляем Push & Pull событие.
  3. Подписываемся на него в браузере.
  4. Находим нужный товар по data-атрибуту.
  5. Обновляем необходимые элементы DOM.
В результате покупатель видит изменения практически сразу после того, как новая цена или остаток появились на сервере.
Вернуться к блогу