Skip to content

Commit efc3131

Browse files
author
Munkh-Ulzii Balidar
committed
29251 Configurable Options Selection :: New GraphQL Query #29251
29978 add code refactoring 29978 add more code refactoring 29251 fix test issues 29251 fix static test 29251 add code refactoring 29251 add backward compatible class 29251 add refactoring, fix media gallery 29251 fix static test 29251 fix empty options 29251 fix media gallery 29251 fix web-api test 29251 add debug in failing web-api test 29251 remove debug 29251 add debug in failing web-api test 29251 add debug in failing web-api test 29251 add variant data provider 29251 clean up code 29251 clean up code
1 parent 1dc62a7 commit efc3131

File tree

14 files changed

+1213
-1
lines changed

14 files changed

+1213
-1
lines changed

app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,14 @@
1010
use Magento\Catalog\Api\Data\ProductInterface;
1111
use Magento\Catalog\Model\Product;
1212
use Magento\Catalog\Model\ProductFactory;
13+
use Magento\Catalog\Model\ProductRepository;
14+
use Magento\ConfigurableProduct\Helper\Data;
1315
use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute;
1416
use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Attribute\Collection
1517
as AttributeCollection;
1618
use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Attribute\CollectionFactory;
19+
use Magento\Framework\Api\SearchCriteriaBuilder;
20+
use Magento\Framework\App\ObjectManager;
1721
use Magento\Framework\EntityManager\MetadataPool;
1822

1923
/**
@@ -31,11 +35,36 @@ class Collection
3135
*/
3236
private $productFactory;
3337

38+
/**
39+
* @var ProductRepository
40+
*/
41+
private $productRepository;
42+
3443
/**
3544
* @var MetadataPool
3645
*/
3746
private $metadataPool;
3847

48+
/**
49+
* @var Data
50+
*/
51+
private $configurableProductHelper;
52+
53+
/**
54+
* @var Metadata
55+
*/
56+
private $optionsMetadata;
57+
58+
/**
59+
* @var SelectionUidFormatter
60+
*/
61+
private $selectionUidFormatter;
62+
63+
/**
64+
* @var SearchCriteriaBuilder
65+
*/
66+
private $searchCriteriaBuilder;
67+
3968
/**
4069
* @var int[]
4170
*/
@@ -49,16 +78,32 @@ class Collection
4978
/**
5079
* @param CollectionFactory $attributeCollectionFactory
5180
* @param ProductFactory $productFactory
81+
* @param ProductRepository $productRepository
5282
* @param MetadataPool $metadataPool
83+
* @param Data $configurableProductHelper
84+
* @param Metadata $optionsMetadata
85+
* @param SelectionUidFormatter $selectionUidFormatter
86+
* @param SearchCriteriaBuilder $searchCriteriaBuilder
5387
*/
5488
public function __construct(
5589
CollectionFactory $attributeCollectionFactory,
5690
ProductFactory $productFactory,
57-
MetadataPool $metadataPool
91+
ProductRepository $productRepository,
92+
MetadataPool $metadataPool,
93+
Data $configurableProductHelper,
94+
Metadata $optionsMetadata,
95+
SelectionUidFormatter $selectionUidFormatter,
96+
SearchCriteriaBuilder $searchCriteriaBuilder
5897
) {
5998
$this->attributeCollectionFactory = $attributeCollectionFactory;
6099
$this->productFactory = $productFactory;
100+
$this->productRepository = $productRepository;
61101
$this->metadataPool = $metadataPool;
102+
$this->configurableProductHelper = $configurableProductHelper;
103+
$this->optionsMetadata = $optionsMetadata;
104+
$this->selectionUidFormatter = $selectionUidFormatter;
105+
$this->searchCriteriaBuilder = $searchCriteriaBuilder ??
106+
ObjectManager::getInstance()->get(SearchCriteriaBuilder::class);
62107
}
63108

64109
/**
@@ -111,6 +156,8 @@ private function fetch() : array
111156
$attributeCollection->setProductFilter($product);
112157
}
113158

159+
$products = $this->getProducts($this->productIds);
160+
114161
/** @var Attribute $attribute */
115162
foreach ($attributeCollection->getItems() as $attribute) {
116163
$productId = (int)$attribute->getProductId();
@@ -128,8 +175,37 @@ private function fetch() : array
128175
$this->attributeMap[$productId][$attribute->getId()]['values'] = $attributeData['options'];
129176
$this->attributeMap[$productId][$attribute->getId()]['label']
130177
= $attribute->getProductAttribute()->getStoreLabel();
178+
179+
if (isset($products[$productId])) {
180+
$options = $this->configurableProductHelper->getOptions(
181+
$products[$productId],
182+
$this->optionsMetadata->getAllowProducts($products[$productId])
183+
);
184+
foreach ($attributeData['options'] as $index => $value) {
185+
$this->attributeMap[$productId][$attribute->getId()]['values'][$index]['uid']
186+
= $this->selectionUidFormatter->encode((int)$attribute->getId(), (int)$value['value_index']);
187+
$this->attributeMap[$productId][$attribute->getId()]['values'][$index]
188+
['is_available_for_selection'] =
189+
isset($options[$attribute['attribute_id']][$value['value_index']])
190+
&& $options[$attribute['attribute_id']][$value['value_index']];
191+
}
192+
}
131193
}
132194

133195
return $this->attributeMap;
134196
}
197+
198+
/**
199+
* Load products by entity ids
200+
*
201+
* @param int[] $productIds
202+
* @return ProductInterface[]
203+
*/
204+
private function getProducts($productIds)
205+
{
206+
$this->searchCriteriaBuilder->addFilter('entity_id', $productIds, 'in');
207+
$searchCriteria = $this->searchCriteriaBuilder->create();
208+
$products = $this->productRepository->getList($searchCriteria)->getItems();
209+
return $products;
210+
}
135211
}
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+
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
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;
9+
10+
use Magento\Catalog\Api\Data\ProductInterface;
11+
use Magento\Catalog\Api\ProductRepositoryInterface;
12+
use Magento\ConfigurableProduct\Helper\Data;
13+
use Magento\ConfigurableProductGraphQl\Model\Options\DataProvider\Variant;
14+
use Magento\Framework\Exception\NoSuchEntityException;
15+
16+
/**
17+
* Retrieve metadata for configurable option selection.
18+
*/
19+
class Metadata
20+
{
21+
/**
22+
* @var Data
23+
*/
24+
private $configurableProductHelper;
25+
26+
/**
27+
* @var SelectionUidFormatter
28+
*/
29+
private $selectionUidFormatter;
30+
31+
/**
32+
* @var ProductRepositoryInterface
33+
*/
34+
private $productRepository;
35+
36+
/**
37+
* @var Variant
38+
*/
39+
private $variant;
40+
41+
/**
42+
* @param Data $configurableProductHelper
43+
* @param SelectionUidFormatter $selectionUidFormatter
44+
* @param ProductRepositoryInterface $productRepository
45+
* @param Variant $variant
46+
*/
47+
public function __construct(
48+
Data $configurableProductHelper,
49+
SelectionUidFormatter $selectionUidFormatter,
50+
ProductRepositoryInterface $productRepository,
51+
Variant $variant
52+
) {
53+
$this->configurableProductHelper = $configurableProductHelper;
54+
$this->selectionUidFormatter = $selectionUidFormatter;
55+
$this->productRepository = $productRepository;
56+
$this->variant = $variant;
57+
}
58+
59+
/**
60+
* Load available selections from configurable options.
61+
*
62+
* @param ProductInterface $product
63+
* @param array $selectedOptionsUid
64+
* @return array
65+
* @throws NoSuchEntityException
66+
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
67+
*/
68+
public function getAvailableSelections(
69+
ProductInterface $product,
70+
array $selectedOptionsUid
71+
): array {
72+
$options = $this->configurableProductHelper->getOptions($product, $this->getAllowProducts($product));
73+
$selectedOptions = $this->selectionUidFormatter->extract($selectedOptionsUid);
74+
$attributeCodes = $this->getAttributeCodes($product);
75+
$availableSelections = $availableProducts = $variantData = [];
76+
77+
if (isset($options['index']) && $options['index']) {
78+
foreach ($options['index'] as $productId => $productOptions) {
79+
if (!empty($selectedOptions) && !$this->hasProductRequiredOptions($selectedOptions, $productOptions)) {
80+
continue;
81+
}
82+
83+
$availableProducts[] = $productId;
84+
foreach ($productOptions as $attributeId => $optionIndex) {
85+
$uid = $this->selectionUidFormatter->encode($attributeId, (int)$optionIndex);
86+
87+
if (isset($availableSelections[$attributeId]['option_value_uids'])
88+
&& in_array($uid, $availableSelections[$attributeId]['option_value_uids'])
89+
) {
90+
continue;
91+
}
92+
$availableSelections[$attributeId]['option_value_uids'][] = $uid;
93+
$availableSelections[$attributeId]['attribute_code'] = $attributeCodes[$attributeId];
94+
}
95+
96+
if ($this->hasSelectionProduct($selectedOptions, $productOptions)) {
97+
$variantProduct = $this->productRepository->getById($productId);
98+
$variantData = $variantProduct->getData();
99+
$variantData['model'] = $variantProduct;
100+
}
101+
}
102+
}
103+
104+
return [
105+
'options_available_for_selection' => $availableSelections,
106+
'variant' => $variantData,
107+
'availableSelectionProducts' => array_unique($availableProducts),
108+
'product' => $product
109+
];
110+
}
111+
112+
/**
113+
* Get allowed products.
114+
*
115+
* @param ProductInterface $product
116+
* @return ProductInterface[]
117+
*/
118+
public function getAllowProducts(ProductInterface $product): array
119+
{
120+
return $this->variant->getSalableVariantsByParent($product) ?? [];
121+
}
122+
123+
/**
124+
* Check if a product has the selected options.
125+
*
126+
* @param array $requiredOptions
127+
* @param array $productOptions
128+
* @return bool
129+
*/
130+
private function hasProductRequiredOptions($requiredOptions, $productOptions): bool
131+
{
132+
$result = true;
133+
foreach ($requiredOptions as $attributeId => $optionIndex) {
134+
if (!isset($productOptions[$attributeId]) || !$productOptions[$attributeId]
135+
|| $optionIndex != $productOptions[$attributeId]
136+
) {
137+
$result = false;
138+
break;
139+
}
140+
}
141+
142+
return $result;
143+
}
144+
145+
/**
146+
* Check if selected options match a product.
147+
*
148+
* @param array $requiredOptions
149+
* @param array $productOptions
150+
* @return bool
151+
*/
152+
private function hasSelectionProduct($requiredOptions, $productOptions): bool
153+
{
154+
return $this->hasProductRequiredOptions($productOptions, $requiredOptions);
155+
}
156+
157+
/**
158+
* Retrieve attribute codes
159+
*
160+
* @param ProductInterface $product
161+
* @return string[]
162+
*/
163+
private function getAttributeCodes(ProductInterface $product): array
164+
{
165+
$allowedAttributes = $this->configurableProductHelper->getAllowAttributes($product);
166+
$attributeCodes = [];
167+
foreach ($allowedAttributes as $attribute) {
168+
$attributeCodes[$attribute->getAttributeId()] = $attribute->getProductAttribute()->getAttributeCode();
169+
}
170+
171+
return $attributeCodes;
172+
}
173+
}

0 commit comments

Comments
 (0)