77namespace Magento \Quote \Model \Quote \Item ;
88
99use Magento \Catalog \Api \ProductRepositoryInterface ;
10+ use Magento \Framework \App \ObjectManager ;
1011use Magento \Framework \Exception \CouldNotSaveException ;
1112use Magento \Framework \Exception \InputException ;
1213use Magento \Framework \Exception \NoSuchEntityException ;
1314use Magento \Quote \Api \CartItemRepositoryInterface ;
1415use Magento \Quote \Api \CartRepositoryInterface ;
16+ use Magento \Quote \Api \Data \CartInterface ;
17+ use Magento \Quote \Api \Data \CartItemInterface ;
1518use Magento \Quote \Api \Data \CartItemInterfaceFactory ;
19+ use Magento \Quote \Model \QuoteMutexInterface ;
20+ use Magento \Quote \Model \QuoteRepository ;
1621
1722/**
1823 * Repository for quote item.
24+ *
25+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
1926 */
2027class Repository implements CartItemRepositoryInterface
2128{
2229 /**
23- * Quote repository.
24- *
2530 * @var CartRepositoryInterface
2631 */
2732 protected $ quoteRepository ;
2833
2934 /**
30- * Product repository.
31- *
3235 * @var ProductRepositoryInterface
3336 */
3437 protected $ productRepository ;
@@ -48,25 +51,33 @@ class Repository implements CartItemRepositoryInterface
4851 */
4952 private $ cartItemOptionsProcessor ;
5053
54+ /**
55+ * @var ?QuoteMutexInterface
56+ */
57+ private ?QuoteMutexInterface $ quoteMutex ;
58+
5159 /**
5260 * @param CartRepositoryInterface $quoteRepository
5361 * @param ProductRepositoryInterface $productRepository
5462 * @param CartItemInterfaceFactory $itemDataFactory
5563 * @param CartItemOptionsProcessor $cartItemOptionsProcessor
5664 * @param CartItemProcessorInterface[] $cartItemProcessors
65+ * @param QuoteMutexInterface|null $quoteMutex
5766 */
5867 public function __construct (
5968 CartRepositoryInterface $ quoteRepository ,
6069 ProductRepositoryInterface $ productRepository ,
6170 CartItemInterfaceFactory $ itemDataFactory ,
6271 CartItemOptionsProcessor $ cartItemOptionsProcessor ,
63- array $ cartItemProcessors = []
72+ array $ cartItemProcessors = [],
73+ ?QuoteMutexInterface $ quoteMutex = null
6474 ) {
6575 $ this ->quoteRepository = $ quoteRepository ;
6676 $ this ->productRepository = $ productRepository ;
6777 $ this ->itemDataFactory = $ itemDataFactory ;
6878 $ this ->cartItemOptionsProcessor = $ cartItemOptionsProcessor ;
6979 $ this ->cartItemProcessors = $ cartItemProcessors ;
80+ $ this ->quoteMutex = $ quoteMutex ?: ObjectManager::getInstance ()->get (QuoteMutexInterface::class);
7081 }
7182
7283 /**
@@ -89,7 +100,7 @@ public function getList($cartId)
89100 /**
90101 * @inheritdoc
91102 */
92- public function save (\ Magento \ Quote \ Api \ Data \ CartItemInterface $ cartItem )
103+ public function save (CartItemInterface $ cartItem )
93104 {
94105 /** @var \Magento\Quote\Model\Quote $quote */
95106 $ cartId = $ cartItem ->getQuoteId ();
@@ -99,12 +110,35 @@ public function save(\Magento\Quote\Api\Data\CartItemInterface $cartItem)
99110 );
100111 }
101112
102- $ quote = $ this ->quoteRepository ->getActive ($ cartId );
113+ return $ this ->quoteMutex ->execute (
114+ [$ cartId ],
115+ \Closure::fromCallable ([$ this , 'saveItem ' ]),
116+ [$ cartItem ]
117+ );
118+ }
119+
120+ /**
121+ * Save cart item.
122+ *
123+ * @param CartItemInterface $cartItem
124+ * @return CartItemInterface
125+ * @throws NoSuchEntityException
126+ * @SuppressWarnings(PHPMD.UnusedPrivateMethod)
127+ */
128+ private function saveItem (CartItemInterface $ cartItem )
129+ {
130+ $ cartId = (int )$ cartItem ->getQuoteId ();
131+ if ($ this ->quoteRepository instanceof QuoteRepository) {
132+ $ quote = $ this ->getNonCachedActiveQuote ($ cartId );
133+ } else {
134+ $ quote = $ this ->quoteRepository ->getActive ($ cartId );
135+ }
103136 $ quoteItems = $ quote ->getItems ();
104137 $ quoteItems [] = $ cartItem ;
105138 $ quote ->setItems ($ quoteItems );
106139 $ this ->quoteRepository ->save ($ quote );
107140 $ quote ->collectTotals ();
141+
108142 return $ quote ->getLastAddedItem ();
109143 }
110144
@@ -130,4 +164,28 @@ public function deleteById($cartId, $itemId)
130164
131165 return true ;
132166 }
167+
168+ /**
169+ * Returns quote repository without internal cache.
170+ *
171+ * Prevents usage of cached quote that causes incorrect quote items update by concurrent web-api requests.
172+ *
173+ * @param int $cartId
174+ * @return CartInterface
175+ * @throws NoSuchEntityException
176+ */
177+ private function getNonCachedActiveQuote (int $ cartId ): CartInterface
178+ {
179+ $ cachedQuote = $ this ->quoteRepository ->getActive ($ cartId );
180+ $ className = get_class ($ this ->quoteRepository );
181+ $ quote = ObjectManager::getInstance ()->create ($ className )->getActive ($ cartId );
182+ foreach ($ quote ->getItems () as $ quoteItem ) {
183+ $ cachedQuoteItem = $ cachedQuote ->getItemById ($ quoteItem ->getId ());
184+ if ($ cachedQuoteItem ) {
185+ $ quoteItem ->setExtensionAttributes ($ cachedQuoteItem ->getExtensionAttributes ());
186+ }
187+ }
188+
189+ return $ quote ;
190+ }
133191}
0 commit comments