vendor/shopware/storefront/Controller/AddressController.php line 63

  1. <?php declare(strict_types=1);
  2. namespace Shopware\Storefront\Controller;
  3. use Shopware\Core\Checkout\Cart\Exception\CustomerNotLoggedInException;
  4. use Shopware\Core\Checkout\Cart\Order\Transformer\CustomerTransformer;
  5. use Shopware\Core\Checkout\Customer\Aggregate\CustomerAddress\CustomerAddressEntity;
  6. use Shopware\Core\Checkout\Customer\CustomerEntity;
  7. use Shopware\Core\Checkout\Customer\Exception\AddressNotFoundException;
  8. use Shopware\Core\Checkout\Customer\Exception\CannotDeleteDefaultAddressException;
  9. use Shopware\Core\Checkout\Customer\SalesChannel\AbstractChangeCustomerProfileRoute;
  10. use Shopware\Core\Checkout\Customer\SalesChannel\AbstractDeleteAddressRoute;
  11. use Shopware\Core\Checkout\Customer\SalesChannel\AbstractListAddressRoute;
  12. use Shopware\Core\Checkout\Customer\SalesChannel\AbstractUpsertAddressRoute;
  13. use Shopware\Core\Checkout\Customer\SalesChannel\AccountService;
  14. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  15. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  16. use Shopware\Core\Framework\Log\Package;
  17. use Shopware\Core\Framework\Routing\Exception\MissingRequestParameterException;
  18. use Shopware\Core\Framework\Uuid\Exception\InvalidUuidException;
  19. use Shopware\Core\Framework\Uuid\Uuid;
  20. use Shopware\Core\Framework\Validation\DataBag\DataBag;
  21. use Shopware\Core\Framework\Validation\DataBag\RequestDataBag;
  22. use Shopware\Core\Framework\Validation\Exception\ConstraintViolationException;
  23. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  24. use Shopware\Storefront\Page\Address\AddressEditorModalStruct;
  25. use Shopware\Storefront\Page\Address\Detail\AddressDetailPageLoadedHook;
  26. use Shopware\Storefront\Page\Address\Detail\AddressDetailPageLoader;
  27. use Shopware\Storefront\Page\Address\Listing\AddressBookWidgetLoadedHook;
  28. use Shopware\Storefront\Page\Address\Listing\AddressListingPageLoadedHook;
  29. use Shopware\Storefront\Page\Address\Listing\AddressListingPageLoader;
  30. use Symfony\Component\HttpFoundation\RedirectResponse;
  31. use Symfony\Component\HttpFoundation\Request;
  32. use Symfony\Component\HttpFoundation\Response;
  33. use Symfony\Component\Routing\Annotation\Route;
  34. /**
  35.  * @internal
  36.  * Do not use direct or indirect repository calls in a controller. Always use a store-api route to get or put data
  37.  */
  38. #[Route(defaults: ['_routeScope' => ['storefront']])]
  39. #[Package('storefront')]
  40. class AddressController extends StorefrontController
  41. {
  42.     private const ADDRESS_TYPE_BILLING 'billing';
  43.     private const ADDRESS_TYPE_SHIPPING 'shipping';
  44.     /**
  45.      * @internal
  46.      */
  47.     public function __construct(
  48.         private readonly AddressListingPageLoader $addressListingPageLoader,
  49.         private readonly AddressDetailPageLoader $addressDetailPageLoader,
  50.         private readonly AccountService $accountService,
  51.         private readonly AbstractListAddressRoute $listAddressRoute,
  52.         private readonly AbstractUpsertAddressRoute $updateAddressRoute,
  53.         private readonly AbstractDeleteAddressRoute $deleteAddressRoute,
  54.         private readonly AbstractChangeCustomerProfileRoute $updateCustomerProfileRoute
  55.     ) {
  56.     }
  57.     #[Route(path'/account/address'name'frontend.account.address.page'options: ['seo' => false], defaults: ['_loginRequired' => true'_noStore' => true], methods: ['GET'])]
  58.     public function accountAddressOverview(Request $requestSalesChannelContext $contextCustomerEntity $customer): Response
  59.     {
  60.         $page $this->addressListingPageLoader->load($request$context$customer);
  61.         $this->hook(new AddressListingPageLoadedHook($page$context));
  62.         return $this->renderStorefront('@Storefront/storefront/page/account/addressbook/index.html.twig', ['page' => $page]);
  63.     }
  64.     #[Route(path'/account/address/create'name'frontend.account.address.create.page'options: ['seo' => false], defaults: ['_loginRequired' => true'_noStore' => true], methods: ['GET'])]
  65.     public function accountCreateAddress(Request $requestRequestDataBag $dataSalesChannelContext $contextCustomerEntity $customer): Response
  66.     {
  67.         $page $this->addressDetailPageLoader->load($request$context$customer);
  68.         $this->hook(new AddressDetailPageLoadedHook($page$context));
  69.         return $this->renderStorefront('@Storefront/storefront/page/account/addressbook/create.html.twig', [
  70.             'page' => $page,
  71.             'data' => $data,
  72.         ]);
  73.     }
  74.     #[Route(path'/account/address/{addressId}'name'frontend.account.address.edit.page'options: ['seo' => false], defaults: ['_loginRequired' => true'_noStore' => true], methods: ['GET'])]
  75.     public function accountEditAddress(Request $requestSalesChannelContext $contextCustomerEntity $customer): Response
  76.     {
  77.         $page $this->addressDetailPageLoader->load($request$context$customer);
  78.         $this->hook(new AddressDetailPageLoadedHook($page$context));
  79.         return $this->renderStorefront('@Storefront/storefront/page/account/addressbook/edit.html.twig', ['page' => $page]);
  80.     }
  81.     #[Route(path'/account/address/default-{type}/{addressId}'name'frontend.account.address.set-default-address'defaults: ['_loginRequired' => true], methods: ['POST'])]
  82.     public function switchDefaultAddress(string $typestring $addressIdSalesChannelContext $contextCustomerEntity $customer): RedirectResponse
  83.     {
  84.         if (!Uuid::isValid($addressId)) {
  85.             throw new InvalidUuidException($addressId);
  86.         }
  87.         $success true;
  88.         try {
  89.             if ($type === self::ADDRESS_TYPE_SHIPPING) {
  90.                 $this->accountService->setDefaultShippingAddress($addressId$context$customer);
  91.             } elseif ($type === self::ADDRESS_TYPE_BILLING) {
  92.                 $this->accountService->setDefaultBillingAddress($addressId$context$customer);
  93.             } else {
  94.                 $success false;
  95.             }
  96.         } catch (AddressNotFoundException) {
  97.             $success false;
  98.         }
  99.         return new RedirectResponse(
  100.             $this->generateUrl('frontend.account.address.page', ['changedDefaultAddress' => $success])
  101.         );
  102.     }
  103.     #[Route(path'/account/address/delete/{addressId}'name'frontend.account.address.delete'options: ['seo' => false], defaults: ['_loginRequired' => true], methods: ['POST'])]
  104.     public function deleteAddress(string $addressIdSalesChannelContext $contextCustomerEntity $customer): Response
  105.     {
  106.         $success true;
  107.         if (!$addressId) {
  108.             throw new MissingRequestParameterException('addressId');
  109.         }
  110.         try {
  111.             $this->deleteAddressRoute->delete($addressId$context$customer);
  112.         } catch (InvalidUuidException AddressNotFoundException CannotDeleteDefaultAddressException) {
  113.             $success false;
  114.         }
  115.         return new RedirectResponse($this->generateUrl('frontend.account.address.page', ['addressDeleted' => $success]));
  116.     }
  117.     #[Route(path'/account/address/create'name'frontend.account.address.create'options: ['seo' => false], defaults: ['_loginRequired' => true], methods: ['POST'])]
  118.     #[Route(path'/account/address/{addressId}'name'frontend.account.address.edit.save'options: ['seo' => false], defaults: ['_loginRequired' => true], methods: ['POST'])]
  119.     public function saveAddress(RequestDataBag $dataSalesChannelContext $contextCustomerEntity $customer): Response
  120.     {
  121.         /** @var RequestDataBag $address */
  122.         $address $data->get('address');
  123.         try {
  124.             $this->updateAddressRoute->upsert(
  125.                 $address->get('id'),
  126.                 $address->toRequestDataBag(),
  127.                 $context,
  128.                 $customer
  129.             );
  130.             return new RedirectResponse($this->generateUrl('frontend.account.address.page', ['addressSaved' => true]));
  131.         } catch (ConstraintViolationException $formViolations) {
  132.         }
  133.         if (!$address->get('id')) {
  134.             return $this->forwardToRoute('frontend.account.address.create.page', ['formViolations' => $formViolations]);
  135.         }
  136.         return $this->forwardToRoute(
  137.             'frontend.account.address.edit.page',
  138.             ['formViolations' => $formViolations],
  139.             ['addressId' => $address->get('id')]
  140.         );
  141.     }
  142.     #[Route(path'/widgets/account/address-book'name'frontend.account.addressbook'options: ['seo' => true], defaults: ['XmlHttpRequest' => true'_loginRequired' => true'_loginRequiredAllowGuest' => true], methods: ['POST'])]
  143.     public function addressBook(Request $requestRequestDataBag $dataBagSalesChannelContext $contextCustomerEntity $customer): Response
  144.     {
  145.         $viewData = new AddressEditorModalStruct();
  146.         $params = [];
  147.         try {
  148.             $this->handleChangeableAddresses($viewData$dataBag$context$customer);
  149.             $this->handleAddressCreation($viewData$dataBag$context$customer);
  150.             $this->handleAddressSelection($viewData$dataBag$context$customer);
  151.             $page $this->addressListingPageLoader->load($request$context$customer);
  152.             $this->hook(new AddressBookWidgetLoadedHook($page$context));
  153.             $viewData->setPage($page);
  154.             $this->handleCustomerVatIds($dataBag$context$customer);
  155.         } catch (ConstraintViolationException $formViolations) {
  156.             $params['formViolations'] = $formViolations;
  157.             $params['postedData'] = $dataBag->get('address');
  158.         } catch (\Exception) {
  159.             $viewData->setSuccess(false);
  160.             $viewData->setMessages([
  161.                 'type' => self::DANGER,
  162.                 'text' => $this->trans('error.message-default'),
  163.             ]);
  164.         }
  165.         if ($request->get('redirectTo') || $request->get('forwardTo')) {
  166.             return $this->createActionResponse($request);
  167.         }
  168.         $params array_merge($params$viewData->getVars());
  169.         $response $this->renderStorefront(
  170.             '@Storefront/storefront/component/address/address-editor-modal.html.twig',
  171.             $params
  172.         );
  173.         $response->headers->set('x-robots-tag''noindex');
  174.         return $response;
  175.     }
  176.     private function handleAddressCreation(
  177.         AddressEditorModalStruct $viewData,
  178.         RequestDataBag $dataBag,
  179.         SalesChannelContext $context,
  180.         CustomerEntity $customer
  181.     ): void {
  182.         /** @var DataBag|null $addressData */
  183.         $addressData $dataBag->get('address');
  184.         if ($addressData === null) {
  185.             return;
  186.         }
  187.         $response $this->updateAddressRoute->upsert(
  188.             $addressData->get('id'),
  189.             $addressData->toRequestDataBag(),
  190.             $context,
  191.             $customer
  192.         );
  193.         $addressId $response->getAddress()->getId();
  194.         $addressType null;
  195.         if ($viewData->isChangeBilling()) {
  196.             $addressType self::ADDRESS_TYPE_BILLING;
  197.         } elseif ($viewData->isChangeShipping()) {
  198.             $addressType self::ADDRESS_TYPE_SHIPPING;
  199.         }
  200.         // prepare data to set newly created address as customers default
  201.         if ($addressType) {
  202.             $dataBag->set('selectAddress', new RequestDataBag([
  203.                 'id' => $addressId,
  204.                 'type' => $addressType,
  205.             ]));
  206.         }
  207.         $viewData->setAddressId($addressId);
  208.         $viewData->setSuccess(true);
  209.         $viewData->setMessages(['type' => 'success''text' => $this->trans('account.addressSaved')]);
  210.     }
  211.     private function handleChangeableAddresses(
  212.         AddressEditorModalStruct $viewData,
  213.         RequestDataBag $dataBag,
  214.         SalesChannelContext $context,
  215.         CustomerEntity $customer
  216.     ): void {
  217.         $changeableAddresses $dataBag->get('changeableAddresses');
  218.         if ($changeableAddresses === null) {
  219.             return;
  220.         }
  221.         $viewData->setChangeShipping((bool) $changeableAddresses->get('changeShipping'));
  222.         $viewData->setChangeBilling((bool) $changeableAddresses->get('changeBilling'));
  223.         $addressId $dataBag->get('id');
  224.         if (!$addressId) {
  225.             return;
  226.         }
  227.         $viewData->setAddress($this->getById($addressId$context$customer));
  228.     }
  229.     /**
  230.      * @throws CustomerNotLoggedInException
  231.      * @throws InvalidUuidException
  232.      */
  233.     private function handleAddressSelection(
  234.         AddressEditorModalStruct $viewData,
  235.         RequestDataBag $dataBag,
  236.         SalesChannelContext $context,
  237.         CustomerEntity $customer
  238.     ): void {
  239.         $selectedAddress $dataBag->get('selectAddress');
  240.         if ($selectedAddress === null) {
  241.             return;
  242.         }
  243.         $addressType $selectedAddress->get('type');
  244.         $addressId $selectedAddress->get('id');
  245.         if (!Uuid::isValid($addressId)) {
  246.             throw new InvalidUuidException($addressId);
  247.         }
  248.         $success true;
  249.         try {
  250.             if ($addressType === self::ADDRESS_TYPE_SHIPPING) {
  251.                 $address $this->getById($addressId$context$customer);
  252.                 $customer->setDefaultShippingAddress($address);
  253.                 $this->accountService->setDefaultShippingAddress($addressId$context$customer);
  254.             } elseif ($addressType === self::ADDRESS_TYPE_BILLING) {
  255.                 $address $this->getById($addressId$context$customer);
  256.                 $customer->setDefaultBillingAddress($address);
  257.                 $this->accountService->setDefaultBillingAddress($addressId$context$customer);
  258.             } else {
  259.                 $success false;
  260.             }
  261.         } catch (AddressNotFoundException) {
  262.             $success false;
  263.         }
  264.         if ($success) {
  265.             $this->addFlash(self::SUCCESS$this->trans('account.addressDefaultChanged'));
  266.         } else {
  267.             $this->addFlash(self::DANGER$this->trans('account.addressDefaultNotChanged'));
  268.         }
  269.         $viewData->setSuccess($success);
  270.     }
  271.     private function getById(string $addressIdSalesChannelContext $contextCustomerEntity $customer): CustomerAddressEntity
  272.     {
  273.         if (!Uuid::isValid($addressId)) {
  274.             throw new InvalidUuidException($addressId);
  275.         }
  276.         $criteria = new Criteria();
  277.         $criteria->addFilter(new EqualsFilter('id'$addressId));
  278.         $criteria->addFilter(new EqualsFilter('customerId'$customer->getId()));
  279.         $address $this->listAddressRoute->load($criteria$context$customer)->getAddressCollection()->get($addressId);
  280.         if (!$address) {
  281.             throw new AddressNotFoundException($addressId);
  282.         }
  283.         return $address;
  284.     }
  285.     private function handleCustomerVatIds(RequestDataBag $dataBagSalesChannelContext $contextCustomerEntity $customer): void
  286.     {
  287.         if (!$dataBag->has('vatIds')) {
  288.             return;
  289.         }
  290.         $newVatIds $dataBag->get('vatIds')->all();
  291.         $oldVatIds $customer->getVatIds() ?? [];
  292.         if (!array_diff($newVatIds$oldVatIds) && !array_diff($oldVatIds$newVatIds)) {
  293.             return;
  294.         }
  295.         $dataCustomer CustomerTransformer::transform($customer);
  296.         $dataCustomer['vatIds'] = $newVatIds;
  297.         $dataCustomer['accountType'] = $customer->getCompany() === null CustomerEntity::ACCOUNT_TYPE_PRIVATE CustomerEntity::ACCOUNT_TYPE_BUSINESS;
  298.         $newDataBag = new RequestDataBag($dataCustomer);
  299.         $this->updateCustomerProfileRoute->change($newDataBag$context$customer);
  300.     }
  301. }