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

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Shopware\Storefront\Controller;
  3. use Shopware\Core\Checkout\Cart\Cart;
  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\CartService;
  8. use Shopware\Core\Checkout\Customer\SalesChannel\AbstractLogoutRoute;
  9. use Shopware\Core\Checkout\Order\Exception\EmptyCartException;
  10. use Shopware\Core\Checkout\Order\SalesChannel\OrderService;
  11. use Shopware\Core\Checkout\Payment\Exception\InvalidOrderException;
  12. use Shopware\Core\Checkout\Payment\Exception\PaymentProcessException;
  13. use Shopware\Core\Checkout\Payment\Exception\UnknownPaymentMethodException;
  14. use Shopware\Core\Checkout\Payment\PaymentService;
  15. use Shopware\Core\Framework\Feature;
  16. use Shopware\Core\Framework\Log\Package;
  17. use Shopware\Core\Framework\Routing\Annotation\RouteScope;
  18. use Shopware\Core\Framework\Routing\Annotation\Since;
  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\Framework\Routing\Annotation\NoStore;
  28. use Shopware\Storefront\Page\Checkout\Cart\CheckoutCartPageLoadedHook;
  29. use Shopware\Storefront\Page\Checkout\Cart\CheckoutCartPageLoader;
  30. use Shopware\Storefront\Page\Checkout\Confirm\CheckoutConfirmPageLoadedHook;
  31. use Shopware\Storefront\Page\Checkout\Confirm\CheckoutConfirmPageLoader;
  32. use Shopware\Storefront\Page\Checkout\Finish\CheckoutFinishPageLoadedHook;
  33. use Shopware\Storefront\Page\Checkout\Finish\CheckoutFinishPageLoader;
  34. use Shopware\Storefront\Page\Checkout\Offcanvas\CheckoutInfoWidgetLoadedHook;
  35. use Shopware\Storefront\Page\Checkout\Offcanvas\CheckoutOffcanvasWidgetLoadedHook;
  36. use Shopware\Storefront\Page\Checkout\Offcanvas\OffcanvasCartPageLoader;
  37. use Symfony\Component\HttpFoundation\RedirectResponse;
  38. use Symfony\Component\HttpFoundation\Request;
  39. use Symfony\Component\HttpFoundation\Response;
  40. use Symfony\Component\HttpFoundation\Session\SessionInterface;
  41. use Symfony\Component\Routing\Annotation\Route;
  42. /**
  43. * @Route(defaults={"_routeScope"={"storefront"}})
  44. *
  45. * @deprecated tag:v6.5.0 - reason:becomes-internal - Will be internal
  46. */
  47. #[Package('storefront')]
  48. class CheckoutController extends StorefrontController
  49. {
  50. private const REDIRECTED_FROM_SAME_ROUTE = 'redirected';
  51. private CartService $cartService;
  52. private CheckoutCartPageLoader $cartPageLoader;
  53. private CheckoutConfirmPageLoader $confirmPageLoader;
  54. private CheckoutFinishPageLoader $finishPageLoader;
  55. private OrderService $orderService;
  56. private PaymentService $paymentService;
  57. private OffcanvasCartPageLoader $offcanvasCartPageLoader;
  58. private SystemConfigService $config;
  59. private AbstractLogoutRoute $logoutRoute;
  60. /**
  61. * @internal
  62. */
  63. public function __construct(
  64. CartService $cartService,
  65. CheckoutCartPageLoader $cartPageLoader,
  66. CheckoutConfirmPageLoader $confirmPageLoader,
  67. CheckoutFinishPageLoader $finishPageLoader,
  68. OrderService $orderService,
  69. PaymentService $paymentService,
  70. OffcanvasCartPageLoader $offcanvasCartPageLoader,
  71. SystemConfigService $config,
  72. AbstractLogoutRoute $logoutRoute
  73. ) {
  74. $this->cartService = $cartService;
  75. $this->cartPageLoader = $cartPageLoader;
  76. $this->confirmPageLoader = $confirmPageLoader;
  77. $this->finishPageLoader = $finishPageLoader;
  78. $this->orderService = $orderService;
  79. $this->paymentService = $paymentService;
  80. $this->offcanvasCartPageLoader = $offcanvasCartPageLoader;
  81. $this->config = $config;
  82. $this->logoutRoute = $logoutRoute;
  83. }
  84. /**
  85. * @Since("6.0.0.0")
  86. * @NoStore
  87. * @Route("/checkout/cart", name="frontend.checkout.cart.page", options={"seo"="false"}, methods={"GET"})
  88. */
  89. public function cartPage(Request $request, SalesChannelContext $context): Response
  90. {
  91. $page = $this->cartPageLoader->load($request, $context);
  92. $cart = $page->getCart();
  93. $cartErrors = $cart->getErrors();
  94. $this->hook(new CheckoutCartPageLoadedHook($page, $context));
  95. $this->addCartErrors($cart);
  96. if (!$request->query->getBoolean(self::REDIRECTED_FROM_SAME_ROUTE) && $this->routeNeedsReload($cartErrors)) {
  97. $cartErrors->clear();
  98. // To prevent redirect loops add the identifier that the request already got redirected from the same origin
  99. return $this->redirectToRoute(
  100. 'frontend.checkout.cart.page',
  101. \array_merge(
  102. $request->query->all(),
  103. [self::REDIRECTED_FROM_SAME_ROUTE => true]
  104. ),
  105. );
  106. }
  107. $cartErrors->clear();
  108. return $this->renderStorefront('@Storefront/storefront/page/checkout/cart/index.html.twig', ['page' => $page]);
  109. }
  110. /**
  111. * @Since("6.0.0.0")
  112. * @NoStore
  113. * @Route("/checkout/confirm", name="frontend.checkout.confirm.page", options={"seo"="false"}, methods={"GET"}, defaults={"XmlHttpRequest"=true})
  114. */
  115. public function confirmPage(Request $request, SalesChannelContext $context): Response
  116. {
  117. if (!$context->getCustomer()) {
  118. return $this->redirectToRoute('frontend.checkout.register.page');
  119. }
  120. if ($this->cartService->getCart($context->getToken(), $context)->getLineItems()->count() === 0) {
  121. return $this->redirectToRoute('frontend.checkout.cart.page');
  122. }
  123. $page = $this->confirmPageLoader->load($request, $context);
  124. $cart = $page->getCart();
  125. $cartErrors = $cart->getErrors();
  126. $this->hook(new CheckoutConfirmPageLoadedHook($page, $context));
  127. $this->addCartErrors($cart);
  128. if (!$request->query->getBoolean(self::REDIRECTED_FROM_SAME_ROUTE) && $this->routeNeedsReload($cartErrors)) {
  129. $cartErrors->clear();
  130. // To prevent redirect loops add the identifier that the request already got redirected from the same origin
  131. return $this->redirectToRoute(
  132. 'frontend.checkout.confirm.page',
  133. \array_merge(
  134. $request->query->all(),
  135. [self::REDIRECTED_FROM_SAME_ROUTE => true]
  136. ),
  137. );
  138. }
  139. return $this->renderStorefront('@Storefront/storefront/page/checkout/confirm/index.html.twig', ['page' => $page]);
  140. }
  141. /**
  142. * @Since("6.0.0.0")
  143. * @Route("/checkout/finish", name="frontend.checkout.finish.page", options={"seo"="false"}, methods={"GET"})
  144. * @NoStore
  145. */
  146. public function finishPage(Request $request, SalesChannelContext $context, RequestDataBag $dataBag): Response
  147. {
  148. if ($context->getCustomer() === null) {
  149. return $this->redirectToRoute('frontend.checkout.register.page');
  150. }
  151. $page = $this->finishPageLoader->load($request, $context);
  152. $this->hook(new CheckoutFinishPageLoadedHook($page, $context));
  153. if ($page->isPaymentFailed() === true) {
  154. return $this->redirectToRoute(
  155. 'frontend.account.edit-order.page',
  156. [
  157. 'orderId' => $request->get('orderId'),
  158. 'error-code' => 'CHECKOUT__UNKNOWN_ERROR',
  159. ]
  160. );
  161. }
  162. if ($context->getCustomer()->getGuest() && $this->config->get('core.cart.logoutGuestAfterCheckout', $context->getSalesChannelId())) {
  163. $this->logoutRoute->logout($context, $dataBag);
  164. }
  165. return $this->renderStorefront('@Storefront/storefront/page/checkout/finish/index.html.twig', ['page' => $page]);
  166. }
  167. /**
  168. * @Since("6.0.0.0")
  169. * @Route("/checkout/order", name="frontend.checkout.finish.order", options={"seo"="false"}, methods={"POST"})
  170. */
  171. public function order(RequestDataBag $data, SalesChannelContext $context, Request $request): Response
  172. {
  173. if (!$context->getCustomer()) {
  174. return $this->redirectToRoute('frontend.checkout.register.page');
  175. }
  176. try {
  177. $this->addAffiliateTracking($data, $request->getSession());
  178. $orderId = Profiler::trace('checkout-order', function () use ($data, $context) {
  179. return $this->orderService->createOrder($data, $context);
  180. });
  181. } catch (ConstraintViolationException $formViolations) {
  182. return $this->forwardToRoute('frontend.checkout.confirm.page', ['formViolations' => $formViolations]);
  183. } catch (InvalidCartException | Error | EmptyCartException $error) {
  184. $this->addCartErrors(
  185. $this->cartService->getCart($context->getToken(), $context)
  186. );
  187. return $this->forwardToRoute('frontend.checkout.confirm.page');
  188. }
  189. try {
  190. $finishUrl = $this->generateUrl('frontend.checkout.finish.page', ['orderId' => $orderId]);
  191. $errorUrl = $this->generateUrl('frontend.account.edit-order.page', ['orderId' => $orderId]);
  192. $response = Profiler::trace('handle-payment', function () use ($orderId, $data, $context, $finishUrl, $errorUrl): ?RedirectResponse {
  193. return $this->paymentService->handlePaymentByOrder($orderId, $data, $context, $finishUrl, $errorUrl);
  194. });
  195. return $response ?? new RedirectResponse($finishUrl);
  196. } catch (PaymentProcessException | InvalidOrderException | UnknownPaymentMethodException $e) {
  197. return $this->forwardToRoute('frontend.checkout.finish.page', ['orderId' => $orderId, 'changedPayment' => false, 'paymentFailed' => true]);
  198. }
  199. }
  200. /**
  201. * @Since("6.0.0.0")
  202. * @Route("/widgets/checkout/info", name="frontend.checkout.info", methods={"GET"}, defaults={"XmlHttpRequest"=true})
  203. */
  204. public function info(Request $request, SalesChannelContext $context): Response
  205. {
  206. $cart = $this->cartService->getCart($context->getToken(), $context);
  207. if ($cart->getLineItems()->count() <= 0
  208. && (Feature::isActive('v6.5.0.0') || Feature::isActive('PERFORMANCE_TWEAKS'))
  209. ) {
  210. return new Response(null, Response::HTTP_NO_CONTENT);
  211. }
  212. $page = $this->offcanvasCartPageLoader->load($request, $context);
  213. $this->hook(new CheckoutInfoWidgetLoadedHook($page, $context));
  214. $response = $this->renderStorefront('@Storefront/storefront/layout/header/actions/cart-widget.html.twig', ['page' => $page]);
  215. $response->headers->set('x-robots-tag', 'noindex');
  216. return $response;
  217. }
  218. /**
  219. * @Since("6.0.0.0")
  220. * @Route("/checkout/offcanvas", name="frontend.cart.offcanvas", options={"seo"="false"}, methods={"GET"}, defaults={"XmlHttpRequest"=true})
  221. */
  222. public function offcanvas(Request $request, SalesChannelContext $context): Response
  223. {
  224. $page = $this->offcanvasCartPageLoader->load($request, $context);
  225. $this->hook(new CheckoutOffcanvasWidgetLoadedHook($page, $context));
  226. $cart = $page->getCart();
  227. $this->addCartErrors($cart);
  228. $cartErrors = $cart->getErrors();
  229. if (!$request->query->getBoolean(self::REDIRECTED_FROM_SAME_ROUTE) && $this->routeNeedsReload($cartErrors)) {
  230. $cartErrors->clear();
  231. // To prevent redirect loops add the identifier that the request already got redirected from the same origin
  232. return $this->redirectToRoute(
  233. 'frontend.cart.offcanvas',
  234. \array_merge(
  235. $request->query->all(),
  236. [self::REDIRECTED_FROM_SAME_ROUTE => true]
  237. ),
  238. );
  239. }
  240. $cartErrors->clear();
  241. return $this->renderStorefront('@Storefront/storefront/component/checkout/offcanvas-cart.html.twig', ['page' => $page]);
  242. }
  243. private function addAffiliateTracking(RequestDataBag $dataBag, SessionInterface $session): void
  244. {
  245. $affiliateCode = $session->get(AffiliateTrackingListener::AFFILIATE_CODE_KEY);
  246. $campaignCode = $session->get(AffiliateTrackingListener::CAMPAIGN_CODE_KEY);
  247. if ($affiliateCode) {
  248. $dataBag->set(AffiliateTrackingListener::AFFILIATE_CODE_KEY, $affiliateCode);
  249. }
  250. if ($campaignCode) {
  251. $dataBag->set(AffiliateTrackingListener::CAMPAIGN_CODE_KEY, $campaignCode);
  252. }
  253. }
  254. private function routeNeedsReload(ErrorCollection $cartErrors): bool
  255. {
  256. foreach ($cartErrors as $error) {
  257. if ($error instanceof ShippingMethodChangedError || $error instanceof PaymentMethodChangedError) {
  258. return true;
  259. }
  260. }
  261. return false;
  262. }
  263. }