Skip to content

Commit ea20fdb

Browse files
Roman HaninRoman Hanin
authored andcommitted
B2B-2243: Optimize addProductsToCart operations
1 parent 2605796 commit ea20fdb

File tree

5 files changed

+140
-43
lines changed

5 files changed

+140
-43
lines changed

app/code/Magento/Quote/Model/Cart/AddProductsToCart.php

Lines changed: 21 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
namespace Magento\Quote\Model\Cart;
99

1010
use Magento\Catalog\Api\ProductRepositoryInterface;
11-
use Magento\Framework\Api\SearchCriteriaBuilder;
1211
use Magento\Framework\App\ObjectManager;
1312
use Magento\Framework\Exception\NoSuchEntityException;
1413
use Magento\Quote\Api\CartRepositoryInterface;
@@ -68,31 +67,29 @@ class AddProductsToCart
6867
private $requestBuilder;
6968

7069
/**
71-
* @var SearchCriteriaBuilder
70+
* @var ProductReaderInterface
7271
*/
73-
private $searchCriteriaBuilder;
72+
private $productReader;
7473

7574
/**
7675
* @param ProductRepositoryInterface $productRepository
7776
* @param CartRepositoryInterface $cartRepository
7877
* @param MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId
7978
* @param BuyRequestBuilder $requestBuilder
80-
* @param SearchCriteriaBuilder|null $searchCriteriaBuilder
79+
* @param ProductReaderInterface $productReader
8180
*/
8281
public function __construct(
8382
ProductRepositoryInterface $productRepository,
8483
CartRepositoryInterface $cartRepository,
8584
MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId,
8685
BuyRequestBuilder $requestBuilder,
87-
SearchCriteriaBuilder $searchCriteriaBuilder = null
86+
ProductReaderInterface $productReader = null
8887
) {
8988
$this->productRepository = $productRepository;
9089
$this->cartRepository = $cartRepository;
9190
$this->maskedQuoteIdToQuoteId = $maskedQuoteIdToQuoteId;
9291
$this->requestBuilder = $requestBuilder;
93-
$this->searchCriteriaBuilder = $searchCriteriaBuilder ?: ObjectManager::getInstance()->get(
94-
SearchCriteriaBuilder::class
95-
);
92+
$this->productReader = $productReader ?: ObjectManager::getInstance()->get(ProductReaderInterface::class);
9693
}
9794

9895
/**
@@ -134,11 +131,9 @@ public function execute(string $maskedCartId, array $cartItems): AddProductsToCa
134131
}
135132
}
136133
}
137-
138134
if ($saveCart) {
139135
$this->cartRepository->save($cart);
140136
}
141-
142137
if (count($allErrors) !== 0) {
143138
/* Revert changes introduced by add to cart processes in case of an error */
144139
$cart->getItemsCollection()->clear();
@@ -157,17 +152,14 @@ public function execute(string $maskedCartId, array $cartItems): AddProductsToCa
157152
public function addItemsToCart(Quote $cart, array $cartItems): array
158153
{
159154
$failedCartItems = [];
160-
$cartItemSkus = \array_map(
155+
// add new cart items for preload
156+
$skus = \array_map(
161157
function ($item) {
162158
return $item->getSku();
163159
},
164160
$cartItems
165161
);
166-
167-
$searchCriteria = $this->searchCriteriaBuilder->addFilter('sku', $cartItemSkus, 'in')->create();
168-
// getList() call caches product models in runtime cache
169-
$this->productRepository->getList($searchCriteria)->getItems();
170-
162+
$this->productReader->loadProducts($skus, $cart->getStoreId());
171163
foreach ($cartItems as $cartItemPosition => $cartItem) {
172164
$errors = $this->addItemToCart($cart, $cartItem, $cartItemPosition);
173165
if ($errors) {
@@ -191,37 +183,32 @@ private function addItemToCart(Quote $cart, Data\CartItem $cartItem, int $cartIt
191183
$sku = $cartItem->getSku();
192184
$errors = [];
193185
$result = null;
194-
$product = null;
195186

196187
if ($cartItem->getQuantity() <= 0) {
197188
$errors[] = $this->createError(
198189
__('The product quantity should be greater than 0')->render(),
199190
$cartItemPosition
200191
);
201192
} else {
202-
try {
203-
$product = $this->productRepository->get($sku, false, $cart->getStoreId(), false);
204-
} catch (NoSuchEntityException $e) {
193+
$product = $this->productReader->getProductBySku($sku);
194+
if (!$product) {
205195
$errors[] = $this->createError(
206196
__('Could not find a product with SKU "%sku"', ['sku' => $sku])->render(),
207197
$cartItemPosition
208198
);
209199
}
200+
try {
201+
$result = $cart->addProduct($product, $this->requestBuilder->build($cartItem));
202+
} catch (\Throwable $e) {
203+
$errors[] = $this->createError(
204+
__($e->getMessage())->render(),
205+
$cartItemPosition
206+
);
207+
}
210208

211-
if ($product !== null) {
212-
try {
213-
$result = $cart->addProduct($product, $this->requestBuilder->build($cartItem));
214-
} catch (\Throwable $e) {
215-
$errors[] = $this->createError(
216-
__($e->getMessage())->render(),
217-
$cartItemPosition
218-
);
219-
}
220-
221-
if (is_string($result)) {
222-
foreach (array_unique(explode("\n", $result)) as $error) {
223-
$errors[] = $this->createError(__($error)->render(), $cartItemPosition);
224-
}
209+
if (is_string($result)) {
210+
foreach (array_unique(explode("\n", $result)) as $error) {
211+
$errors[] = $this->createError(__($error)->render(), $cartItemPosition);
225212
}
226213
}
227214
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Quote\Model\Cart;
9+
10+
use Magento\Catalog\Api\Data\ProductInterface;
11+
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory;
12+
use Magento\Quote\Model\Quote\Config;
13+
14+
/**
15+
* Cart reader product loader.
16+
*/
17+
class ProductReader implements ProductReaderInterface
18+
{
19+
/**
20+
* @var ProductCollectionFactory
21+
*/
22+
private $productCollectionFactory;
23+
24+
/**
25+
* @var ProductInterface[]
26+
*/
27+
private $productsBySku;
28+
29+
/**
30+
* @var Config
31+
*/
32+
private $quoteConfig;
33+
34+
/**
35+
* @param ProductCollectionFactory $productCollectionFactory
36+
* @param Config $quoteConfig
37+
*/
38+
public function __construct(
39+
ProductCollectionFactory $productCollectionFactory,
40+
Config $quoteConfig
41+
) {
42+
$this->productCollectionFactory = $productCollectionFactory;
43+
$this->quoteConfig = $quoteConfig;
44+
}
45+
46+
/**
47+
* @inheirtdoc
48+
*/
49+
public function loadProducts(array $skus, int $storeId): void
50+
{
51+
$this->productCollection = $this->productCollectionFactory->create();
52+
53+
$this->productCollection->addAttributeToSelect($this->quoteConfig->getProductAttributes());
54+
$this->productCollection->setStoreId($storeId);
55+
$this->productCollection->addFieldToFilter(ProductInterface::SKU, ['in' => $skus]);
56+
$this->productCollection->joinAttribute('status', 'catalog_product/status', 'entity_id', null, 'inner');
57+
$this->productCollection->joinAttribute('visibility', 'catalog_product/visibility', 'entity_id', null, 'inner');
58+
$this->productCollection->load();
59+
foreach ($this->productCollection->getItems() as $productItem) {
60+
$this->productsBySku[$productItem->getData(ProductInterface::SKU)] = $productItem;
61+
}
62+
}
63+
64+
/**
65+
* @inheirtdoc
66+
*/
67+
public function getProductBySku(string $sku) : ?ProductInterface
68+
{
69+
return $this->productsBySku[$sku] ?: null;
70+
}
71+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Quote\Model\Cart;
9+
10+
use Magento\Catalog\Api\Data\ProductInterface;
11+
12+
/**
13+
* Cart layer product loader.
14+
*/
15+
interface ProductReaderInterface
16+
{
17+
/**
18+
* Load products by skus for specified store.
19+
*
20+
* @param string[] $skus
21+
* @param int $storeId
22+
* @return void
23+
*/
24+
public function loadProducts(array $skus, int $storeId);
25+
26+
/**
27+
* Get product by specified sku.
28+
*
29+
* @param string $sku
30+
* @return ProductInterface
31+
*/
32+
public function getProductBySku(string $sku) : ?ProductInterface;
33+
}

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

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,11 @@ class Quote extends AbstractExtensibleModel implements \Magento\Quote\Api\Data\C
368368
*/
369369
private $allowedCountriesReader;
370370

371+
/**
372+
* @var Quote\Item[]
373+
*/
374+
private $allItemsCache;
375+
371376
/**
372377
* @param \Magento\Framework\Model\Context $context
373378
* @param \Magento\Framework\Registry $registry
@@ -1428,16 +1433,17 @@ public function getItemsCollection($useCache = true)
14281433
*/
14291434
public function getAllItems()
14301435
{
1431-
$items = [];
1436+
if (!empty($this->allItemsCache)) {
1437+
return $this->allItemsCache;
1438+
}
14321439
/** @var \Magento\Quote\Model\Quote\Item $item */
14331440
foreach ($this->getItemsCollection() as $item) {
14341441
$product = $item->getProduct();
14351442
if (!$item->isDeleted() && ($product && (int)$product->getStatus() !== ProductStatus::STATUS_DISABLED)) {
1436-
$items[] = $item;
1443+
$this->allItemsCache[$product->getSku()] = $item;
14371444
}
14381445
}
1439-
1440-
return $items;
1446+
return $this->allItemsCache;
14411447
}
14421448

14431449
/**
@@ -1572,7 +1578,7 @@ public function removeItem($itemId)
15721578
if ($parent) {
15731579
$parent->isDeleted(true);
15741580
}
1575-
1581+
unset($this->allItemsCache[$item->getSku()]);
15761582
$this->_eventManager->dispatch('sales_quote_remove_item', ['quote_item' => $item]);
15771583
}
15781584

@@ -1843,10 +1849,9 @@ public function updateItem($itemId, $buyRequest, $params = null)
18431849
*/
18441850
public function getItemByProduct($product)
18451851
{
1846-
foreach ($this->getAllItems() as $item) {
1847-
if ($item->representProduct($product)) {
1848-
return $item;
1849-
}
1852+
$item = $this->getAllItems()[$product->getSku()] ?? null;
1853+
if ($item && $item->representProduct($product)) {
1854+
return $item;
18501855
}
18511856
return false;
18521857
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
<preference for="Magento\Quote\Model\ValidationRules\QuoteValidationRuleInterface" type="Magento\Quote\Model\ValidationRules\QuoteValidationComposite\Proxy"/>
4747
<preference for="Magento\Quote\Model\QuoteMutexInterface" type="Magento\Quote\Model\QuoteMutex"/>
4848
<preference for="Magento\Quote\Model\Quote\Item\Option\ComparatorInterface" type="Magento\Quote\Model\Quote\Item\Option\Comparator"/>
49+
<preference for="Magento\Quote\Model\Cart\ProductReaderInterface" type="Magento\Quote\Model\Cart\ProductReader"/>
4950
<type name="Magento\Webapi\Controller\Rest\ParamsOverrider">
5051
<arguments>
5152
<argument name="paramOverriders" xsi:type="array">

0 commit comments

Comments
 (0)