vendor/shopware/storefront/Controller/CheckoutController.php line 103

  1. <?php declare(strict_types=1);
  2. namespace Shopware\Storefront\Controller;
  3. use Shopware\Core\Checkout\Cart\CartException;
  4. use Shopware\Core\Checkout\Cart\Error\Error;
  5. use Shopware\Core\Checkout\Cart\Error\ErrorCollection;
  6. use Shopware\Core\Checkout\Cart\Exception\InvalidCartException;
  7. use Shopware\Core\Checkout\Cart\SalesChannel\AbstractCartLoadRoute;
  8. use Shopware\Core\Checkout\Cart\SalesChannel\CartService;
  9. use Shopware\Core\Checkout\Customer\SalesChannel\AbstractLogoutRoute;
  10. use Shopware\Core\Checkout\Order\Exception\EmptyCartException;
  11. use Shopware\Core\Checkout\Order\OrderException;
  12. use Shopware\Core\Checkout\Order\SalesChannel\OrderService;
  13. use Shopware\Core\Checkout\Payment\Exception\InvalidOrderException;
  14. use Shopware\Core\Checkout\Payment\Exception\PaymentProcessException;
  15. use Shopware\Core\Checkout\Payment\Exception\UnknownPaymentMethodException;
  16. use Shopware\Core\Checkout\Payment\PaymentException;
  17. use Shopware\Core\Checkout\Payment\PaymentService;
  18. use Shopware\Core\Framework\Log\Package;
  19. use Shopware\Core\Framework\Validation\DataBag\RequestDataBag;
  20. use Shopware\Core\Framework\Validation\Exception\ConstraintViolationException;
  21. use Shopware\Core\Profiling\Profiler;
  22. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  23. use Shopware\Core\System\SystemConfig\SystemConfigService;
  24. use Shopware\Storefront\Checkout\Cart\Error\PaymentMethodChangedError;
  25. use Shopware\Storefront\Checkout\Cart\Error\ShippingMethodChangedError;
  26. use Shopware\Storefront\Framework\AffiliateTracking\AffiliateTrackingListener;
  27. use Shopware\Storefront\Page\Checkout\Cart\CheckoutCartPageLoadedHook;
  28. use Shopware\Storefront\Page\Checkout\Cart\CheckoutCartPageLoader;
  29. use Shopware\Storefront\Page\Checkout\Confirm\CheckoutConfirmPageLoadedHook;
  30. use Shopware\Storefront\Page\Checkout\Confirm\CheckoutConfirmPageLoader;
  31. use Shopware\Storefront\Page\Checkout\Finish\CheckoutFinishPageLoadedHook;
  32. use Shopware\Storefront\Page\Checkout\Finish\CheckoutFinishPageLoader;
  33. use Shopware\Storefront\Page\Checkout\Offcanvas\CheckoutInfoWidgetLoadedHook;
  34. use Shopware\Storefront\Page\Checkout\Offcanvas\CheckoutOffcanvasWidgetLoadedHook;
  35. use Shopware\Storefront\Page\Checkout\Offcanvas\OffcanvasCartPageLoader;
  36. use Symfony\Component\HttpFoundation\RedirectResponse;
  37. use Symfony\Component\HttpFoundation\Request;
  38. use Symfony\Component\HttpFoundation\Response;
  39. use Symfony\Component\HttpFoundation\Session\SessionInterface;
  40. use Symfony\Component\Routing\Annotation\Route;
  41. /**
  42.  * @internal
  43.  * Do not use direct or indirect repository calls in a controller. Always use a store-api route to get or put datas
  44.  */
  45. #[Route(defaults: ['_routeScope' => ['storefront']])]
  46. #[Package('storefront')]
  47. class CheckoutController extends StorefrontController
  48. {
  49.     private const REDIRECTED_FROM_SAME_ROUTE 'redirected';
  50.     /**
  51.      * @internal
  52.      */
  53.     public function __construct(
  54.         private readonly CartService $cartService,
  55.         private readonly CheckoutCartPageLoader $cartPageLoader,
  56.         private readonly CheckoutConfirmPageLoader $confirmPageLoader,
  57.         private readonly CheckoutFinishPageLoader $finishPageLoader,
  58.         private readonly OrderService $orderService,
  59.         private readonly PaymentService $paymentService,
  60.         private readonly OffcanvasCartPageLoader $offcanvasCartPageLoader,
  61.         private readonly SystemConfigService $config,
  62.         private readonly AbstractLogoutRoute $logoutRoute,
  63.         private readonly AbstractCartLoadRoute $cartLoadRoute
  64.     ) {
  65.     }
  66.     #[Route(path'/checkout/cart'name'frontend.checkout.cart.page'options: ['seo' => false], defaults: ['_noStore' => true], methods: ['GET'])]
  67.     public function cartPage(Request $requestSalesChannelContext $context): Response
  68.     {
  69.         $page $this->cartPageLoader->load($request$context);
  70.         $cart $page->getCart();
  71.         $cartErrors $cart->getErrors();
  72.         $this->hook(new CheckoutCartPageLoadedHook($page$context));
  73.         $this->addCartErrors($cart);
  74.         if (!$request->query->getBoolean(self::REDIRECTED_FROM_SAME_ROUTE) && $this->routeNeedsReload($cartErrors)) {
  75.             $cartErrors->clear();
  76.             // To prevent redirect loops add the identifier that the request already got redirected from the same origin
  77.             return $this->redirectToRoute(
  78.                 'frontend.checkout.cart.page',
  79.                 [...$request->query->all(), ...[self::REDIRECTED_FROM_SAME_ROUTE => true]],
  80.             );
  81.         }
  82.         $cartErrors->clear();
  83.         return $this->renderStorefront('@Storefront/storefront/page/checkout/cart/index.html.twig', ['page' => $page]);
  84.     }
  85.     #[Route(path'/checkout/cart.json'name'frontend.checkout.cart.json'methods: ['GET'], options: ['seo' => false], defaults: ['XmlHttpRequest' => true])]
  86.     public function cartJson(Request $requestSalesChannelContext $context): Response
  87.     {
  88.         return $this->cartLoadRoute->load($request$context);
  89.     }
  90.     #[Route(path'/checkout/confirm'name'frontend.checkout.confirm.page'options: ['seo' => false], defaults: ['XmlHttpRequest' => true'_noStore' => true], methods: ['GET'])]
  91.     public function confirmPage(Request $requestSalesChannelContext $context): Response
  92.     {
  93.         if (!$context->getCustomer()) {
  94.             return $this->redirectToRoute('frontend.checkout.register.page');
  95.         }
  96.         if ($this->cartService->getCart($context->getToken(), $context)->getLineItems()->count() === 0) {
  97.             return $this->redirectToRoute('frontend.checkout.cart.page');
  98.         }
  99.         $page $this->confirmPageLoader->load($request$context);
  100.         $cart $page->getCart();
  101.         $cartErrors $cart->getErrors();
  102.         $this->hook(new CheckoutConfirmPageLoadedHook($page$context));
  103.         $this->addCartErrors($cart);
  104.         if (!$request->query->getBoolean(self::REDIRECTED_FROM_SAME_ROUTE) && $this->routeNeedsReload($cartErrors)) {
  105.             $cartErrors->clear();
  106.             // To prevent redirect loops add the identifier that the request already got redirected from the same origin
  107.             return $this->redirectToRoute(
  108.                 'frontend.checkout.confirm.page',
  109.                 [...$request->query->all(), ...[self::REDIRECTED_FROM_SAME_ROUTE => true]],
  110.             );
  111.         }
  112.         return $this->renderStorefront('@Storefront/storefront/page/checkout/confirm/index.html.twig', ['page' => $page]);
  113.     }
  114.     #[Route(path'/checkout/finish'name'frontend.checkout.finish.page'options: ['seo' => false], defaults: ['_noStore' => true], methods: ['GET'])]
  115.     public function finishPage(Request $requestSalesChannelContext $contextRequestDataBag $dataBag): Response
  116.     {
  117.         if ($context->getCustomer() === null) {
  118.             return $this->redirectToRoute('frontend.checkout.register.page');
  119.         }
  120.         try {
  121.             $page $this->finishPageLoader->load($request$context);
  122.         } catch (OrderException $exception) {
  123.             $this->addFlash(self::DANGER$this->trans('error.' $exception->getErrorCode()));
  124.             return $this->redirectToRoute('frontend.checkout.cart.page');
  125.         }
  126.         $this->hook(new CheckoutFinishPageLoadedHook($page$context));
  127.         if ($page->isPaymentFailed() === true) {
  128.             return $this->redirectToRoute(
  129.                 'frontend.account.edit-order.page',
  130.                 [
  131.                     'orderId' => $request->get('orderId'),
  132.                     'error-code' => 'CHECKOUT__UNKNOWN_ERROR',
  133.                 ]
  134.             );
  135.         }
  136.         if ($context->getCustomer()->getGuest() && $this->config->get('core.cart.logoutGuestAfterCheckout'$context->getSalesChannelId())) {
  137.             $this->logoutRoute->logout($context$dataBag);
  138.         }
  139.         return $this->renderStorefront('@Storefront/storefront/page/checkout/finish/index.html.twig', ['page' => $page]);
  140.     }
  141.     #[Route(path'/checkout/order'name'frontend.checkout.finish.order'options: ['seo' => false], methods: ['POST'])]
  142.     public function order(RequestDataBag $dataSalesChannelContext $contextRequest $request): Response
  143.     {
  144.         if (!$context->getCustomer()) {
  145.             return $this->redirectToRoute('frontend.checkout.register.page');
  146.         }
  147.         try {
  148.             $this->addAffiliateTracking($data$request->getSession());
  149.             $orderId Profiler::trace('checkout-order', fn () => $this->orderService->createOrder($data$context));
  150.         } catch (ConstraintViolationException $formViolations) {
  151.             return $this->forwardToRoute('frontend.checkout.confirm.page', ['formViolations' => $formViolations]);
  152.         } catch (InvalidCartException|Error|EmptyCartException) {
  153.             $this->addCartErrors(
  154.                 $this->cartService->getCart($context->getToken(), $context)
  155.             );
  156.             return $this->forwardToRoute('frontend.checkout.confirm.page');
  157.         } catch (UnknownPaymentMethodException|CartException $e) {
  158.             if ($e->getErrorCode() === CartException::CART_PAYMENT_INVALID_ORDER_STORED_CODE && $e->getParameter('orderId')) {
  159.                 return $this->forwardToRoute('frontend.checkout.finish.page', ['orderId' => $e->getParameter('orderId'), 'changedPayment' => false'paymentFailed' => true]);
  160.             }
  161.             $message $this->trans('error.' $e->getErrorCode());
  162.             $this->addFlash('danger'$message);
  163.             return $this->forwardToRoute('frontend.checkout.confirm.page');
  164.         }
  165.         try {
  166.             $finishUrl $this->generateUrl('frontend.checkout.finish.page', ['orderId' => $orderId]);
  167.             $errorUrl $this->generateUrl('frontend.account.edit-order.page', ['orderId' => $orderId]);
  168.             $response Profiler::trace('handle-payment', fn (): ?RedirectResponse => $this->paymentService->handlePaymentByOrder($orderId$data$context$finishUrl$errorUrl));
  169.             return $response ?? new RedirectResponse($finishUrl);
  170.         } catch (PaymentProcessException|InvalidOrderException|PaymentException|UnknownPaymentMethodException) {
  171.             return $this->forwardToRoute('frontend.checkout.finish.page', ['orderId' => $orderId'changedPayment' => false'paymentFailed' => true]);
  172.         }
  173.     }
  174.     #[Route(path'/widgets/checkout/info'name'frontend.checkout.info'defaults: ['XmlHttpRequest' => true], methods: ['GET'])]
  175.     public function info(Request $requestSalesChannelContext $context): Response
  176.     {
  177.         $cart $this->cartService->getCart($context->getToken(), $context);
  178.         if ($cart->getLineItems()->count() <= 0) {
  179.             return new Response(nullResponse::HTTP_NO_CONTENT);
  180.         }
  181.         $page $this->offcanvasCartPageLoader->load($request$context);
  182.         $this->hook(new CheckoutInfoWidgetLoadedHook($page$context));
  183.         $response $this->renderStorefront('@Storefront/storefront/layout/header/actions/cart-widget.html.twig', ['page' => $page]);
  184.         $response->headers->set('x-robots-tag''noindex');
  185.         return $response;
  186.     }
  187.     #[Route(path'/checkout/offcanvas'name'frontend.cart.offcanvas'options: ['seo' => false], defaults: ['XmlHttpRequest' => true], methods: ['GET'])]
  188.     public function offcanvas(Request $requestSalesChannelContext $context): Response
  189.     {
  190.         $page $this->offcanvasCartPageLoader->load($request$context);
  191.         $this->hook(new CheckoutOffcanvasWidgetLoadedHook($page$context));
  192.         $cart $page->getCart();
  193.         $this->addCartErrors($cart);
  194.         $cartErrors $cart->getErrors();
  195.         if (!$request->query->getBoolean(self::REDIRECTED_FROM_SAME_ROUTE) && $this->routeNeedsReload($cartErrors)) {
  196.             $cartErrors->clear();
  197.             // To prevent redirect loops add the identifier that the request already got redirected from the same origin
  198.             return $this->redirectToRoute(
  199.                 'frontend.cart.offcanvas',
  200.                 [...$request->query->all(), ...[self::REDIRECTED_FROM_SAME_ROUTE => true]],
  201.             );
  202.         }
  203.         $cartErrors->clear();
  204.         return $this->renderStorefront('@Storefront/storefront/component/checkout/offcanvas-cart.html.twig', ['page' => $page]);
  205.     }
  206.     private function addAffiliateTracking(RequestDataBag $dataBagSessionInterface $session): void
  207.     {
  208.         $affiliateCode $session->get(AffiliateTrackingListener::AFFILIATE_CODE_KEY);
  209.         $campaignCode $session->get(AffiliateTrackingListener::CAMPAIGN_CODE_KEY);
  210.         if ($affiliateCode) {
  211.             $dataBag->set(AffiliateTrackingListener::AFFILIATE_CODE_KEY$affiliateCode);
  212.         }
  213.         if ($campaignCode) {
  214.             $dataBag->set(AffiliateTrackingListener::CAMPAIGN_CODE_KEY$campaignCode);
  215.         }
  216.     }
  217.     private function routeNeedsReload(ErrorCollection $cartErrors): bool
  218.     {
  219.         foreach ($cartErrors as $error) {
  220.             if ($error instanceof ShippingMethodChangedError || $error instanceof PaymentMethodChangedError) {
  221.                 return true;
  222.             }
  223.         }
  224.         return false;
  225.     }
  226. }