Skip to content

Commit 095906c

Browse files
committed
Merge branch 'ACP2E-2664' of https://github.com/magento-l3/magento2ce into PR-01-26-2024
2 parents 9ab6691 + 9e4ddd8 commit 095906c

File tree

4 files changed

+159
-8
lines changed

4 files changed

+159
-8
lines changed

app/code/Magento/Quote/Model/Quote/Item/Repository.php

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,31 @@
77
namespace Magento\Quote\Model\Quote\Item;
88

99
use Magento\Catalog\Api\ProductRepositoryInterface;
10+
use Magento\Framework\App\ObjectManager;
1011
use Magento\Framework\Exception\CouldNotSaveException;
1112
use Magento\Framework\Exception\InputException;
1213
use Magento\Framework\Exception\NoSuchEntityException;
1314
use Magento\Quote\Api\CartItemRepositoryInterface;
1415
use Magento\Quote\Api\CartRepositoryInterface;
16+
use Magento\Quote\Api\Data\CartInterface;
17+
use Magento\Quote\Api\Data\CartItemInterface;
1518
use 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
*/
2027
class 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
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
/************************************************************************
3+
*
4+
* Copyright 2024 Adobe
5+
* All Rights Reserved.
6+
*
7+
* NOTICE: All information contained herein is, and remains
8+
* the property of Adobe and its suppliers, if any. The intellectual
9+
* and technical concepts contained herein are proprietary to Adobe
10+
* and its suppliers and are protected by all applicable intellectual
11+
* property laws, including trade secret and copyright laws.
12+
* Dissemination of this information or reproduction of this material
13+
* is strictly forbidden unless prior written permission is obtained
14+
* from Adobe.
15+
* ***********************************************************************
16+
*/
17+
declare(strict_types=1);
18+
19+
namespace Magento\Quote\Model;
20+
21+
use Magento\Framework\App\ResourceConnection;
22+
23+
/**
24+
* @inheritDoc
25+
*/
26+
class QuoteIdMutex implements QuoteMutexInterface
27+
{
28+
/**
29+
* @var ResourceConnection
30+
*/
31+
private $resourceConnection;
32+
33+
/**
34+
* @param ResourceConnection $resourceConnection
35+
*/
36+
public function __construct(
37+
ResourceConnection $resourceConnection
38+
) {
39+
$this->resourceConnection = $resourceConnection;
40+
}
41+
42+
/**
43+
* @inheritDoc
44+
*/
45+
public function execute(array $maskedIds, callable $callable, array $args = [])
46+
{
47+
if (empty($maskedIds)) {
48+
throw new \InvalidArgumentException('Quote ids must be provided');
49+
}
50+
51+
$connection = $this->resourceConnection->getConnection();
52+
$connection->beginTransaction();
53+
$query = $connection->select()
54+
->from($this->resourceConnection->getTableName('quote'), 'entity_id')
55+
->where('entity_id IN (?)', $maskedIds)
56+
->forUpdate(true);
57+
$connection->query($query);
58+
59+
try {
60+
$result = $callable(...$args);
61+
$this->resourceConnection->getConnection()->commit();
62+
return $result;
63+
} catch (\Throwable $e) {
64+
$this->resourceConnection->getConnection()->rollBack();
65+
throw $e;
66+
}
67+
}
68+
}

app/code/Magento/Quote/Test/Unit/Model/Quote/Item/RepositoryTest.php

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,27 @@ public function testSave()
146146
$quoteMock->expects($this->once())->method('collectTotals')->willReturnSelf();
147147
$quoteMock->expects($this->once())->method('getLastAddedItem')->willReturn($itemId);
148148

149-
$this->assertEquals($itemId, $this->repository->save($this->itemMock));
149+
$this->assertEquals(
150+
$itemId,
151+
$this->invokeMethod($this->repository, 'saveItem', [$this->itemMock])
152+
);
153+
}
154+
155+
/**
156+
* Invokes private method.
157+
*
158+
* @param $object
159+
* @param $methodName
160+
* @param array $parameters
161+
* @return mixed
162+
*/
163+
private function invokeMethod(&$object, $methodName, array $parameters = [])
164+
{
165+
$reflection = new \ReflectionClass(get_class($object));
166+
$method = $reflection->getMethod($methodName);
167+
$method->setAccessible(true);
168+
169+
return $method->invokeArgs($object, $parameters);
150170
}
151171

152172
/**

app/code/Magento/Quote/etc/di.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,4 +160,9 @@
160160
</argument>
161161
</arguments>
162162
</type>
163+
<type name="Magento\Quote\Model\Quote\Item\Repository">
164+
<arguments>
165+
<argument name="quoteMutex" xsi:type="object">Magento\Quote\Model\QuoteIdMutex</argument>
166+
</arguments>
167+
</type>
163168
</config>

0 commit comments

Comments
 (0)