Skip to content

Commit 263708d

Browse files
authored
Merge pull request #7879 from magento-arcticfoxes/B2B-2423
B2B-2423: Optimize addProductsToCart operations
2 parents b08879a + 67bea81 commit 263708d

File tree

12 files changed

+281
-85
lines changed

12 files changed

+281
-85
lines changed

app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Deferred/Product.php

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,7 @@ public function __construct(
6262
*/
6363
public function addProductSku(string $sku) : void
6464
{
65-
if (!in_array($sku, $this->productSkus) && !empty($this->productList)) {
66-
$this->productList = [];
67-
$this->productSkus[] = $sku;
68-
} elseif (!in_array($sku, $this->productSkus)) {
65+
if (!in_array($sku, $this->productSkus)) {
6966
$this->productSkus[] = $sku;
7067
}
7168
}
@@ -79,12 +76,7 @@ public function addProductSku(string $sku) : void
7976
public function addProductSkus(array $skus) : void
8077
{
8178
foreach ($skus as $sku) {
82-
if (!in_array($sku, $this->productSkus) && !empty($this->productList)) {
83-
$this->productList = [];
84-
$this->productSkus[] = $sku;
85-
} elseif (!in_array($sku, $this->productSkus)) {
86-
$this->productSkus[] = $sku;
87-
}
79+
$this->addProductSku($sku);
8880
}
8981
}
9082

@@ -108,28 +100,37 @@ public function addEavAttributes(array $attributeCodes) : void
108100
*/
109101
public function getProductBySku(string $sku, ContextInterface $context = null) : array
110102
{
111-
$products = $this->fetch($context);
103+
if (isset($this->productList[$sku])) {
104+
return $this->productList[$sku];
105+
}
112106

113-
if (!isset($products[$sku])) {
107+
$this->fetch($context);
108+
109+
if (!isset($this->productList[$sku])) {
114110
return [];
115111
}
116112

117-
return $products[$sku];
113+
return $this->productList[$sku];
118114
}
119115

120116
/**
121117
* Fetch product data and return in array format. Keys for products will be their skus.
122118
*
123119
* @param null|ContextInterface $context
124-
* @return array
125120
*/
126-
private function fetch(ContextInterface $context = null) : array
121+
private function fetch(ContextInterface $context = null): void
127122
{
128-
if (empty($this->productSkus) || !empty($this->productList)) {
129-
return $this->productList;
123+
if (empty($this->productSkus)) {
124+
return;
130125
}
131126

132-
$this->searchCriteriaBuilder->addFilter(ProductInterface::SKU, $this->productSkus, 'in');
127+
$skusToFetch = array_diff($this->productSkus, array_keys($this->productList));
128+
129+
if (empty($skusToFetch)) {
130+
return;
131+
}
132+
133+
$this->searchCriteriaBuilder->addFilter(ProductInterface::SKU, $skusToFetch, 'in');
133134
$result = $this->productDataProvider->getList(
134135
$this->searchCriteriaBuilder->create(),
135136
$this->attributeCodes,
@@ -142,7 +143,5 @@ private function fetch(ContextInterface $context = null) : array
142143
foreach ($result->getItems() as $product) {
143144
$this->productList[$product->getSku()] = ['model' => $product];
144145
}
145-
146-
return $this->productList;
147146
}
148147
}

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

Lines changed: 17 additions & 33 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;
@@ -47,11 +46,6 @@ class AddProductsToCart
4746
'The requested qty is not available' => self::ERROR_INSUFFICIENT_STOCK,
4847
];
4948

50-
/**
51-
* @var ProductRepositoryInterface
52-
*/
53-
private $productRepository;
54-
5549
/**
5650
* @var CartRepositoryInterface
5751
*/
@@ -68,31 +62,30 @@ class AddProductsToCart
6862
private $requestBuilder;
6963

7064
/**
71-
* @var SearchCriteriaBuilder
65+
* @var ProductReaderInterface
7266
*/
73-
private $searchCriteriaBuilder;
67+
private $productReader;
7468

7569
/**
7670
* @param ProductRepositoryInterface $productRepository
7771
* @param CartRepositoryInterface $cartRepository
7872
* @param MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId
7973
* @param BuyRequestBuilder $requestBuilder
80-
* @param SearchCriteriaBuilder|null $searchCriteriaBuilder
74+
* @param ProductReaderInterface|null $productReader
75+
*
76+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
8177
*/
8278
public function __construct(
8379
ProductRepositoryInterface $productRepository,
8480
CartRepositoryInterface $cartRepository,
8581
MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId,
8682
BuyRequestBuilder $requestBuilder,
87-
SearchCriteriaBuilder $searchCriteriaBuilder = null
83+
ProductReaderInterface $productReader = null
8884
) {
89-
$this->productRepository = $productRepository;
9085
$this->cartRepository = $cartRepository;
9186
$this->maskedQuoteIdToQuoteId = $maskedQuoteIdToQuoteId;
9287
$this->requestBuilder = $requestBuilder;
93-
$this->searchCriteriaBuilder = $searchCriteriaBuilder ?: ObjectManager::getInstance()->get(
94-
SearchCriteriaBuilder::class
95-
);
88+
$this->productReader = $productReader ?: ObjectManager::getInstance()->get(ProductReaderInterface::class);
9689
}
9790

9891
/**
@@ -134,11 +127,9 @@ public function execute(string $maskedCartId, array $cartItems): AddProductsToCa
134127
}
135128
}
136129
}
137-
138130
if ($saveCart) {
139131
$this->cartRepository->save($cart);
140132
}
141-
142133
if (count($allErrors) !== 0) {
143134
/* Revert changes introduced by add to cart processes in case of an error */
144135
$cart->getItemsCollection()->clear();
@@ -157,17 +148,14 @@ public function execute(string $maskedCartId, array $cartItems): AddProductsToCa
157148
public function addItemsToCart(Quote $cart, array $cartItems): array
158149
{
159150
$failedCartItems = [];
160-
$cartItemSkus = \array_map(
151+
// add new cart items for preload
152+
$skus = \array_map(
161153
function ($item) {
162154
return $item->getSku();
163155
},
164156
$cartItems
165157
);
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-
158+
$this->productReader->loadProducts($skus, $cart->getStoreId());
171159
foreach ($cartItems as $cartItemPosition => $cartItem) {
172160
$errors = $this->addItemToCart($cart, $cartItem, $cartItemPosition);
173161
if ($errors) {
@@ -191,24 +179,20 @@ private function addItemToCart(Quote $cart, Data\CartItem $cartItem, int $cartIt
191179
$sku = $cartItem->getSku();
192180
$errors = [];
193181
$result = null;
194-
$product = null;
195182

196183
if ($cartItem->getQuantity() <= 0) {
197184
$errors[] = $this->createError(
198185
__('The product quantity should be greater than 0')->render(),
199186
$cartItemPosition
200187
);
201188
} else {
202-
try {
203-
$product = $this->productRepository->get($sku, false, $cart->getStoreId(), false);
204-
} catch (NoSuchEntityException $e) {
189+
$product = $this->productReader->getProductBySku($sku);
190+
if (!$product || !$product->isSaleable() || !$product->isAvailable()) {
205191
$errors[] = $this->createError(
206192
__('Could not find a product with SKU "%sku"', ['sku' => $sku])->render(),
207193
$cartItemPosition
208194
);
209-
}
210-
211-
if ($product !== null) {
195+
} else {
212196
try {
213197
$result = $cart->addProduct($product, $this->requestBuilder->build($cartItem));
214198
} catch (\Throwable $e) {
@@ -217,11 +201,11 @@ private function addItemToCart(Quote $cart, Data\CartItem $cartItem, int $cartIt
217201
$cartItemPosition
218202
);
219203
}
204+
}
220205

221-
if (is_string($result)) {
222-
foreach (array_unique(explode("\n", $result)) as $error) {
223-
$errors[] = $this->createError(__($error)->render(), $cartItemPosition);
224-
}
206+
if (is_string($result)) {
207+
foreach (array_unique(explode("\n", $result)) as $error) {
208+
$errors[] = $this->createError(__($error)->render(), $cartItemPosition);
225209
}
226210
}
227211
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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\Collection;
12+
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory;
13+
use Magento\Quote\Model\Quote\Config;
14+
15+
/**
16+
* Cart reader product loader.
17+
*/
18+
class ProductReader implements ProductReaderInterface
19+
{
20+
/**
21+
* @var ProductCollectionFactory
22+
*/
23+
private $productCollectionFactory;
24+
25+
/**
26+
* @var ProductInterface[]
27+
*/
28+
private $productsBySku;
29+
30+
/**
31+
* @var Config
32+
*/
33+
private $quoteConfig;
34+
35+
/**
36+
* @var Collection
37+
*/
38+
private $productCollection;
39+
40+
/**
41+
* @param ProductCollectionFactory $productCollectionFactory
42+
* @param Config $quoteConfig
43+
*/
44+
public function __construct(
45+
ProductCollectionFactory $productCollectionFactory,
46+
Config $quoteConfig
47+
) {
48+
$this->productCollectionFactory = $productCollectionFactory;
49+
$this->quoteConfig = $quoteConfig;
50+
}
51+
52+
/**
53+
* @inheritDoc
54+
*/
55+
public function loadProducts(array $skus, int $storeId): void
56+
{
57+
$this->productCollection = $this->productCollectionFactory->create();
58+
59+
$this->productCollection->addAttributeToSelect($this->quoteConfig->getProductAttributes());
60+
$this->productCollection->setStoreId($storeId);
61+
$this->productCollection->addStoreFilter($storeId);
62+
$this->productCollection->addFieldToFilter(ProductInterface::SKU, ['in' => $skus]);
63+
$this->productCollection->joinAttribute('status', 'catalog_product/status', 'entity_id', null, 'inner');
64+
$this->productCollection->joinAttribute('visibility', 'catalog_product/visibility', 'entity_id', null, 'inner');
65+
$this->productCollection->load();
66+
foreach ($this->productCollection->getItems() as $productItem) {
67+
$this->productsBySku[$productItem->getData(ProductInterface::SKU)] = $productItem;
68+
}
69+
}
70+
71+
/**
72+
* @inheritDoc
73+
*/
74+
public function getProductBySku(string $sku) : ?ProductInterface
75+
{
76+
return $this->productsBySku[$sku] ?? null;
77+
}
78+
}
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: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1843,8 +1843,14 @@ public function updateItem($itemId, $buyRequest, $params = null)
18431843
*/
18441844
public function getItemByProduct($product)
18451845
{
1846-
foreach ($this->getAllItems() as $item) {
1847-
if ($item->representProduct($product)) {
1846+
/** @var \Magento\Quote\Model\Quote\Item[] $items */
1847+
$items = $this->getItemsCollection()->getItemsByColumnValue('product_id', $product->getId());
1848+
foreach ($items as $item) {
1849+
if (!$item->isDeleted()
1850+
&& $item->getProduct()
1851+
&& $item->getProduct()->getStatus() !== ProductStatus::STATUS_DISABLED
1852+
&& $item->representProduct($product)
1853+
) {
18481854
return $item;
18491855
}
18501856
}

app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ private function removeItemsWithAbsentProducts(): void
382382

383383
$productCollection = $this->_productCollectionFactory->create()->addIdFilter($this->_productIds);
384384
$existingProductsIds = $productCollection->getAllIds();
385-
$absentProductsIds = array_diff($this->_productIds, $existingProductsIds);
385+
$absentProductsIds = array_unique(array_diff($this->_productIds, $existingProductsIds));
386386
// Remove not existing products from items collection
387387
if (!empty($absentProductsIds)) {
388388
foreach ($absentProductsIds as $productIdToExclude) {

0 commit comments

Comments
 (0)