Ситуация знакомая многим, кто работает с Sylius: покупатель набирает товары в корзину, переходит к оформлению, логинится — и корзина пустая. Товары исчезли. Покупатель уходит, поддержка получает тикет, бизнес теряет деньги.
Мы в NJ Soft сталкивались с этой проблемой на нескольких проектах и каждый раз разбирались заново, потому что причины бывают разные. В этой статье собрали всё в одном месте: как Sylius работает с корзиной, почему она сбрасывается и что с этим делать.
Как Sylius хранит корзину
Для начала разберёмся, как устроена корзина «под капотом».
Когда неавторизованный пользователь добавляет товар в корзину, Sylius создаёт объект Order со статусом state=cart и привязывает его к сессии через SessionCartContext. Это и есть гостевая корзина — она живёт до тех пор, пока жива сессия.
После логина корзина должна «перепривязаться» к объекту Customer. За это отвечает цепочка компонентов:
SessionCartContext— достаёт корзину из сессии (для гостей).CustomerCartContext— достаёт корзину из базы данных (для авторизованных пользователей).CompositeCartContext— обёртка, которая перебирает все контексты по приоритету и возвращает первую найденную корзину.CartBlenderInterface— сервис, который сливает гостевую корзину с корзиной покупателя.
Как должно работать слияние
Штатный сценарий выглядит так:
- Пользователь логинится.
- Symfony диспатчит событие
sylius.customer.logged_in. - Слушатель вызывает
CartBlender::blend(). CartBlenderберёт гостевую корзину из сессии и корзину покупателя из БД.- Товары из гостевой корзины добавляются к корзине покупателя (стратегия по умолчанию).
- Сессионный
cart_idобнуляется, покупатель получает итоговую корзину.
Звучит просто. На практике — ломается в четырёх местах.
Типичные причины сброса корзины
1. Инвалидация сессии при логине
Это самая частая причина. Symfony по умолчанию регенерирует Session ID при аутентификации — это настройка session.storage.migrate_on_login, и она включена из соображений безопасности (защита от session fixation).
Проблема в том, что если CartContext не успевает восстановить cart_id из нового сессионного контекста, корзина «теряется». Технически заказ всё ещё в базе, но связь с сессией разорвана, и Sylius создаёт новую пустую корзину.
Решение: явно сохранять cart_id в сессии до регенерации. Можно подписаться на событие security.interactive_login и сохранить идентификатор корзины заранее:
// src/EventListener/CartSessionListener.php
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
class CartSessionListener
{
public function __construct(
private RequestStack $requestStack,
) {}
public function onInteractiveLogin(InteractiveLoginEvent $event): void
{
$session = $this->requestStack->getSession();
$cartId = $session->get('_sylius.cart.FASHION_WEB'); // ключ зависит от канала
if ($cartId) {
// Сохраняем в атрибутах запроса, чтобы восстановить после регенерации
$event->getRequest()->attributes->set('_cart_id_backup', $cartId);
}
}
}
2. Отключённый или неправильно настроенный CartBlender
Если в проекте используется кастомный аутентификатор Symfony (а в большинстве реальных проектов это так), событие sylius.customer.logged_in может просто не диспатчиться. Sylius ожидает, что аутентификация пройдёт через его стандартный flow, но кастомный security.yaml легко может перехватить процесс раньше.
Решение: явно диспатчить событие в onAuthenticationSuccess() вашего аутентификатора. Вот как это выглядит:
// src/Security/ShopUserAuthenticator.php
use Sylius\Bundle\CustomerBundle\Event\CustomerEvents as SyliusCustomerEvents;
use Symfony\Component\EventDispatcher\GenericEvent;
use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;
class ShopUserAuthenticator extends AbstractLoginFormAuthenticator
{
public function __construct(
// ... остальные зависимости
private EventDispatcherInterface $eventDispatcher,
) {}
public function onAuthenticationSuccess(
Request $request,
TokenInterface $token,
string $firewallName
): Response {
// Обязательно: диспатчим событие для CartBlender
$this->eventDispatcher->dispatch(
new GenericEvent($token->getUser()->getCustomer()),
SyliusCustomerEvents::POST_LOGGED_IN
);
return parent::onAuthenticationSuccess($request, $token, $firewallName);
}
}
Без этого вызова CartBlender просто не узнает, что пользователь залогинился, и слияние не произойдёт.
3. Несколько CartContext-ов с неверным приоритетом
CompositeCartContext перебирает зарегистрированные контексты по приоритету. Если CustomerCartContext срабатывает раньше SessionCartContext, то после логина Sylius сразу вернёт корзину покупателя из БД (скорее всего пустую) и никогда не доберётся до гостевой корзины в сессии.
Решение: проверить приоритеты тегов sylius.context.cart в конфигурации сервисов:
php bin/console debug:container --tag=sylius.context.cart
Убедитесь, что SessionCartContext имеет более высокий приоритет (меньшее числовое значение), чем CustomerCartContext. Если в проекте есть кастомные CartContext-ы — проверьте и их тоже.
4. Race condition при AJAX-запросах
Этот случай коварнее остальных. Фронтенд отправляет AJAX-запрос на получение корзины параллельно с запросом логина. Запрос корзины приходит на сервер до того, как авторизация завершится, — и возвращает пустую корзину нового пользователя. Фронтенд получает пустой ответ и рисует пустую корзину.
Решение: блокировать запросы корзины на фронтенде до завершения авторизации. Если используете JavaScript-фреймворк — добавьте флаг isAuthenticating и не отправляйте запросы к API корзины, пока он активен. Если используете стандартные шаблоны Sylius — убедитесь, что после логина происходит полный редирект, а не частичное обновление страницы.
Диагностика
Если корзина пропадает и вы не уверены в причине, вот чек-лист для быстрой диагностики.
Проверить, какой CartContext возвращает корзину:
$cart = $container->get('sylius.context.cart')->getCart();
dump($cart->getId(), $cart->getCustomer());
Посмотреть, диспатчится ли событие:
Откройте Symfony Profiler → вкладка Events → найдите sylius.customer.logged_in. Если его нет — проблема в аутентификаторе (причина №2).
Проверить слушателей на событие:
$listeners = $dispatcher->getListeners('sylius.customer.logged_in');
dump($listeners);
Проверить «осиротевшие» корзины в БД:
SELECT id, customer_id, state, token_value
FROM sylius_order
WHERE state = 'cart'
AND customer_id IS NULL
ORDER BY created_at DESC
LIMIT 10;
Если после логина в таблице остаются записи с customer_id = NULL и state = cart — значит слияние не отработало.
Особый случай: авторизация на шаге оформления заказа
Отдельная головная боль — когда пользователь логинится уже в процессе чекаута, например на шаге ввода адреса или выбора оплаты.
Checkout в Sylius управляется собственным стейт-машиной, и если пользователь авторизуется на шаге address или payment, order_id в URL может указывать на гостевой заказ. После слияния корзин нужно редиректить пользователя на корзину авторизованного покупателя, а не продолжать гостевой чекаут с невалидным order_id.
Решение: переопределить CheckoutResolver или добавить middleware-редирект, который после логина на чекауте перебросит пользователя на начало оформления с правильной корзиной:
// src/EventListener/PostLoginCheckoutRedirectListener.php
class PostLoginCheckoutRedirectListener
{
public function onPostLogin(GenericEvent $event): void
{
$request = $this->requestStack->getCurrentRequest();
// Если логин произошёл во время чекаута — редирект на корзину
if (str_contains($request->getPathInfo(), '/checkout/')) {
$this->session->set('_sylius.redirect_after_login',
$this->router->generate('sylius_shop_cart_summary')
);
}
}
}
Итоги
Проблема сброса корзины при авторизации в Sylius — системная. Фреймворк предполагает, что разработчик будет кастомизировать аутентификацию, но механизм слияния корзин при этом остаётся хрупким местом.
Вот краткий чек-лист для проверки:
- Диспатч события — убедитесь, что
sylius.customer.logged_inвызывается при кастомной аутентификации. - Приоритеты CartContext —
SessionCartContextдолжен идти раньшеCustomerCartContext. - Регенерация сессии —
cart_idдолжен пережить смену Session ID. - Поведение при чекауте — после логина на шаге оформления пользователь должен получить правильную корзину.
И самое важное: этот сценарий — «гость → логин → оформление заказа» — обязательно нужно покрывать автотестами. Ручное тестирование здесь недостаточно, потому что баг может проявляться только при определённом порядке запросов или при наличии существующей корзины у покупателя.





