diff --git a/vendor/magento/module-quote/Model/CartLockedException.php b/vendor/magento/module-quote/Model/CartLockedException.php new file mode 100644 index 00000000000..7f2aa877842 --- /dev/null +++ b/vendor/magento/module-quote/Model/CartLockedException.php @@ -0,0 +1,18 @@ +lockManager = $lockManager; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function execute(int $id, callable $callable, array $args = []) + { + $lockName = 'cart_lock_' . $id; + + if (!$this->lockManager->lock($lockName, 0)) { + $this->logger->critical( + 'The cart is locked for processing, the request has been aborted. Quote ID: ' . $id + ); + throw new CartLockedException( + __('The cart is locked for processing. Please try again later.') + ); + } + + try { + $result = $callable(...$args); + } finally { + $this->lockManager->unlock($lockName); + } + + return $result; + } +} diff --git a/vendor/magento/module-quote/Model/CartMutexInterface.php b/vendor/magento/module-quote/Model/CartMutexInterface.php new file mode 100644 index 00000000000..6681c638829 --- /dev/null +++ b/vendor/magento/module-quote/Model/CartMutexInterface.php @@ -0,0 +1,25 @@ +eventManager = $eventManager; $this->submitQuoteValidator = $submitQuoteValidator; @@ -257,6 +264,8 @@ class QuoteManagement implements CartManagementInterface ->get(RequestInterface::class); $this->remoteAddress = $remoteAddress ?: ObjectManager::getInstance() ->get(RemoteAddress::class); + $this->cartMutex = $cartMutex + ?? ObjectManager::getInstance()->get(CartMutexInterface::class); } /** @@ -382,10 +391,28 @@ class QuoteManagement implements CartManagementInterface /** * @inheritdoc + */ + public function placeOrder($cartId, PaymentInterface $paymentMethod = null) + { + return $this->cartMutex->execute( + (int)$cartId, + \Closure::fromCallable([$this, 'placeOrderRun']), + [$cartId, $paymentMethod] + ); + } + + /** + * Places an order for a specified cart. + * + * @param int $cartId The cart ID. + * @param PaymentInterface|null $paymentMethod + * @throws CouldNotSaveException + * @return int Order ID. * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.UnusedPrivateMethod) */ - public function placeOrder($cartId, PaymentInterface $paymentMethod = null) + private function placeOrderRun($cartId, PaymentInterface $paymentMethod = null) { $quote = $this->quoteRepository->getActive($cartId); $customer = $quote->getCustomer(); diff --git a/vendor/magento/module-quote/Model/QuoteRepository.php b/vendor/magento/module-quote/Model/QuoteRepository.php index b1bef834197..483b3724044 100644 --- a/vendor/magento/module-quote/Model/QuoteRepository.php +++ b/vendor/magento/module-quote/Model/QuoteRepository.php @@ -165,6 +165,7 @@ class QuoteRepository implements CartRepositoryInterface */ public function getActive($cartId, array $sharedStoreIds = []) { + $this->validateCachedActiveQuote((int)$cartId); $quote = $this->get($cartId, $sharedStoreIds); if (!$quote->getIsActive()) { throw NoSuchEntityException::singleField('cartId', $cartId); @@ -172,11 +173,33 @@ class QuoteRepository implements CartRepositoryInterface return $quote; } + /** + * Validates if cached quote is still active. + * + * @param int $cartId + * @return void + * @throws NoSuchEntityException + */ + private function validateCachedActiveQuote(int $cartId): void + { + if (isset($this->quotesById[$cartId])) { + $quote = $this->cartFactory->create(); + if (is_callable([$quote, 'setSharedStoreIds'])) { + $quote->setSharedStoreIds(['*']); + } + $quote->loadActive($cartId); + if (!$quote->getIsActive()) { + throw NoSuchEntityException::singleField('cartId', $cartId); + } + } + } + /** * @inheritdoc */ public function getActiveForCustomer($customerId, array $sharedStoreIds = []) { + $this->validateCachedCustomerActiveQuote((int)$customerId); $quote = $this->getForCustomer($customerId, $sharedStoreIds); if (!$quote->getIsActive()) { throw NoSuchEntityException::singleField('customerId', $customerId); @@ -184,6 +207,28 @@ class QuoteRepository implements CartRepositoryInterface return $quote; } + /** + * Validates if cached customer quote is still active. + * + * @param int $customerId + * @return void + * @throws NoSuchEntityException + */ + private function validateCachedCustomerActiveQuote(int $customerId): void + { + if (isset($this->quotesByCustomerId[$customerId])) { + $quoteId = $this->quotesByCustomerId[$customerId]->getId(); + $quote = $this->cartFactory->create(); + if (is_callable([$quote, 'setSharedStoreIds'])) { + $quote->setSharedStoreIds(['*']); + } + $quote->loadActive($quoteId); + if (!$quote->getIsActive()) { + throw NoSuchEntityException::singleField('customerId', $customerId); + } + } + } + /** * @inheritdoc */ diff --git a/vendor/magento/module-quote/etc/di.xml b/vendor/magento/module-quote/etc/di.xml index 01821c63801..5e945d583d5 100644 --- a/vendor/magento/module-quote/etc/di.xml +++ b/vendor/magento/module-quote/etc/di.xml @@ -45,6 +45,7 @@ + diff --git a/vendor/magento/module-quote/i18n/en_US.csv b/vendor/magento/module-quote/i18n/en_US.csv index c899c432c70..4240990fd87 100644 --- a/vendor/magento/module-quote/i18n/en_US.csv +++ b/vendor/magento/module-quote/i18n/en_US.csv @@ -68,3 +68,4 @@ Carts,Carts "Invalid state change requested","Invalid state change requested" "Validated Country Code","Validated Country Code" "Validated Vat Number","Validated Vat Number" +"The cart is locked for processing. Please try again later.","The cart is locked for processing. Please try again later."