diff --git a/vendor/magento/module-quote/Model/Quote/Item/Repository.php b/vendor/magento/module-quote/Model/Quote/Item/Repository.php
index 6fb512a619de..4f7864db29a8 100644
--- a/vendor/magento/module-quote/Model/Quote/Item/Repository.php
+++ b/vendor/magento/module-quote/Model/Quote/Item/Repository.php
@@ -7,28 +7,31 @@
namespace Magento\Quote\Model\Quote\Item;
use Magento\Catalog\Api\ProductRepositoryInterface;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\Exception\CouldNotSaveException;
use Magento\Framework\Exception\InputException;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Quote\Api\CartItemRepositoryInterface;
use Magento\Quote\Api\CartRepositoryInterface;
+use Magento\Quote\Api\Data\CartInterface;
+use Magento\Quote\Api\Data\CartItemInterface;
use Magento\Quote\Api\Data\CartItemInterfaceFactory;
+use Magento\Quote\Model\QuoteMutexInterface;
+use Magento\Quote\Model\QuoteRepository;
/**
* Repository for quote item.
+ *
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class Repository implements CartItemRepositoryInterface
{
/**
- * Quote repository.
- *
* @var CartRepositoryInterface
*/
protected $quoteRepository;
/**
- * Product repository.
- *
* @var ProductRepositoryInterface
*/
protected $productRepository;
@@ -48,25 +51,33 @@ class Repository implements CartItemRepositoryInterface
*/
private $cartItemOptionsProcessor;
+ /**
+ * @var ?QuoteMutexInterface
+ */
+ private ?QuoteMutexInterface $quoteMutex;
+
/**
* @param CartRepositoryInterface $quoteRepository
* @param ProductRepositoryInterface $productRepository
* @param CartItemInterfaceFactory $itemDataFactory
* @param CartItemOptionsProcessor $cartItemOptionsProcessor
* @param CartItemProcessorInterface[] $cartItemProcessors
+ * @param QuoteMutexInterface|null $quoteMutex
*/
public function __construct(
CartRepositoryInterface $quoteRepository,
ProductRepositoryInterface $productRepository,
CartItemInterfaceFactory $itemDataFactory,
CartItemOptionsProcessor $cartItemOptionsProcessor,
- array $cartItemProcessors = []
+ array $cartItemProcessors = [],
+ ?QuoteMutexInterface $quoteMutex = null
) {
$this->quoteRepository = $quoteRepository;
$this->productRepository = $productRepository;
$this->itemDataFactory = $itemDataFactory;
$this->cartItemOptionsProcessor = $cartItemOptionsProcessor;
$this->cartItemProcessors = $cartItemProcessors;
+ $this->quoteMutex = $quoteMutex ?: ObjectManager::getInstance()->get(QuoteMutexInterface::class);
}
/**
@@ -89,7 +100,7 @@ public function getList($cartId)
/**
* @inheritdoc
*/
- public function save(\Magento\Quote\Api\Data\CartItemInterface $cartItem)
+ public function save(CartItemInterface $cartItem)
{
/** @var \Magento\Quote\Model\Quote $quote */
$cartId = $cartItem->getQuoteId();
@@ -99,12 +110,35 @@ public function save(\Magento\Quote\Api\Data\CartItemInterface $cartItem)
);
}
- $quote = $this->quoteRepository->getActive($cartId);
+ return $this->quoteMutex->execute(
+ [$cartId],
+ \Closure::fromCallable([$this, 'saveItem']),
+ [$cartItem]
+ );
+ }
+
+ /**
+ * Save cart item.
+ *
+ * @param CartItemInterface $cartItem
+ * @return CartItemInterface
+ * @throws NoSuchEntityException
+ * @SuppressWarnings(PHPMD.UnusedPrivateMethod)
+ */
+ private function saveItem(CartItemInterface $cartItem)
+ {
+ $cartId = (int)$cartItem->getQuoteId();
+ if ($this->quoteRepository instanceof QuoteRepository) {
+ $quote = $this->getNonCachedActiveQuote($cartId);
+ } else {
+ $quote = $this->quoteRepository->getActive($cartId);
+ }
$quoteItems = $quote->getItems();
$quoteItems[] = $cartItem;
$quote->setItems($quoteItems);
$this->quoteRepository->save($quote);
$quote->collectTotals();
+
return $quote->getLastAddedItem();
}
@@ -130,4 +164,28 @@ public function deleteById($cartId, $itemId)
return true;
}
+
+ /**
+ * Returns quote repository without internal cache.
+ *
+ * Prevents usage of cached quote that causes incorrect quote items update by concurrent web-api requests.
+ *
+ * @param int $cartId
+ * @return CartInterface
+ * @throws NoSuchEntityException
+ */
+ private function getNonCachedActiveQuote(int $cartId): CartInterface
+ {
+ $cachedQuote = $this->quoteRepository->getActive($cartId);
+ $className = get_class($this->quoteRepository);
+ $quote = ObjectManager::getInstance()->create($className)->getActive($cartId);
+ foreach ($quote->getItems() as $quoteItem) {
+ $cachedQuoteItem = $cachedQuote->getItemById($quoteItem->getId());
+ if ($cachedQuoteItem) {
+ $quoteItem->setExtensionAttributes($cachedQuoteItem->getExtensionAttributes());
+ }
+ }
+
+ return $quote;
+ }
}
diff --git a/vendor/magento/module-quote/Model/QuoteIdMutex.php b/vendor/magento/module-quote/Model/QuoteIdMutex.php
new file mode 100644
index 000000000000..aa5bcb089dc5
--- /dev/null
+++ b/vendor/magento/module-quote/Model/QuoteIdMutex.php
@@ -0,0 +1,68 @@
+resourceConnection = $resourceConnection;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function execute(array $maskedIds, callable $callable, array $args = [])
+ {
+ if (empty($maskedIds)) {
+ throw new \InvalidArgumentException('Quote ids must be provided');
+ }
+
+ $connection = $this->resourceConnection->getConnection();
+ $connection->beginTransaction();
+ $query = $connection->select()
+ ->from($this->resourceConnection->getTableName('quote'), 'entity_id')
+ ->where('entity_id IN (?)', $maskedIds)
+ ->forUpdate(true);
+ $connection->query($query);
+
+ try {
+ $result = $callable(...$args);
+ $this->resourceConnection->getConnection()->commit();
+ return $result;
+ } catch (\Throwable $e) {
+ $this->resourceConnection->getConnection()->rollBack();
+ throw $e;
+ }
+ }
+}
diff --git a/vendor/magento/module-quote/etc/di.xml b/vendor/magento/module-quote/etc/di.xml
index 01821c63801a..e7267eaa9821 100644
--- a/vendor/magento/module-quote/etc/di.xml
+++ b/vendor/magento/module-quote/etc/di.xml
@@ -143,4 +143,9 @@
Enter a valid payment method and try again.
+
+
+ Magento\Quote\Model\QuoteIdMutex
+
+