Блог разработчика 1С-Битрикс

Все о классе Fuser в 1С-Битрикс: практическое руководство (теория + много кода)

\Bitrix\Sale\Fuser — служебный класс модуля sale (Интернет-магазин), который управляет идентификатором покупателя (FUSER_ID).
Этот идентификатор — «якорь» для корзины, отложенных товаров и любых данных, которые должны переживать перезагрузки страниц и даже гостевой режим.

Класс Fuser в 1С-Битрикс: практическое руководство

1) Что такое FUSER_ID и как он живёт

Цель: стабильно связать визит (гостя или пользователя) с корзиной.
Где хранится привязка:

  • Сессия (SALE_USER_ID) — быстрый доступ в пределах PHP-сессии.
  • Cookie (SALE_UID) — при включённых настройках, позволяет «узнавать» гостя между сессиями.
  • БД (b_sale_fuser) — строка с датами, кодом и, при наличии, USER_ID.

Создание/поиск FUSER:

  1. Ищем в сессии.
  2. Ищем в cookie (учёт кодированного значения при encode_fuser_id=Y).
  3. Ищем по текущему USER_ID (если авторизован).
  4. При необходимости создаём запись в b_sale_fuser (если вы не запретили создание).

Ключевые опции модуля sale (меняют поведение куки):

  • sale.encode_fuser_id = Y — в cookie хранится код, а не числовой ID.
  • sale.use_secure_cookies = Y — Secure-cookie (требует HTTPS).
  • sale.save_anonymous_fuser_cookie = Y — разрешить куки гостям при создании FUSER.
  • 2) Обзор методов \Bitrix\Sale\Fuser

    public static function getId(bool $skipCreate = false): ?int

    Вернёт текущий FUSER_ID.
    - skipCreate = false (по умолчанию) — создаст, если нет.
    - skipCreate = true — только попробует найти, иначе null.

    public static function refreshSessionCurrentId(): ?int

    Вернёт FUSER_ID из сессии, а если его нет — вызовет getId(true) (без создания).

    public static function getIdByUserId($userId)false|int

    Вернёт FUSER_ID для указанного USER_ID. Если записи нет — создаст и вернёт ID. В нештатных случаях может вернуть false — проверяйте.

    public static function getUserIdById($fuserId): int

    Вернёт USER_ID, связанный с FUSER, либо 0, если это «анонимный» FUSER.

    public static function getRegeneratedId(): ?int

    Только для авторизованных. Перегенерирует код FUSER, обновит cookie/сессию и вернёт FUSER_ID. Для гостей вернёт null.

    public static function add(array $options = []): \Bitrix\Sale\Result

    Явно создаёт FUSER.
    - $options['save'] = true — записать cookie:
    — для авторизованного пользователя — всегда;
    — для анонима — только если sale.save_anonymous_fuser_cookie=Y.

    public static function update(int $id, array $options = []): \Bitrix\Sale\Result

    Обновляет дату, при необходимости код/привязку, а также может сохранить cookie, но только если пользователь авторизован и ['save'=>true]. Для гостей update(..., ['save'=>true]) cookie не поставит — это важно.

    public static function deleteOld(int $days): void

    Удалит «старые» анонимные FUSER без корзины, старше $days.

    public static function handlerOnUserLogin($userId, array $params): void

    Вызов при успешной авторизации. Склеивает корзины (анонимную → пользовательскую), может обновить и сохранить cookie (см. ['update'=>true,'save'=>true]).

    public static function handlerOnUserLogout($userId): void

    Очистит сессию и cookie FUSER.

    3) Быстрый старт: минимальный рабочий пример

    Универсальный подсчёт количества (без завязки на версии Bitrix)

    <?php
    use Bitrix\Main\Loader;
    use Bitrix\Main\Context;
    use Bitrix\Sale\Fuser;
    use Bitrix\Sale\Basket;
    require($_SERVER['DOCUMENT_ROOT'].'/bitrix/modules/main/include/prolog_before.php');
    Loader::includeModule('sale');
    $siteId  = Context::getCurrent()->getSite();
    $fuserId = Fuser::getId(); // создастся при необходимости
    $basket = Basket::loadItemsForFUser($fuserId, $siteId);
    // ВАЖНО: getQuantityList() в большинстве версий возвращает [BASKET_ITEM_ID => QTY].
    // Надёжнее посчитать количество вручную:
    $totalQty = 0;
    foreach ($basket as $item) {
        $totalQty += $item->getQuantity(); // это метод у BasketItem
    }
    echo 'Товаров в корзине: ' . $totalQty;
        

    4) «Тихий режим»: проверить наличие корзины без создания

    <?php
    use Bitrix\Main\Loader;
    use Bitrix\Main\Context;
    use Bitrix\Sale\Fuser;
    use Bitrix\Sale\Basket;
    require($_SERVER['DOCUMENT_ROOT'].'/bitrix/modules/main/include/prolog_before.php');
    Loader::includeModule('sale');
    $siteId  = Context::getCurrent()->getSite();
    $fuserId = Fuser::getId(true); // не создаём
    if ($fuserId === null) {
        echo 'У пользователя ещё нет FUSER/корзины.';
        return;
    }
    $basket = Basket::loadItemsForFUser($fuserId, $siteId);
    // Общая сумма (учтите, что getPrice() может вернуть Decimal)
    $total = 0.0;
    foreach ($basket as $item) {
        $total += $item->getFinalPrice();
    }
    echo 'Есть корзина на сумму: ' . $total . ' руб.';
        

    5) Подгрузка корзины по FUSER_ID (D7) и вывод позиций

    <?php
    use Bitrix\Main\Loader;
    use Bitrix\Main\Context;
    use Bitrix\Sale\Fuser;
    use Bitrix\Sale\Basket;
    require($_SERVER['DOCUMENT_ROOT'].'/bitrix/modules/main/include/prolog_before.php');
    Loader::includeModule('sale');
    $siteId  = Context::getCurrent()->getSite();
    $fuserId = Fuser::getId();
    $basket = Basket::loadItemsForFUser($fuserId, $siteId);
    foreach ($basket as $item) {
        printf(
            "ID: %d | NAME: %s | QTY: %s | PRICE: %s\n",
            $item->getId(),
            $item->getField('NAME'),
            $item->getQuantity(),
            (string)$item->getPrice()
        );
    }
        

    6) Получить FUSER_ID по USER_ID и обратно

    <?php
    use Bitrix\Main\Loader;
    use Bitrix\Sale\Fuser;
    require($_SERVER['DOCUMENT_ROOT'].'/bitrix/modules/main/include/prolog_before.php');
    Loader::includeModule('sale');
    // FUSER_ID по USER_ID (создаст при необходимости):
    $userId  = 123;
    $fuserId = Fuser::getIdByUserId($userId);
    if ($fuserId === false) {
        echo 'Не удалось получить/создать FUSER для пользователя.';
        return;
    }
    // USER_ID по FUSER_ID:
    $backUserId = Fuser::getUserIdById($fuserId); // 0 — если FUSER «анонимный»
    echo "FUSER_ID: {$fuserId}, USER_ID: {$backUserId}";
        

    7) Склейка корзин при авторизации (кастомный логин)

    <?php
    use Bitrix\Main\Loader;
    use Bitrix\Sale\Fuser;
    require($_SERVER['DOCUMENT_ROOT'].'/bitrix/modules/main/include/prolog_before.php');
    Loader::includeModule('sale');
    // После кастомной авторизации:
    $userId = 123;
    Fuser::handlerOnUserLogin($userId, [
        'update' => true, // обновить запись FUSER (дата, привязка)
        'save'   => true, // сохранить cookie (для авторизованного)
    ]);
    // При выходе:
    Fuser::handlerOnUserLogout($userId);
        

    Хендлер внутри переносит позиции из «анонимной» корзины в пользовательскую (\CSaleBasket::TransferBasket(...)) и может удалить старый FUSER при успехе.

    8) Cookie и сессия: как правильно «сохранить» гостя

    Важно понимать:

    • Fuser::update($id, ['save'=>true]) запишет cookie только если пользователь авторизован.
    • Для гостя cookie ставится в момент создания FUSER через Fuser::add(['save'=>true]) и только при включённой опции sale.save_anonymous_fuser_cookie=Y.

    Пример: принудительно сохранить cookie (для авторизованного)

    <?php
    use Bitrix\Main\Loader;
    use Bitrix\Sale\Fuser;
    require($_SERVER['DOCUMENT_ROOT'].'/bitrix/modules/main/include/prolog_before.php');
    Loader::includeModule('sale');
    $fuserId = Fuser::getId();
    global $USER;
    if (is_object($USER) && $USER->IsAuthorized()) {
        Fuser::update($fuserId, ['save' => true]); // cookie запишется
    }
        

    Перегенерация кода FUSER (только для авторизованных)

    <?php
    use Bitrix\Main\Loader;
    use Bitrix\Sale\Fuser;
    require($_SERVER['DOCUMENT_ROOT'].'/bitrix/modules/main/include/prolog_before.php');
    Loader::includeModule('sale');
    $newFuserId = Fuser::getRegeneratedId(); // null для гостя
    if ($newFuserId !== null) {
        echo "Код FUSER перегенерирован. Новый ID: {$newFuserId}";
    } else {
        echo "Пользователь не авторизован — перегенерация невозможна.";
    }
        

    Обновить сессию без создания

    <?php
    use Bitrix\Main\Loader;
    use Bitrix\Sale\Fuser;
    require($_SERVER['DOCUMENT_ROOT'].'/bitrix/modules/main/include/prolog_before.php');
    Loader::includeModule('sale');
    $fuserId = Fuser::refreshSessionCurrentId(); // из сессии → поиск без создания
    echo $fuserId === null ? 'FUSER нет' : "FUSER_ID: {$fuserId}";
        

    9) AJAX-эндпоинт «сумма корзины» (минимум зависимостей)

    <?php
    // local/ajax/basket_total.php
    use Bitrix\Main\Loader;
    use Bitrix\Main\Context;
    use Bitrix\Main\Engine\Response\Json;
    use Bitrix\Sale\Fuser;
    use Bitrix\Sale\Basket;
    require($_SERVER['DOCUMENT_ROOT'].'/bitrix/modules/main/include/prolog_before.php');
    if (!Loader::includeModule('sale')) {
        (new Json(['error' => 'sale module required']))->send(); die();
    }
    $siteId  = Context::getCurrent()->getSite();
    $fuserId = Fuser::getId(); // создаст при необходимости
    $basket  = Basket::loadItemsForFUser($fuserId, $siteId);
    $total = 0.0; $count = 0;
    foreach ($basket as $item) {
        $count += $item->getQuantity();
        $total += $item->getFinalPrice();
    }
    (new Json([
        'fuserId'    => $fuserId,
        'itemsCount' => $count,
        'total'      => $total,
    ]))->send(); die();
        

    10) D7-контроллер API для фронта

    <?php
    // local/lib/Controller/BasketController.php
    namespace Local\Controller;
    use Bitrix\Main\Engine\Controller;
    use Bitrix\Main\Engine\ActionFilter;
    use Bitrix\Main\Context;
    use Bitrix\Main\Loader;
    use Bitrix\Sale\Fuser;
    use Bitrix\Sale\Basket;
    class BasketController extends Controller
    {
        public function configureActions(): array
        {
            return [
                'summary' => [
                    'prefilters' => [
                        new ActionFilter\HttpMethod([ActionFilter\HttpMethod::METHOD_GET]),
                        new ActionFilter\Csrf(false),
                    ],
                ],
            ];
        }
        public function summaryAction(): array
        {
            if (!Loader::includeModule('sale')) {
                return ['error' => 'sale module required'];
            }
            $siteId  = Context::getCurrent()->getSite();
            $fuserId = Fuser::getId(); // создастся при необходимости
            $basket  = Basket::loadItemsForFUser($fuserId, $siteId);
            $items = [];
            $sum = 0.0; $qty = 0;
            foreach ($basket as $item) {
                $itemSum = $item->getFinalPrice();
                $sum += $itemSum;
                $qty += $item->getQuantity();
                $items[] = [
                    'id'         => $item->getId(),
                    'productId'  => $item->getProductId(),
                    'name'       => $item->getField('NAME'),
                    'qty'        => $item->getQuantity(),
                    'price'      => (string)$item->getPrice(),
                    'sum'        => $itemSum,
                ];
            }
            return [
                'fuserId' => $fuserId,
                'qty'     => $qty,
                'sum'     => $sum,
                'items'   => $items,
            ];
        }
    }
        

    11) Добавление товара в корзину (надёжный D7-вариант)

    <?php
    use Bitrix\Main\Loader;
    use Bitrix\Main\Context;
    use Bitrix\Sale\Fuser;
    use Bitrix\Sale\Basket;
    use Bitrix\Sale\BasketItem;
    require($_SERVER['DOCUMENT_ROOT'].'/bitrix/modules/main/include/prolog_before.php');
    Loader::includeModule('sale');
    $siteId   = Context::getCurrent()->getSite();
    $fuserId  = Fuser::getId(); // создастся при первом добавлении
    $basket   = Basket::loadItemsForFUser($fuserId, $siteId);
    $productId = (int)($_REQUEST['PRODUCT_ID'] ?? 0);
    $qty       = max(1, (float)($_REQUEST['QTY'] ?? 1));
    if ($productId <= 0) { echo 'Неверный PRODUCT_ID'; die(); }
    // Ищем существующую позицию каталога
    $basketItem = $basket->getExistsItem('catalog', $productId);
    if ($basketItem) {
        $basketItem->setField('QUANTITY', $basketItem->getQuantity() + $qty);
    } else {
        /** @var BasketItem $basketItem */
        $basketItem = $basket->createItem('catalog', $productId);
        // Лучше setField по одному — меньше рисков
        $basketItem->setField('QUANTITY', $qty);
        $basketItem->setField('CURRENCY', \Bitrix\Currency\CurrencyManager::getBaseCurrency());
        $basketItem->setField('LID', $siteId);
        $basketItem->setField('PRODUCT_PROVIDER_CLASS', '\Bitrix\Catalog\Product\Basket');
        $basketItem->setField('NAME', 'Товар #'.$productId);
    }
    $result = $basket->save();
    echo $result->isSuccess()
        ? 'Добавлено. FUSER_ID: '.$fuserId
        : 'Ошибка: '.implode('; ', $result->getErrorMessages());
        

    12) Админ-утилита: «подменить» корзину под пользователя

    <?php
    use Bitrix\Main\Loader;
    use Bitrix\Main\Engine\CurrentUser;
    use Bitrix\Main\Context;
    use Bitrix\Sale\Fuser;
    use Bitrix\Sale\Basket;
    require($_SERVER['DOCUMENT_ROOT'].'/bitrix/modules/main/include/prolog_before.php');
    Loader::includeModule('sale');
    if (!CurrentUser::get()->isAdmin()) { die('Доступ запрещён'); }
    $userId  = 123;
    $fuserId = Fuser::getIdByUserId($userId);
    if ($fuserId === false) { die('Не удалось получить FUSER'); }
    $siteId  = Context::getCurrent()->getSite();
    $basket  = Basket::loadItemsForFUser($fuserId, $siteId);
    // Дальше — аудит/экспорт/миграция...
        

    13) Очистка старых FUSER (агент)

    <?php
    use Bitrix\Main\Loader;
    use Bitrix\Sale\Fuser;
    function AgentDeleteOldFusers()
    {
        if (!Loader::includeModule('sale')) { return __FUNCTION__.'();'; }
        // Удалит записи из b_sale_fuser:
        // - старше 30 дней по DATE_UPDATE,
        // - без USER_ID,
        // - без позиций в b_sale_basket
        Fuser::deleteOld(30);
        return __FUNCTION__.'();';
    }
        

    14) Отладка и аналитика (микропримеры)

    Вывести FUSER_ID в dataLayer:

    <?php $fuserId = (int)\Bitrix\Sale\Fuser::getId(); ?>
    <script>
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({ event: 'fuserInit', fuserId: <?= $fuserId ?> });
    </script>
        

    Понять, есть ли корзина без создания:

    $fuserId = \Bitrix\Sale\Fuser::getId(true);
    $hasBasket = false;
    if ($fuserId !== null) {
        $basket = \Bitrix\Sale\Basket::loadItemsForFUser(
            $fuserId,
            \Bitrix\Main\Context::getCurrent()->getSite()
        );
        $qty = 0; $sum = 0.0;
        foreach ($basket as $item) {
            $qty += $item->getQuantity();
            $sum += $item->getFinalPrice();
        }
        $hasBasket = $qty > 0 || $sum > 0;
    }
        

    15) Частые ошибки и как их избегать

    • «Я вызывал Fuser::update(..., ['save'=>true]), а cookie гостю не ставится»
      Так задумано. Для гостя cookie ставится только при создании FUSER и только если sale.save_anonymous_fuser_cookie=Y.
    • «После логина корзина пустая»
      В кастомной авторизации обязательно зовите Fuser::handlerOnUserLogin($userId, ['update'=>true, 'save'=>true]).
    • «Метод Basket::getQuantity() не найден»
      Он есть у элемента корзины (BasketItem), а не у Basket. Считайте foreach ($basket as $item) { $item->getQuantity(); }.
    • «getQuantityList() не содержит QUANTITY_SUM»
      В большинстве установок метод возвращает массив вида [BASKET_ITEM_ID => QTY]. Не полагайтесь на QUANTITY_SUM — лучше считайте вручную.
    • «Сумма выводится криво»
      В свежих версиях getPrice() может вернуть Bitrix\Main\Type\Decimal. Приводите к строке: (string)$basket->getPrice() или округляйте ->round(2).
    • «Cookie не ставится по HTTPS, но работает по HTTP»
      Проверьте sale.use_secure_cookies=Y и чтобы сайт реально отдавался по HTTPS. Secure-cookie не пойдут по HTTP.
    • «Создаётся слишком много FUSER у анонимов»
      На страницах без взаимодействия с корзиной используйте Fuser::getId(true) или refreshSessionCurrentId().

    16) Лучшие практики

    1. Разделяйте чтение и создание: где можно — getId(true), где нужно — getId().
    2. Следите за SITE_ID: Basket::loadItemsForFUser() всегда передавайте актуальный SITE_ID.
    3. Включайте безопасность: encode_fuser_id=Y, use_secure_cookies=Y, и в целом — постоянный HTTPS.
    4. Склейка — под контролем: кастомный логин → обязательно handlerOnUserLogin.
    5. Чистите хвосты: агент deleteOld(n) спасёт БД от мусора.
    6. Считайте вручную: количество/сумму безопасно агрегировать в цикле, независимо от версии.

    17) Резюме

    • FUSER_ID — фундамент корзины в Битрикс.
    • Основной вход — Fuser::getId() / getId(true); для авторизованных есть getRegeneratedId().
    • Kуки для гостя — только при создании FUSER и при включённой опции; для авторизованного — через update(..., ['save'=>true]).
    • Для корректной работы корзины учитывайте SITE_ID, HTTPS и настройки модуля sale.
    • Надёжнее всего подсчитывать количество/сумму вручную, не полагаясь на «магические» поля.

    Дополнение к статье: собранный мини-модуль FuserKit для версии 1С-Битрикс: Управление сайтом:

    • REST-контроллер: summary, add (с проверкой остатков), clear, remove;
    • Роуты под новый роутер;
    • Агент очистки старых FUSER;
    • AJAX-эндпоинты: сумма корзины, полная очистка, удаление одной позиции.

    FuserKit — мини-модуль (REST + AJAX + Агент)

    Структура

    /local
      /modules
        /local.fuserkit
          /install
            index.php
            version.php
          include.php
          /lib
            /Controller
              BasketController.php
            /Agent
              Cleanup.php
      routes.php
      /ajax
        fuserkit_basket_total.php
        fuserkit_basket_clear.php
        fuserkit_basket_remove.php
        

    После копирования установи модуль в админке:
    «Маркетплейс → Установленные решения → local.fuserkit → Установить».
    Это зарегистрирует агент. Роуты и AJAX начнут работать сразу.

    /local/modules/local.fuserkit/install/version.php

    <?php
    $arModuleVersion = [
        'VERSION'      => '1.0.0',
        'VERSION_DATE' => '2025-09-22 00:00:00',
    ];
        

    /local/modules/local.fuserkit/install/index.php

    <?php
    use Bitrix\Main\Localization\Loc;
    Loc::loadMessages(__FILE__);
    class local_fuserkit extends CModule
    {
        public $MODULE_ID           = 'local.fuserkit';
        public $MODULE_VERSION;
        public $MODULE_VERSION_DATE;
        public $MODULE_NAME         = 'FuserKit: утилиты для FUSER (контроллер + AJAX + агент)';
        public $MODULE_DESCRIPTION  = 'REST-контроллер корзины, AJAX-эндпоинты и агент очистки старых FUSER.';
        public $PARTNER_NAME        = 'Local';
        public $PARTNER_URI         = 'https://example.local'; // поле требуется формально
        public function __construct()
        {
            $arModuleVersion = [];
            include __DIR__ . '/version.php';
            $this->MODULE_VERSION      = $arModuleVersion['VERSION'];
            $this->MODULE_VERSION_DATE = $arModuleVersion['VERSION_DATE'];
        }
        public function DoInstall()
        {
            RegisterModule($this->MODULE_ID);
            // Ежедневный агент: чистить анонимные FUSER старше 30 дней
            CAgent::AddAgent(
                '\Local\FuserKit\Agent\Cleanup::run(30);',
                $this->MODULE_ID,
                'N',
                86400,                                     // раз в сутки
                '',
                'Y',
                date('d.m.Y H:i:s', time() + 3600),        // старт через час
                100,
                false,
                false
            );
        }
        public function DoUninstall()
        {
            CAgent::RemoveModuleAgents($this->MODULE_ID);
            UnRegisterModule($this->MODULE_ID);
        }
    }
        

    /local/modules/local.fuserkit/include.php

    <?php
    use Bitrix\Main\Loader;
    Loader::registerAutoLoadClasses('local.fuserkit', [
        'Local\\FuserKit\\Controller\\BasketController' => 'lib/Controller/BasketController.php',
        'Local\\FuserKit\\Agent\\Cleanup'               => 'lib/Agent/Cleanup.php',
    ]);
        

    /local/modules/local.fuserkit/lib/Controller/BasketController.php

    <?php
    namespace Local\FuserKit\Controller;
    use Bitrix\Main\Engine\ActionFilter;
    use Bitrix\Main\Engine\Controller;
    use Bitrix\Main\Context;
    use Bitrix\Main\Loader;
    use Bitrix\Sale\Fuser;
    use Bitrix\Sale\Basket;
    use Bitrix\Sale\BasketItem;
    /**
     * REST: работа с корзиной текущего FUSER.
     *
     * GET  /rest/basket.summary  → summaryAction
     * POST /rest/basket.add      → addAction
     * POST /rest/basket.clear    → clearAction
     * POST /rest/basket.remove   → removeAction
     */
    class BasketController extends Controller
    {
        public function configureActions(): array
        {
            return [
                'summary' => [
                    'prefilters' => [
                        new ActionFilter\HttpMethod([ActionFilter\HttpMethod::METHOD_GET]),
                        new ActionFilter\Csrf(false),
                    ],
                ],
                'add' => [
                    'prefilters' => [
                        new ActionFilter\HttpMethod([ActionFilter\HttpMethod::METHOD_POST]),
                        new ActionFilter\Csrf(false),
                    ],
                ],
                'clear' => [
                    'prefilters' => [
                        new ActionFilter\HttpMethod([ActionFilter\HttpMethod::METHOD_POST]),
                        new ActionFilter\Csrf(false),
                    ],
                ],
                'remove' => [
                    'prefilters' => [
                        new ActionFilter\HttpMethod([ActionFilter\HttpMethod::METHOD_POST]),
                        new ActionFilter\Csrf(false),
                    ],
                ],
            ];
        }
        /**
         * Сводка по корзине текущего FUSER.
         */
        public function summaryAction(): array
        {
            if (!Loader::includeModule('sale')) {
                return ['error' => 'sale module required'];
            }
            $siteId  = Context::getCurrent()->getSite();
            if (!$siteId) {
                return ['error' => 'SITE_ID is not defined'];
            }
            $fuserId = Fuser::getId(); // создастся при необходимости
            $basket  = Basket::loadItemsForFUser($fuserId, $siteId);
            $items = [];
            $sum   = 0.0;
            $qty   = 0.0;
            foreach ($basket as $item) {
                $itemSum = (float)$item->getFinalPrice();
                $sum += $itemSum;
                $itemQty = (float)$item->getQuantity();
                $qty += $itemQty;
                $items[] = [
                    'id'        => (int)$item->getId(),
                    'productId' => (int)$item->getProductId(),
                    'name'      => (string)$item->getField('NAME'),
                    'qty'       => $itemQty,
                    'price'     => (string)$item->getPrice(), // Decimal → строка
                    'sum'       => $itemSum,
                ];
            }
            return [
                'fuserId' => (int)$fuserId,
                'qty'     => $qty,
                'sum'     => $sum,
                'items'   => $items,
            ];
        }
        /**
         * Добавить товар с проверкой остатков.
         *
         * POST:
         * - productId (int, required)
         * - qty (float, default=1)
         * - strict (bool, default=false)
         *   strict=true  → при нехватке остатка вернем ошибку и не добавим
         *   strict=false → добавим доступное количество (частично)
         */
        public function addAction(): array
        {
            if (!Loader::includeModule('sale'))     { return ['error' => 'sale module required']; }
            if (!Loader::includeModule('catalog'))  { return ['error' => 'catalog module required']; }
            $request   = $this->getRequest();
            $productId = (int)($request->getPost('productId') ?? $request->get('productId'));
            $qtyRaw    = $request->getPost('qty') ?? $request->get('qty') ?? 1;
            $qty       = (float)$qtyRaw;
            $strictRaw = $request->getPost('strict') ?? $request->get('strict') ?? '0';
            $strict    = in_array(strtolower((string)$strictRaw), ['1','y','yes','true'], true);
            if ($productId <= 0) { return ['error' => 'Invalid productId']; }
            if ($qty <= 0)       { $qty = 1.0; }
            $siteId  = Context::getCurrent()->getSite();
            if (!$siteId) { return ['error' => 'SITE_ID is not defined']; }
            // Доступное количество по складам/настройкам каталога
            $available = (float)\Bitrix\Catalog\Product\Basket::getAvailableQuantity($productId, $siteId);
            if ($available <= 0.0) {
                return ['error' => 'OUT_OF_STOCK', 'available' => 0.0, 'requested' => $qty];
            }
            $addQty = $qty;
            $notice = null;
            if ($qty > $available) {
                if ($strict) {
                    return [
                        'error'     => 'NOT_ENOUGH_STOCK',
                        'available' => $available,
                        'requested' => $qty,
                    ];
                }
                $addQty = $available;
                $notice = 'REQUESTED_QTY_REDUCED_TO_AVAILABLE';
            }
            $fuserId = Fuser::getId(); // создастся при добавлении
            $basket  = Basket::loadItemsForFUser($fuserId, $siteId);
            // Если позиция уже есть — аккуратно проверяем максимум
            $basketItem = $basket->getExistsItem('catalog', $productId);
            if ($basketItem) {
                $already   = (float)$basketItem->getQuantity();
                $maxForAdd = max(0.0, $available - $already);
                if ($addQty > $maxForAdd) {
                    if ($strict) {
                        return [
                            'error'       => 'NOT_ENOUGH_STOCK',
                            'available'   => $available,
                            'inBasket'    => $already,
                            'maxAdd'      => $maxForAdd,
                            'requestedAdd'=> $addQty,
                        ];
                    }
                    $addQty = $maxForAdd;
                    $notice = 'REQUESTED_QTY_REDUCED_TO_AVAILABLE';
                }
                if ($addQty <= 0.0) {
                    return ['error' => 'NOTHING_TO_ADD', 'available' => $available, 'inBasket' => $already];
                }
                $basketItem->setField('QUANTITY', $already + $addQty);
            } else {
                /** @var BasketItem $basketItem */
                $basketItem = $basket->createItem('catalog', $productId);
                $basketItem->setField('QUANTITY', $addQty);
                $basketItem->setField('CURRENCY', \Bitrix\Currency\CurrencyManager::getBaseCurrency());
                $basketItem->setField('LID', $siteId);
                $basketItem->setField('PRODUCT_PROVIDER_CLASS', '\Bitrix\Catalog\Product\Basket');
                $basketItem->setField('NAME', 'Товар #'.$productId);
            }
            $result = $basket->save();
            if (!$result->isSuccess()) {
                return ['error' => implode('; ', $result->getErrorMessages())];
            }
            // Резерв делается на этапе оформления заказа. Здесь только валидация/количество.
            $summary = $this->summaryAction();
            if ($notice) { $summary['notice'] = $notice; }
            return $summary;
        }
        /**
         * Очистить корзину текущего FUSER.
         */
        public function clearAction(): array
        {
            if (!Loader::includeModule('sale')) { return ['error' => 'sale module required']; }
            $siteId  = Context::getCurrent()->getSite();
            if (!$siteId) { return ['error' => 'SITE_ID is not defined']; }
            $fuserId = Fuser::getId(); // если корзины не было — создастся пустая (это ок)
            $basket  = Basket::loadItemsForFUser($fuserId, $siteId);
            foreach ($basket as $item) {
                $item->delete();
            }
            $result = $basket->save();
            if (!$result->isSuccess()) {
                return ['error' => implode('; ', $result->getErrorMessages())];
            }
            return [
                'fuserId' => (int)$fuserId,
                'qty'     => 0,
                'sum'     => 0,
                'items'   => [],
                'status'  => 'cleared',
            ];
        }
        /**
         * Удалить одну позицию из корзины текущего FUSER.
         *
         * POST:
         * - basketItemId (int) — приоритет
         *   ИЛИ
         * - productId (int)    — удалить позицию по товару ('catalog', productId)
         */
        public function removeAction(): array
        {
            if (!Loader::includeModule('sale')) { return ['error' => 'sale module required']; }
            $request      = $this->getRequest();
            $basketItemId = (int)($request->getPost('basketItemId') ?? $request->get('basketItemId'));
            $productId    = (int)($request->getPost('productId') ?? $request->get('productId'));
            $siteId  = Context::getCurrent()->getSite();
            if (!$siteId) { return ['error' => 'SITE_ID is not defined']; }
            $fuserId = Fuser::getId();
            $basket  = Basket::loadItemsForFUser($fuserId, $siteId);
            $targetItem = null;
            if ($basketItemId > 0) {
                foreach ($basket as $item) {
                    if ((int)$item->getId() === $basketItemId) { $targetItem = $item; break; }
                }
                if (!$targetItem) {
                    return ['error' => 'BASKET_ITEM_NOT_FOUND_BY_ID', 'basketItemId' => $basketItemId];
                }
            } elseif ($productId > 0) {
                $targetItem = $basket->getExistsItem('catalog', $productId);
                if (!$targetItem) {
                    return ['error' => 'BASKET_ITEM_NOT_FOUND_BY_PRODUCT', 'productId' => $productId];
                }
            } else {
                return ['error' => 'basketItemId or productId is required'];
            }
            $targetItem->delete();
            $result = $basket->save();
            if (!$result->isSuccess()) {
                return ['error' => implode('; ', $result->getErrorMessages())];
            }
            return $this->summaryAction();
        }
    }
        

    /local/modules/local.fuserkit/lib/Agent/Cleanup.php

    <?php
    namespace Local\FuserKit\Agent;
    use Bitrix\Main\Loader;
    use Bitrix\Sale\Fuser;
    /**
     * Агент очистки: удаляет анонимные FUSER без позиций, старше N дней.
     */
    class Cleanup
    {
        public static function run(int $days = 30): string
        {
            if (Loader::includeModule('sale')) {
                Fuser::deleteOld($days);
            }
            return __METHOD__.'('.(int)$days.');';
        }
    }
        

    /local/routes.php

    <?php
    use Bitrix\Main\Routing\RoutingConfigurator;
    use Bitrix\Main\Loader;
    /**
     * Роуты REST:
     * GET  /rest/basket.summary → summary
     * POST /rest/basket.add     → add
     * POST /rest/basket.clear   → clear
     * POST /rest/basket.remove  → remove
     */
    return static function (RoutingConfigurator $routes) {
        Loader::includeModule('local.fuserkit');
        $routes->get(
            '/rest/basket.summary',
            [\Local\FuserKit\Controller\BasketController::class, 'summary']
        );
        $routes->post(
            '/rest/basket.add',
            [\Local\FuserKit\Controller\BasketController::class, 'add']
        );
        $routes->post(
            '/rest/basket.clear',
            [\Local\FuserKit\Controller\BasketController::class, 'clear']
        );
        $routes->post(
            '/rest/basket.remove',
            [\Local\FuserKit\Controller\BasketController::class, 'remove']
        );
    };
        

    /local/ajax/fuserkit_basket_total.php

    <?php
    use Bitrix\Main\Loader;
    use Bitrix\Main\Context;
    use Bitrix\Main\Engine\Response\Json;
    use Bitrix\Sale\Fuser;
    use Bitrix\Sale\Basket;
    header('Content-Type: application/json; charset=UTF-8');
    require $_SERVER['DOCUMENT_ROOT'].'/bitrix/modules/main/include/prolog_before.php';
    if (!Loader::includeModule('sale')) {
        (new Json(['error' => 'sale module required']))->send(); die();
    }
    $siteId  = Context::getCurrent()->getSite();
    $fuserId = Fuser::getId();
    $basket  = Basket::loadItemsForFUser($fuserId, $siteId);
    $totalQty = 0.0;
    $totalSum = 0.0;
    foreach ($basket as $item) {
        $totalQty += (float)$item->getQuantity();
        $totalSum += (float)$item->getFinalPrice();
    }
    (new Json([
        'fuserId'    => (int)$fuserId,
        'itemsCount' => $totalQty,
        'total'      => $totalSum,
    ]))->send(); die();
        

    /local/ajax/fuserkit_basket_clear.php

    <?php
    use Bitrix\Main\Loader;
    use Bitrix\Main\Context;
    use Bitrix\Main\Engine\Response\Json;
    use Bitrix\Sale\Fuser;
    use Bitrix\Sale\Basket;
    header('Content-Type: application/json; charset=UTF-8');
    require $_SERVER['DOCUMENT_ROOT'].'/bitrix/modules/main/include/prolog_before.php';
    if (!Loader::includeModule('sale')) {
        (new Json(['error' => 'sale module required']))->send(); die();
    }
    $siteId  = Context::getCurrent()->getSite();
    if (!$siteId) { (new Json(['error' => 'SITE_ID is not defined']))->send(); die(); }
    $fuserId = Fuser::getId();
    $basket  = Basket::loadItemsForFUser($fuserId, $siteId);
    foreach ($basket as $item) { $item->delete(); }
    $result = $basket->save();
    if (!$result->isSuccess()) {
        (new Json(['error' => implode('; ', $result->getErrorMessages())]))->send(); die();
    }
    (new Json([
        'fuserId' => (int)$fuserId,
        'qty'     => 0,
        'sum'     => 0,
        'items'   => [],
        'status'  => 'cleared',
    ]))->send(); die();
        

    /local/ajax/fuserkit_basket_remove.php

    <?php
    use Bitrix\Main\Loader;
    use Bitrix\Main\Context;
    use Bitrix\Main\Engine\Response\Json;
    use Bitrix\Sale\Fuser;
    use Bitrix\Sale\Basket;
    header('Content-Type: application/json; charset=UTF-8');
    require $_SERVER['DOCUMENT_ROOT'].'/bitrix/modules/main/include/prolog_before.php';
    if (!Loader::includeModule('sale')) {
        (new Json(['error' => 'sale module required']))->send(); die();
    }
    $request      = Context::getCurrent()->getRequest();
    $basketItemId = (int)($request->getPost('basketItemId') ?? $request->get('basketItemId'));
    $productId    = (int)($request->getPost('productId') ?? $request->get('productId'));
    $siteId  = Context::getCurrent()->getSite();
    if (!$siteId) { (new Json(['error' => 'SITE_ID is not defined']))->send(); die(); }
    $fuserId = Fuser::getId();
    $basket  = Basket::loadItemsForFUser($fuserId, $siteId);
    $targetItem = null;
    if ($basketItemId > 0) {
        foreach ($basket as $item) {
            if ((int)$item->getId() === $basketItemId) { $targetItem = $item; break; }
        }
        if (!$targetItem) {
            (new Json(['error' => 'BASKET_ITEM_NOT_FOUND_BY_ID', 'basketItemId' => $basketItemId]))->send(); die();
        }
    } elseif ($productId > 0) {
        $targetItem = $basket->getExistsItem('catalog', $productId);
        if (!$targetItem) {
            (new Json(['error' => 'BASKET_ITEM_NOT_FOUND_BY_PRODUCT', 'productId' => $productId]))->send(); die();
        }
    } else {
        (new Json(['error' => 'basketItemId or productId is required']))->send(); die();
    }
    $targetItem->delete();
    $result = $basket->save();
    if (!$result->isSuccess()) {
        (new Json(['error' => implode('; ', $result->getErrorMessages())]))->send(); die();
    }
    // Сводка
    $totalQty = 0.0; $totalSum = 0.0; $items = [];
    foreach ($basket as $it) {
        $totalQty += (float)$it->getQuantity();
        $totalSum += (float)$it->getFinalPrice();
        $items[] = [
            'id'        => (int)$it->getId(),
            'productId' => (int)$it->getProductId(),
            'name'      => (string)$it->getField('NAME'),
            'qty'       => (float)$it->getQuantity(),
            'price'     => (string)$it->getPrice(),
            'sum'       => (float)$it->getFinalPrice(),
        ];
    }
    (new Json([
        'fuserId'    => (int)$fuserId,
        'itemsCount' => $totalQty,
        'total'      => $totalSum,
        'items'      => $items,
        'status'     => 'removed',
    ]))->send(); die();
        

    Быстрые проверки

    REST:

    <script>
    // Сводка
    fetch('/rest/basket.summary').then(r => r.json()).then(console.log);
    // Добавить (частично, если не хватает остатков)
    fetch('/rest/basket.add', {
      method: 'POST',
      headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'},
      body: new URLSearchParams({ productId: 18, qty: 3, strict: 0 })
    }).then(r => r.json()).then(console.log);
    // Очистить всю корзину
    fetch('/rest/basket.clear', { method: 'POST' })
      .then(r => r.json()).then(console.log);
    // Удалить одну позицию по basketItemId
    fetch('/rest/basket.remove', {
      method: 'POST',
      headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'},
      body: new URLSearchParams({ basketItemId: 12345 })
    }).then(r => r.json()).then(console.log);
    // Удалить по productId
    fetch('/rest/basket.remove', {
      method: 'POST',
      headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'},
      body: new URLSearchParams({ productId: 18 })
    }).then(r => r.json()).then(console.log);
        

    AJAX:

    /local/ajax/fuserkit_basket_total.php
    /local/ajax/fuserkit_basket_clear.php
    /local/ajax/fuserkit_basket_remove.php?productId=18
        

    Нюансы

    • \Bitrix\Sale\Fuser::getId(true) используем на «холодных» страницах, чтобы не плодить записи.
    • Cookie гостю ставятся только при создании FUSER и если включено sale.save_anonymous_fuser_cookie=Y.
    • Fuser::update($id, ['save'=>true]) пишет cookie только авторизованному пользователю — это норма.
    • Basket::getPrice()/BasketItem::getPrice() могут быть Decimal. Для вывода — (string)$price, для математики — (float).
    • Остатки проверяем через \Bitrix\Catalog\Product\Basket::getAvailableQuantity($productId, $siteId). Резерв делается при оформлении заказа.
Теги: Fuser, корзина, FUSER_ID, интернет-магазин, sale модуль, PHP, разработка


Валерий Макеев
23.09.2025 10:49
Проверяем наличие существующей корзины пользователя и выводим количество товаров, но не создаем новую запись FUSER, если корзины нет -  идеально для страниц каталога где не требуется взаимодействие с  корзиной.
Код
<?php
// fuser_check.php - проверяет наличие корзины у пользователя без создания новой записи
require $_SERVER['DOCUMENT_ROOT'].'/bitrix/modules/main/include/prolog_before.php';

use Bitrix\Main\Loader;
use Bitrix\Main\Context;
use Bitrix\Sale\Fuser;
use Bitrix\Sale\Basket;

if (Loader::includeModule('sale')) {
    $siteId = Context::getCurrent()->getSite();
    $fuserId = Fuser::getId(true); // ТИХИЙ РЕЖИМ - не создаем новую запись
    
    if ($fuserId === null) {
        echo "У пользователя нет активной корзины (FUSER не создан)";
    } else {
        $basket = Basket::loadItemsForFUser($fuserId, $siteId);
        $itemCount = 0;
        foreach ($basket as $item) {
            $itemCount += $item->getQuantity();
        }
        echo "FUSER_ID: {$fuserId}, Товаров в корзине: {$itemCount} шт.";
    }
} else {
    echo "Модуль sale не установлен";
}

require $_SERVER['DOCUMENT_ROOT'].'/bitrix/modules/main/include/epilog_after.php';
?>

Стоимость услуг по разработке и сопровождению сайтов на 1C-Битрикс

Участие в проекте

привлечение в проект на part-time основе

от 30 000 рублей / неделя

Возможно участие в проекте на ежедневной основе, как разработчика. Занятость - до 20 часов в неделю
Минимальный срок - одна неделя.

* сумма фиксированная

Аутсорсинг

готов помочь, если нет времени

договорная

Могу взять на себя работы по full-stack

* на основе готовой верстки

Разработка корпоративного сайта

от 7 дней

от 40 000 рублей

Разработка сайта без системы оплаты заказов через корзину

* стоимость зависит от наличия верстки, использования готового решения и т.д.