Skip to content

Commit f13bfd7

Browse files
authored
Merge pull request #6255 from magento-honey-badgers/29251-configurable-options-selection
[honey] Configurable option/variants optimization PR
2 parents 39244bf + badf7a1 commit f13bfd7

File tree

19 files changed

+1266
-21
lines changed

19 files changed

+1266
-21
lines changed

app/code/Magento/ConfigurableProduct/Helper/Data.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
namespace Magento\ConfigurableProduct\Helper;
88

99
use Magento\Catalog\Model\Product\Image\UrlBuilder;
10+
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
1011
use Magento\Framework\App\ObjectManager;
1112
use Magento\Catalog\Helper\Image as ImageHelper;
1213
use Magento\Catalog\Api\Data\ProductInterface;
14+
use Magento\Catalog\Model\Product;
1315
use Magento\Catalog\Model\Product\Image;
1416

1517
/**
@@ -73,7 +75,7 @@ public function getGalleryImages(ProductInterface $product)
7375
/**
7476
* Get Options for Configurable Product Options
7577
*
76-
* @param \Magento\Catalog\Model\Product $currentProduct
78+
* @param Product $currentProduct
7779
* @param array $allowedProducts
7880
* @return array
7981
*/
@@ -100,11 +102,13 @@ public function getOptions($currentProduct, $allowedProducts)
100102
/**
101103
* Get allowed attributes
102104
*
103-
* @param \Magento\Catalog\Model\Product $product
105+
* @param Product $product
104106
* @return array
105107
*/
106108
public function getAllowAttributes($product)
107109
{
108-
return $product->getTypeInstance()->getConfigurableAttributes($product);
110+
return ($product->getTypeId() == Configurable::TYPE_CODE)
111+
? $product->getTypeInstance()->getConfigurableAttributes($product)
112+
: [];
109113
}
110114
}

app/code/Magento/ConfigurableProduct/Test/Unit/Helper/DataTest.php

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ protected function setUp(): void
4949
->getMock();
5050
$this->_imageHelperMock = $this->createMock(Image::class);
5151
$this->_productMock = $this->createMock(Product::class);
52-
52+
$this->_productMock->setTypeId(Configurable::TYPE_CODE);
5353
$this->_model = $objectManager->getObject(
5454
Data::class,
5555
[
@@ -66,6 +66,10 @@ public function testGetAllowAttributes()
6666
->method('getConfigurableAttributes')
6767
->with($this->_productMock);
6868

69+
$this->_productMock->expects($this->once())
70+
->method('getTypeId')
71+
->willReturn(Configurable::TYPE_CODE);
72+
6973
$this->_productMock->expects($this->once())
7074
->method('getTypeInstance')
7175
->willReturn($typeInstanceMock);
@@ -114,12 +118,16 @@ public function testGetOptions(array $expected, array $data)
114118

115119
/**
116120
* @return array
121+
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
117122
*/
118-
public function getOptionsDataProvider()
123+
public function getOptionsDataProvider(): array
119124
{
120125
$currentProductMock = $this->createPartialMock(
121126
Product::class,
122-
['getTypeInstance']
127+
[
128+
'getTypeInstance',
129+
'getTypeId'
130+
]
123131
);
124132
$provider = [];
125133
$provider[] = [
@@ -156,6 +164,9 @@ public function getOptionsDataProvider()
156164
$typeInstanceMock->expects($this->any())
157165
->method('getConfigurableAttributes')
158166
->willReturn($attributes);
167+
$currentProductMock->expects($this->any())
168+
->method('getTypeId')
169+
->willReturn(Configurable::TYPE_CODE);
159170
$currentProductMock->expects($this->any())
160171
->method('getTypeInstance')
161172
->willReturn($typeInstanceMock);
@@ -215,7 +226,7 @@ public function getOptionsDataProvider()
215226
* @param string $key
216227
* @return string
217228
*/
218-
public function getDataCallback($key)
229+
public function getDataCallback($key): string
219230
{
220231
$map = [];
221232
for ($k = 1; $k < 3; $k++) {
@@ -279,7 +290,7 @@ public function testGetGalleryImages()
279290
/**
280291
* @return Collection
281292
*/
282-
private function getImagesCollection()
293+
private function getImagesCollection(): MockObject
283294
{
284295
$collectionMock = $this->getMockBuilder(Collection::class)
285296
->disableOriginalConstructor()

app/code/Magento/ConfigurableProductGraphQl/Model/Cart/BuyRequest/SuperAttributeDataProvider.php

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,17 @@
88
namespace Magento\ConfigurableProductGraphQl\Model\Cart\BuyRequest;
99

1010
use Magento\Catalog\Api\Data\ProductInterface;
11+
use Magento\Catalog\Api\ProductRepositoryInterface;
12+
use Magento\CatalogInventory\Api\StockStateInterface;
13+
use Magento\ConfigurableProductGraphQl\Model\Options\Collection as OptionCollection;
14+
use Magento\Framework\EntityManager\MetadataPool;
15+
use Magento\Framework\Exception\LocalizedException;
1116
use Magento\Framework\Exception\NoSuchEntityException;
1217
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
1318
use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException;
1419
use Magento\Framework\Stdlib\ArrayManager;
20+
use Magento\Quote\Model\Quote;
1521
use Magento\QuoteGraphQl\Model\Cart\BuyRequest\BuyRequestDataProviderInterface;
16-
use Magento\Catalog\Api\ProductRepositoryInterface;
17-
use Magento\ConfigurableProductGraphQl\Model\Options\Collection as OptionCollection;
18-
use Magento\Framework\EntityManager\MetadataPool;
1922

2023
/**
2124
* DataProvider for building super attribute options in buy requests
@@ -42,22 +45,30 @@ class SuperAttributeDataProvider implements BuyRequestDataProviderInterface
4245
*/
4346
private $metadataPool;
4447

48+
/**
49+
* @var StockStateInterface
50+
*/
51+
private $stockState;
52+
4553
/**
4654
* @param ArrayManager $arrayManager
4755
* @param ProductRepositoryInterface $productRepository
4856
* @param OptionCollection $optionCollection
4957
* @param MetadataPool $metadataPool
58+
* @param StockStateInterface $stockState
5059
*/
5160
public function __construct(
5261
ArrayManager $arrayManager,
5362
ProductRepositoryInterface $productRepository,
5463
OptionCollection $optionCollection,
55-
MetadataPool $metadataPool
64+
MetadataPool $metadataPool,
65+
StockStateInterface $stockState
5666
) {
5767
$this->arrayManager = $arrayManager;
5868
$this->productRepository = $productRepository;
5969
$this->optionCollection = $optionCollection;
6070
$this->metadataPool = $metadataPool;
71+
$this->stockState = $stockState;
6172
}
6273

6374
/**
@@ -70,13 +81,20 @@ public function execute(array $cartItemData): array
7081
return [];
7182
}
7283
$sku = $this->arrayManager->get('data/sku', $cartItemData);
73-
84+
$qty = $this->arrayManager->get('data/quantity', $cartItemData);
85+
$cart = $this->arrayManager->get('model', $cartItemData);
86+
if (!$cart instanceof Quote) {
87+
throw new LocalizedException(__('"model" value should be specified'));
88+
}
7489
try {
7590
$parentProduct = $this->productRepository->get($parentSku);
7691
$product = $this->productRepository->get($sku);
7792
} catch (NoSuchEntityException $e) {
7893
throw new GraphQlNoSuchEntityException(__('Could not find specified product.'));
7994
}
95+
96+
$this->checkProductStock($sku, (float) $qty, (int) $cart->getStoreId());
97+
8098
$configurableProductLinks = $parentProduct->getExtensionAttributes()->getConfigurableProductLinks();
8199
if (!in_array($product->getId(), $configurableProductLinks)) {
82100
throw new GraphQlInputException(__('Could not find specified product.'));
@@ -95,6 +113,47 @@ public function execute(array $cartItemData): array
95113
}
96114
}
97115
}
116+
$this->checkSuperAttributeData($parentSku, $superAttributesData);
117+
98118
return ['super_attribute' => $superAttributesData];
99119
}
120+
121+
/**
122+
* Stock check for a product
123+
*
124+
* @param string $sku
125+
* @param float $qty
126+
* @param int $scopeId
127+
*/
128+
private function checkProductStock(string $sku, float $qty, int $scopeId): void
129+
{
130+
// Child stock check has to be performed a catalog by default would not show/check it
131+
$childProduct = $this->productRepository->get($sku, false, null, true);
132+
133+
$result = $this->stockState->checkQuoteItemQty($childProduct->getId(), $qty, $qty, $qty, $scopeId);
134+
135+
if ($result->getHasError()) {
136+
throw new LocalizedException(
137+
__($result->getMessage())
138+
);
139+
}
140+
}
141+
142+
/**
143+
* Check super attribute data.
144+
*
145+
* Some options might be disabled and/or available when parent and child sku are provided.
146+
*
147+
* @param string $parentSku
148+
* @param array $superAttributesData
149+
* @throws LocalizedException
150+
*/
151+
private function checkSuperAttributeData(string $parentSku, array $superAttributesData): void
152+
{
153+
if (empty($superAttributesData)) {
154+
throw new LocalizedException(
155+
__('The product with SKU %sku is out of stock.', ['sku' => $parentSku])
156+
);
157+
}
158+
}
100159
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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\ConfigurableProductGraphQl\Model\Options\DataProvider;
9+
10+
use Magento\Catalog\Api\Data\ProductInterface;
11+
use Magento\CatalogInventory\Model\ResourceModel\Stock\StatusFactory;
12+
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
13+
14+
/**
15+
* Retrieve child products
16+
*/
17+
class Variant
18+
{
19+
/**
20+
* @var Configurable
21+
*/
22+
private $configurableType;
23+
24+
/**
25+
* @var StatusFactory
26+
*/
27+
private $stockStatusFactory;
28+
29+
/**
30+
* @param Configurable $configurableType
31+
* @param StatusFactory $stockStatusFactory
32+
*/
33+
public function __construct(
34+
Configurable $configurableType,
35+
StatusFactory $stockStatusFactory
36+
) {
37+
$this->configurableType = $configurableType;
38+
$this->stockStatusFactory = $stockStatusFactory;
39+
}
40+
41+
/**
42+
* Load available child products by parent
43+
*
44+
* @param ProductInterface $product
45+
* @return ProductInterface[]
46+
* @throws \Magento\Framework\Exception\LocalizedException
47+
*/
48+
public function getSalableVariantsByParent(ProductInterface $product)
49+
{
50+
$collection = $this->configurableType->getUsedProductCollection($product);
51+
$collection
52+
->addAttributeToSelect('*')
53+
->addFilterByRequiredOptions();
54+
$collection->addMediaGalleryData();
55+
$collection->addTierPriceData();
56+
57+
$stockFlag = 'has_stock_status_filter';
58+
if (!$collection->hasFlag($stockFlag)) {
59+
$stockStatusResource = $this->stockStatusFactory->create();
60+
$stockStatusResource->addStockDataToCollection($collection, true);
61+
$collection->setFlag($stockFlag, true);
62+
}
63+
$collection->clear();
64+
65+
return $collection->getItems();
66+
}
67+
}

0 commit comments

Comments
 (0)