diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/Option.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/Option.php
new file mode 100644
index 0000000000000..68968b6f3819a
--- /dev/null
+++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/Option.php
@@ -0,0 +1,63 @@
+idEncoder = $idEncoder;
+ $this->valueFormatter = $valueFormatter;
+ }
+
+ /**
+ * Format configurable product options according to the GraphQL schema
+ *
+ * @param Attribute $attribute
+ * @param array $optionIds
+ * @return array|null
+ */
+ public function format(Attribute $attribute, array $optionIds): ?array
+ {
+ $optionValues = [];
+
+ foreach ($attribute->getOptions() as $option) {
+ $optionValues[] = $this->valueFormatter->format($option, $attribute, $optionIds);
+ }
+
+ return [
+ 'uid' => $this->idEncoder->encode($attribute->getProductSuperAttributeId()),
+ 'attribute_code' => $attribute->getProductAttribute()->getAttributeCode(),
+ 'label' => $attribute->getLabel(),
+ 'values' => $optionValues,
+ ];
+ }
+}
diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/OptionValue.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/OptionValue.php
new file mode 100644
index 0000000000000..5d721f13fbb9d
--- /dev/null
+++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/OptionValue.php
@@ -0,0 +1,83 @@
+selectionUidFormatter = $selectionUidFormatter;
+ $this->stockRegistry = $stockRegistry;
+ }
+
+ /**
+ * Format configurable product option values according to the GraphQL schema
+ *
+ * @param array $optionValue
+ * @param Attribute $attribute
+ * @param array $optionIds
+ * @return array
+ */
+ public function format(array $optionValue, Attribute $attribute, array $optionIds): array
+ {
+ $valueIndex = (int)$optionValue['value_index'];
+ $attributeId = (int)$attribute->getAttributeId();
+
+ return [
+ 'uid' => $this->selectionUidFormatter->encode(
+ $attributeId,
+ $valueIndex
+ ),
+ 'is_available' => $this->getIsAvailable($optionIds[$valueIndex] ?? []),
+ 'is_use_default' => (bool)$attribute->getIsUseDefault(),
+ 'label' => $optionValue['label'],
+ 'value_index' => $optionValue['value_index']
+ ];
+ }
+
+ /**
+ * Get is variants available
+ *
+ * @param array $variantIds
+ * @return bool
+ */
+ private function getIsAvailable(array $variantIds): bool
+ {
+ foreach ($variantIds as $variantId) {
+ if ($this->stockRegistry->getProductStockStatus($variantId)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/Variant.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/Variant.php
new file mode 100644
index 0000000000000..1d73ad6a19336
--- /dev/null
+++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Formatter/Variant.php
@@ -0,0 +1,49 @@
+ $selectedValue) {
+ if (!isset($options[$attributeId][$selectedValue])) {
+ throw new GraphQlInputException(__('configurableOptionValueUids values are incorrect'));
+ }
+
+ $productIds = array_intersect($productIds, $options[$attributeId][$selectedValue]);
+ }
+
+ if (count($productIds) === 1) {
+ $variantProduct = $variants[array_pop($productIds)];
+ $variant = $variantProduct->getData();
+ $variant['url_path'] = $variantProduct->getProductUrl();
+ $variant['model'] = $variantProduct;
+ }
+
+ return $variant;
+ }
+}
diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/ConfigurableOptionsMetadata.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/ConfigurableOptionsMetadata.php
new file mode 100644
index 0000000000000..ce81e970bcd58
--- /dev/null
+++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/ConfigurableOptionsMetadata.php
@@ -0,0 +1,86 @@
+configurableProductHelper = $configurableProductHelper;
+ $this->configurableOptionsFormatter = $configurableOptionsFormatter;
+ }
+
+ /**
+ * Load available selections from configurable options and variant.
+ *
+ * @param ProductInterface $product
+ * @param array $options
+ * @param array $selectedOptions
+ * @return array
+ */
+ public function getAvailableSelections(ProductInterface $product, array $options, array $selectedOptions): array
+ {
+ $attributes = $this->getAttributes($product);
+ $availableSelections = [];
+
+ foreach ($options as $attributeId => $option) {
+ if ($attributeId === 'index' || isset($selectedOptions[$attributeId])) {
+ continue;
+ }
+
+ $availableSelections[] = $this->configurableOptionsFormatter->format(
+ $attributes[$attributeId],
+ $options[$attributeId] ?? []
+ );
+ }
+
+ return $availableSelections;
+ }
+
+ /**
+ * Retrieve configurable attributes for the product
+ *
+ * @param ProductInterface $product
+ * @return Attribute[]
+ */
+ private function getAttributes(ProductInterface $product): array
+ {
+ $allowedAttributes = $this->configurableProductHelper->getAllowAttributes($product);
+ $attributes = [];
+ foreach ($allowedAttributes as $attribute) {
+ $attributes[$attribute->getAttributeId()] = $attribute;
+ }
+
+ return $attributes;
+ }
+}
diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/DataProvider/Variant.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/DataProvider/Variant.php
index 80fbdc76bacb3..7b5a3fb806a5f 100644
--- a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/DataProvider/Variant.php
+++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/DataProvider/Variant.php
@@ -10,6 +10,7 @@
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\CatalogInventory\Model\ResourceModel\Stock\StatusFactory;
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
+use Magento\Framework\Exception\LocalizedException;
/**
* Retrieve child products
@@ -45,7 +46,7 @@ public function __construct(
* @return ProductInterface[]
* @throws \Magento\Framework\Exception\LocalizedException
*/
- public function getSalableVariantsByParent(ProductInterface $product)
+ public function getSalableVariantsByParent(ProductInterface $product): array
{
$collection = $this->configurableType->getUsedProductCollection($product);
$collection
@@ -62,6 +63,6 @@ public function getSalableVariantsByParent(ProductInterface $product)
}
$collection->clear();
- return $collection->getItems();
+ return $collection->getItems() ?? [];
}
}
diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/SelectionUidFormatter.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/SelectionUidFormatter.php
index 1d13ad75489a1..8c82c9414763f 100644
--- a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/SelectionUidFormatter.php
+++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/SelectionUidFormatter.php
@@ -3,8 +3,12 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+
namespace Magento\ConfigurableProductGraphQl\Model\Options;
+use Magento\Framework\GraphQl\Exception\GraphQlInputException;
+use Magento\Framework\GraphQl\Query\Uid;
+
/**
* Handle option selection uid.
*/
@@ -20,6 +24,19 @@ class SelectionUidFormatter
*/
private const UID_SEPARATOR = '/';
+ /**
+ * @var Uid
+ */
+ private $idEncoder;
+
+ /**
+ * @param Uid $idEncoder
+ */
+ public function __construct(Uid $idEncoder)
+ {
+ $this->idEncoder = $idEncoder;
+ }
+
/**
* Create uid and encode.
*
@@ -29,28 +46,22 @@ class SelectionUidFormatter
*/
public function encode(int $attributeId, int $indexId): string
{
- // phpcs:ignore Magento2.Functions.DiscouragedFunction
- return base64_encode(implode(self::UID_SEPARATOR, [
- self::UID_PREFIX,
- $attributeId,
- $indexId
- ]));
+ return $this->idEncoder->encode(implode(self::UID_SEPARATOR, [self::UID_PREFIX, $attributeId, $indexId]));
}
/**
* Retrieve attribute and option index from uid. Array key is the id of attribute and value is the index of option
*
- * @param string $selectionUids
+ * @param array $selectionUids
* @return array
- * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ * @throws GraphQlInputException
*/
public function extract(array $selectionUids): array
{
$attributeOption = [];
foreach ($selectionUids as $uid) {
- // phpcs:ignore Magento2.Functions.DiscouragedFunction
- $optionData = explode(self::UID_SEPARATOR, base64_decode($uid));
- if (count($optionData) == 3) {
+ $optionData = explode(self::UID_SEPARATOR, $this->idEncoder->decode($uid));
+ if (count($optionData) === 3) {
$attributeOption[(int)$optionData[1]] = (int)$optionData[2];
}
}
diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/OptionsSelectionMetadata.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/OptionsSelectionMetadata.php
index 6aa3a9774b439..556aab7e39f7d 100644
--- a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/OptionsSelectionMetadata.php
+++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/OptionsSelectionMetadata.php
@@ -3,17 +3,23 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
+
namespace Magento\ConfigurableProductGraphQl\Model\Resolver;
-use Magento\Catalog\Api\Data\ProductInterface;
+use Magento\ConfigurableProduct\Helper\Data;
+use Magento\ConfigurableProductGraphQl\Model\Formatter\Variant as VariantFormatter;
+use Magento\ConfigurableProductGraphQl\Model\Options\ConfigurableOptionsMetadata;
+use Magento\ConfigurableProductGraphQl\Model\Options\DataProvider\Variant;
use Magento\ConfigurableProductGraphQl\Model\Options\Metadata;
+use Magento\ConfigurableProductGraphQl\Model\Options\SelectionUidFormatter;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
/**
- * Resolver class for option selection metadata.
+ * Resolver for options selection
*/
class OptionsSelectionMetadata implements ResolverInterface
{
@@ -22,13 +28,53 @@ class OptionsSelectionMetadata implements ResolverInterface
*/
private $configurableSelectionMetadata;
+ /**
+ * @var ConfigurableOptionsMetadata
+ */
+ private $configurableOptionsMetadata;
+
+ /**
+ * @var SelectionUidFormatter
+ */
+ private $selectionUidFormatter;
+
+ /**
+ * @var Variant
+ */
+ private $variant;
+
+ /**
+ * @var VariantFormatter
+ */
+ private $variantFormatter;
+
+ /**
+ * @var Data
+ */
+ private $configurableProductHelper;
+
/**
* @param Metadata $configurableSelectionMetadata
+ * @param ConfigurableOptionsMetadata $configurableOptionsMetadata
+ * @param SelectionUidFormatter $selectionUidFormatter
+ * @param Variant $variant
+ * @param VariantFormatter $variantFormatter
+ * @param Data $configurableProductHelper
*/
public function __construct(
- Metadata $configurableSelectionMetadata
+ Metadata $configurableSelectionMetadata,
+ ConfigurableOptionsMetadata $configurableOptionsMetadata,
+ SelectionUidFormatter $selectionUidFormatter,
+ Variant $variant,
+ VariantFormatter $variantFormatter,
+ Data $configurableProductHelper
) {
$this->configurableSelectionMetadata = $configurableSelectionMetadata;
+ $this->configurableOptionsMetadata = $configurableOptionsMetadata;
+ $this->selectionUidFormatter = $selectionUidFormatter;
+ $this->variant = $variant;
+ $this->variantFormatter = $variantFormatter;
+ $this->configurableProductHelper = $configurableProductHelper;
}
/**
@@ -40,10 +86,31 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
throw new LocalizedException(__('"model" value should be specified'));
}
- $selectedOptions = $args['configurableOptionValueUids'] ?? [];
- /** @var ProductInterface $product */
$product = $value['model'];
- return $this->configurableSelectionMetadata->getAvailableSelections($product, $selectedOptions);
+ $selectionUids = $args['configurableOptionValueUids'] ?? [];
+ $selectedOptions = $this->selectionUidFormatter->extract($selectionUids);
+
+ $variants = $this->variant->getSalableVariantsByParent($product);
+ $options = $this->configurableProductHelper->getOptions($product, $variants);
+
+ $configurableOptions = $this->configurableOptionsMetadata->getAvailableSelections(
+ $product,
+ $options,
+ $selectedOptions
+ );
+
+ $optionsAvailableForSelection = $this->configurableSelectionMetadata->getAvailableSelections(
+ $product,
+ $args['configurableOptionValueUids'] ?? []
+ );
+
+ return [
+ 'configurable_options' => $configurableOptions,
+ 'variant' => $this->variantFormatter->format($options, $selectedOptions, $variants),
+ 'model' => $product,
+ 'options_available_for_selection' => $optionsAvailableForSelection['options_available_for_selection'],
+ 'availableSelectionProducts' => $optionsAvailableForSelection['availableSelectionProducts']
+ ];
}
}
diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/SelectionMediaGallery.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/SelectionMediaGallery.php
index 4aa322e66df60..972e4a9fd629a 100644
--- a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/SelectionMediaGallery.php
+++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/SelectionMediaGallery.php
@@ -21,11 +21,11 @@ class SelectionMediaGallery implements ResolverInterface
*/
public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null)
{
- if (!isset($value['product']) || !$value['product']) {
+ if (!isset($value['model']) || !$value['model']) {
return null;
}
- $product = $value['product'];
+ $product = $value['model'];
$availableSelectionProducts = $value['availableSelectionProducts'];
$mediaGalleryEntries = [];
$usedProducts = $product->getTypeInstance()->getUsedProducts($product, null);
diff --git a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls
index 3e84a428765fc..d9ba786c84354 100644
--- a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls
+++ b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls
@@ -7,7 +7,7 @@ type Mutation {
type ConfigurableProduct implements ProductInterface, PhysicalProductInterface, CustomizableProductInterface @doc(description: "ConfigurableProduct defines basic features of a configurable product and its simple product variants") {
variants: [ConfigurableVariant] @doc(description: "An array of variants of products") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\ConfigurableVariant")
configurable_options: [ConfigurableProductOptions] @doc(description: "An array of linked simple product items") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\Options")
- configurable_product_options_selection(configurableOptionValueUids: [ID!]): ConfigurableProductOptionsSelection @doc(description: "Metadata for the specified configurable options selection") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\OptionsSelectionMetadata")
+ configurable_product_options_selection(configurableOptionValueUids: [ID!]): ConfigurableProductOptionsSelection @doc(description: "Specified configurable product options selection") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\OptionsSelectionMetadata")
}
type ConfigurableVariant @doc(description: "An array containing all the simple product variants of a configurable product") {
@@ -83,6 +83,7 @@ type ConfigurableWishlistItem implements WishlistItemInterface @doc(description:
type ConfigurableProductOptionsSelection @doc(description: "Metadata corresponding to the configurable options selection.") {
options_available_for_selection: [ConfigurableOptionAvailableForSelection!] @doc(description: "Configurable options available for further selection based on current selection.")
+ configurable_options: [ConfigurableProductOption!] @doc(description: "Configurable options available for further selection based on current selection.")
media_gallery: [MediaGalleryInterface!] @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\SelectionMediaGallery") @doc(description: "Product images and videos corresponding to the specified configurable options selection.")
variant: SimpleProduct @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\Variant\\Variant") @doc(description: "Variant represented by the specified configurable options selection. It is expected to be null, until selections are made for each configurable option.")
}
@@ -92,6 +93,20 @@ type ConfigurableOptionAvailableForSelection @doc(description: "Configurable opt
attribute_code: String! @doc(description: "Attribute code that uniquely identifies configurable option.")
}
+type ConfigurableProductOption {
+ uid: ID!
+ attribute_code: String!
+ label: String!
+ values: [ConfigurableProductOptionValue!]
+}
+
+type ConfigurableProductOptionValue {
+ uid: ID!
+ is_available: Boolean!
+ is_use_default: Boolean!
+ label: String!
+}
+
type StoreConfig @doc(description: "The type contains information about a store config") {
configurable_thumbnail_source : String @doc(description: "The configuration setting determines which thumbnail should be used in the cart for configurable products.")
}
diff --git a/app/code/Magento/SwatchesGraphQl/composer.json b/app/code/Magento/SwatchesGraphQl/composer.json
index 1b98b4044a2ff..959f0f201d2b3 100644
--- a/app/code/Magento/SwatchesGraphQl/composer.json
+++ b/app/code/Magento/SwatchesGraphQl/composer.json
@@ -9,6 +9,9 @@
"magento/module-catalog": "*",
"magento/module-catalog-graph-ql": "*"
},
+ "suggest": {
+ "magento/module-configurable-product-graph-ql": "*"
+ },
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/SwatchesGraphQl/etc/module.xml b/app/code/Magento/SwatchesGraphQl/etc/module.xml
index 6689f13db754e..71c336a8cd257 100644
--- a/app/code/Magento/SwatchesGraphQl/etc/module.xml
+++ b/app/code/Magento/SwatchesGraphQl/etc/module.xml
@@ -10,6 +10,7 @@
+
diff --git a/app/code/Magento/SwatchesGraphQl/etc/schema.graphqls b/app/code/Magento/SwatchesGraphQl/etc/schema.graphqls
index c51468ccd2856..3491568108daf 100644
--- a/app/code/Magento/SwatchesGraphQl/etc/schema.graphqls
+++ b/app/code/Magento/SwatchesGraphQl/etc/schema.graphqls
@@ -47,3 +47,7 @@ type TextSwatchData implements SwatchDataInterface {
type ColorSwatchData implements SwatchDataInterface {
}
+
+type ConfigurableProductOptionValue {
+ swatch: SwatchDataInterface @resolver(class: "Magento\\SwatchesGraphQl\\Model\\Resolver\\Product\\Options\\SwatchData")
+}
diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartSingleMutationTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartSingleMutationTest.php
index 659720578de10..5811344e4defd 100644
--- a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartSingleMutationTest.php
+++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartSingleMutationTest.php
@@ -3,18 +3,20 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+
declare(strict_types=1);
namespace Magento\GraphQl\ConfigurableProduct;
use Exception;
+use Magento\CatalogInventory\Model\Configuration;
use Magento\Config\Model\ResourceModel\Config;
+use Magento\ConfigurableProductGraphQl\Model\Options\SelectionUidFormatter;
use Magento\Framework\App\Config\ReinitableConfigInterface;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId;
use Magento\TestFramework\Helper\Bootstrap;
use Magento\TestFramework\TestCase\GraphQlAbstract;
-use Magento\CatalogInventory\Model\Configuration;
/**
* Add configurable product to cart testcases
@@ -41,6 +43,11 @@ class AddConfigurableProductToCartSingleMutationTest extends GraphQlAbstract
*/
private $reinitConfig;
+ /**
+ * @var SelectionUidFormatter
+ */
+ private $selectionUidFormatter;
+
/**
* @inheritdoc
*/
@@ -51,13 +58,14 @@ protected function setUp(): void
$this->resourceConfig = $objectManager->get(Config::class);
$this->scopeConfig = $objectManager->get(ScopeConfigInterface::class);
$this->reinitConfig = $objectManager->get(ReinitableConfigInterface::class);
+ $this->selectionUidFormatter = $objectManager->get(SelectionUidFormatter::class);
}
/**
* @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable.php
* @magentoApiDataFixture Magento/Checkout/_files/active_quote.php
*/
- public function testAddConfigurableProductToCart()
+ public function testAddConfigurableProductToCart(): void
{
$product = $this->getConfigurableProductInfo();
$quantity = 2;
@@ -77,7 +85,8 @@ public function testAddConfigurableProductToCart()
);
$response = $this->graphQlMutation($query);
- $expectedProductOptionsValueUid = $this->generateConfigurableSelectionUID($attributeId, $valueIndex);
+
+ $expectedProductOptionsValueUid = $this->selectionUidFormatter->encode($attributeId, $valueIndex);
$expectedProductOptionsUid = base64_encode("configurable/$productRowId/$attributeId");
$cartItem = current($response['addProductsToCart']['cart']['items']);
self::assertEquals($quantity, $cartItem['quantity']);
@@ -94,35 +103,11 @@ public function testAddConfigurableProductToCart()
self::assertArrayHasKey('value_label', $option);
}
- /**
- * Generates UID configurable product
- *
- * @param int $attributeId
- * @param int $valueIndex
- * @return string
- */
- private function generateConfigurableSelectionUID(int $attributeId, int $valueIndex): string
- {
- return base64_encode("configurable/$attributeId/$valueIndex");
- }
-
- /**
- * Generates UID for super configurable product super attributes
- *
- * @param int $attributeId
- * @param int $valueIndex
- * @return string
- */
- private function generateSuperAttributesUIDQuery(int $attributeId, int $valueIndex): string
- {
- return 'selected_options: ["' . $this->generateConfigurableSelectionUID($attributeId, $valueIndex) . '"]';
- }
-
/**
* @magentoApiDataFixture Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php
* @magentoApiDataFixture Magento/Checkout/_files/active_quote.php
*/
- public function testAddConfigurableProductWithWrongSuperAttributes()
+ public function testAddConfigurableProductWithWrongSuperAttributes(): void
{
$product = $this->getConfigurableProductInfo();
$quantity = 2;
@@ -150,7 +135,7 @@ public function testAddConfigurableProductWithWrongSuperAttributes()
* @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable_sku.php
* @magentoApiDataFixture Magento/Checkout/_files/active_quote.php
*/
- public function testAddProductIfQuantityIsNotAvailable()
+ public function testAddProductIfQuantityIsNotAvailable(): void
{
$product = $this->getConfigurableProductInfo();
$parentSku = $product['sku'];
@@ -179,7 +164,7 @@ public function testAddProductIfQuantityIsNotAvailable()
* @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable_sku.php
* @magentoApiDataFixture Magento/Checkout/_files/active_quote.php
*/
- public function testAddNonExistentConfigurableProductParentToCart()
+ public function testAddNonExistentConfigurableProductParentToCart(): void
{
$maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1');
$parentSku = 'configurable_no_exist';
@@ -203,7 +188,7 @@ public function testAddNonExistentConfigurableProductParentToCart()
* @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable_zero_qty_first_child.php
* @magentoApiDataFixture Magento/Checkout/_files/active_quote.php
*/
- public function testOutOfStockVariationToCart()
+ public function testOutOfStockVariationToCart(): void
{
$showOutOfStock = $this->scopeConfig->getValue(Configuration::XML_PATH_SHOW_OUT_OF_STOCK);
@@ -215,7 +200,7 @@ public function testOutOfStockVariationToCart()
$attributeId = (int) $product['configurable_options'][0]['attribute_id'];
$valueIndex = $product['configurable_options'][0]['values'][0]['value_index'];
// Asserting that the first value is the right option we want to add to cart
- $this->assertEquals(
+ self::assertEquals(
$product['configurable_options'][0]['values'][0]['label'],
'Option 1'
);
@@ -237,7 +222,7 @@ public function testOutOfStockVariationToCart()
'There are no source items with the in stock status',
'This product is out of stock.'
];
- $this->assertContains(
+ self::assertContains(
$response['addProductsToCart']['user_errors'][0]['message'],
$expectedErrorMessages
);
@@ -312,6 +297,18 @@ private function getConfigurableProductInfo(): array
return current($searchResponse['products']['items']);
}
+ /**
+ * Generates UID for super configurable product super attributes
+ *
+ * @param int $attributeId
+ * @param int $valueIndex
+ * @return string
+ */
+ private function generateSuperAttributesUIDQuery(int $attributeId, int $valueIndex): string
+ {
+ return 'selected_options: ["' . $this->selectionUidFormatter->encode($attributeId, $valueIndex) . '"]';
+ }
+
/**
* Returns GraphQl query for fetching configurable product information
*
@@ -354,10 +351,27 @@ private function getFetchProductQuery(string $term): string
attribute_code
option_value_uids
}
+ configurable_options {
+ uid
+ attribute_code
+ label
+ values {
+ uid
+ is_available
+ is_use_default
+ label
+ }
+ }
variant {
uid
- name
- attribute_set_id
+ sku
+ url_key
+ url_path
+ }
+ media_gallery {
+ url
+ label
+ disabled
}
}
}
diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableOptionsSelectionTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableOptionsSelectionTest.php
new file mode 100644
index 0000000000000..ac252acfcaa2b
--- /dev/null
+++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableOptionsSelectionTest.php
@@ -0,0 +1,379 @@
+attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepository::class);
+ $this->selectionUidFormatter = Bootstrap::getObjectManager()->create(SelectionUidFormatter::class);
+ $this->indexerFactory = Bootstrap::getObjectManager()->create(IndexerFactory::class);
+ $this->idEncoder = Bootstrap::getObjectManager()->create(Uid::class);
+ }
+
+ /**
+ * Test the first option of the first attribute selected
+ *
+ * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php
+ */
+ public function testSelectedFirstAttributeFirstOption(): void
+ {
+ $attribute = $this->getFirstConfigurableAttribute();
+ $options = $attribute->getOptions();
+ $sku = 'configurable_12345';
+ $firstOptionUid = $this->selectionUidFormatter->encode(
+ (int)$attribute->getAttributeId(),
+ (int)$options[1]->getValue()
+ );
+
+ $this->reindexAll();
+ $response = $this->graphQlQuery($this->getQuery($sku, [$firstOptionUid]));
+
+ self::assertNotEmpty($response['products']['items']);
+ $product = current($response['products']['items']);
+ self::assertEquals('ConfigurableProduct', $product['__typename']);
+ self::assertEquals($sku, $product['sku']);
+ self::assertNotEmpty($product['configurable_product_options_selection']['configurable_options']);
+ self::assertNull($product['configurable_product_options_selection']['variant']);
+ self::assertCount(1, $product['configurable_product_options_selection']['configurable_options']);
+ self::assertCount(4, $product['configurable_product_options_selection']['configurable_options'][0]['values']);
+
+ $secondAttributeOptions = $this->getSecondConfigurableAttribute()->getOptions();
+ $this->assertAvailableOptionUids(
+ $this->getSecondConfigurableAttribute()->getAttributeId(),
+ $secondAttributeOptions,
+ $this->getOptionsUids(
+ $product['configurable_product_options_selection']['configurable_options'][0]['values']
+ )
+ );
+
+ $this->assertMediaGallery($product);
+ }
+
+ /**
+ * Test selected variant
+ *
+ * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php
+ */
+ public function testSelectedVariant(): void
+ {
+ $firstAttribute = $this->getFirstConfigurableAttribute();
+ $firstOptions = $firstAttribute->getOptions();
+ $firstAttributeFirstOptionUid = $this->selectionUidFormatter->encode(
+ (int)$firstAttribute->getAttributeId(),
+ (int)$firstOptions[1]->getValue()
+ );
+ $secondAttribute = $this->getSecondConfigurableAttribute();
+ $secondOptions = $secondAttribute->getOptions();
+ $secondAttributeFirstOptionUid = $this->selectionUidFormatter->encode(
+ (int)$secondAttribute->getAttributeId(),
+ (int)$secondOptions[1]->getValue()
+ );
+
+ $sku = 'configurable_12345';
+
+ $this->reindexAll();
+ $response = $this->graphQlQuery(
+ $this->getQuery($sku, [$firstAttributeFirstOptionUid, $secondAttributeFirstOptionUid])
+ );
+
+ self::assertNotEmpty($response['products']['items']);
+ $product = current($response['products']['items']);
+ self::assertEquals('ConfigurableProduct', $product['__typename']);
+ self::assertEquals($sku, $product['sku']);
+ self::assertEmpty($product['configurable_product_options_selection']['configurable_options']);
+ self::assertNotNull($product['configurable_product_options_selection']['variant']);
+
+ $variantId = $this->idEncoder->decode($product['configurable_product_options_selection']['variant']['uid']);
+ self::assertIsNumeric($variantId);
+ self::assertIsString($product['configurable_product_options_selection']['variant']['sku']);
+ $urlKey = 'configurable-option-first-option-1-second-option-1';
+ self::assertEquals($urlKey, $product['configurable_product_options_selection']['variant']['url_key']);
+
+ $this->assertMediaGallery($product);
+ }
+
+ /**
+ * Test without selected options
+ *
+ * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php
+ */
+ public function testWithoutSelectedOption(): void
+ {
+ $sku = 'configurable_12345';
+ $this->reindexAll();
+ $response = $this->graphQlQuery($this->getQuery($sku));
+
+ self::assertNotEmpty($response['products']['items']);
+ $product = current($response['products']['items']);
+ self::assertEquals('ConfigurableProduct', $product['__typename']);
+ self::assertEquals($sku, $product['sku']);
+
+ self::assertNotEmpty($product['configurable_product_options_selection']['configurable_options']);
+ self::assertNull($product['configurable_product_options_selection']['variant']);
+ self::assertCount(2, $product['configurable_product_options_selection']['configurable_options']);
+ self::assertCount(4, $product['configurable_product_options_selection']['configurable_options'][0]['values']);
+ self::assertCount(4, $product['configurable_product_options_selection']['configurable_options'][1]['values']);
+
+ $firstAttributeOptions = $this->getFirstConfigurableAttribute()->getOptions();
+ $this->assertAvailableOptionUids(
+ $this->getFirstConfigurableAttribute()->getAttributeId(),
+ $firstAttributeOptions,
+ $this->getOptionsUids(
+ $product['configurable_product_options_selection']['configurable_options'][0]['values']
+ )
+ );
+
+ $secondAttributeOptions = $this->getSecondConfigurableAttribute()->getOptions();
+ $this->assertAvailableOptionUids(
+ $this->getSecondConfigurableAttribute()->getAttributeId(),
+ $secondAttributeOptions,
+ $this->getOptionsUids(
+ $product['configurable_product_options_selection']['configurable_options'][1]['values']
+ )
+ );
+
+ $this->assertMediaGallery($product);
+ }
+
+ /**
+ * Test with wrong selected options
+ *
+ * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php
+ */
+ public function testWithWrongSelectedOptions(): void
+ {
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage('configurableOptionValueUids values are incorrect');
+
+ $attribute = $this->getFirstConfigurableAttribute();
+ $options = $attribute->getOptions();
+ $sku = 'configurable_12345';
+ $firstOptionUid = $this->selectionUidFormatter->encode(
+ (int)$attribute->getAttributeId(),
+ $options[1]->getValue() + 100
+ );
+
+ $this->reindexAll();
+ $this->graphQlQuery($this->getQuery($sku, [$firstOptionUid]));
+ }
+
+ /**
+ * Get GraphQL query to test configurable product options selection
+ *
+ * @param string $productSku
+ * @param array $optionValueUids
+ * @param int $pageSize
+ * @param int $currentPage
+ * @return string
+ */
+ private function getQuery(
+ string $productSku,
+ array $optionValueUids = [],
+ int $pageSize = 20,
+ int $currentPage = 1
+ ): string {
+ if (empty($optionValueUids)) {
+ $configurableOptionValueUids = '';
+ } else {
+ $configurableOptionValueUids = '(configurableOptionValueUids: [';
+ foreach ($optionValueUids as $configurableOptionValueUid) {
+ $configurableOptionValueUids .= '"' . $configurableOptionValueUid . '",';
+ }
+ $configurableOptionValueUids .= '])';
+ }
+
+ return <<firstConfigurableAttribute) {
+ $this->firstConfigurableAttribute = $this->attributeRepository->get(
+ 'catalog_product',
+ 'test_configurable_first'
+ );
+ }
+
+ return $this->firstConfigurableAttribute;
+ }
+
+ /**
+ * Get second configurable attribute.
+ *
+ * @return AttributeInterface
+ * @throws NoSuchEntityException
+ */
+ private function getSecondConfigurableAttribute(): AttributeInterface
+ {
+ if (!$this->secondConfigurableAttribute) {
+ $this->secondConfigurableAttribute = $this->attributeRepository->get(
+ 'catalog_product',
+ 'test_configurable_second'
+ );
+ }
+
+ return $this->secondConfigurableAttribute;
+ }
+
+ /**
+ * Assert option uid.
+ *
+ * @param $attributeId
+ * @param $expectedOptions
+ * @param $selectedOptions
+ */
+ private function assertAvailableOptionUids($attributeId, $expectedOptions, $selectedOptions): void
+ {
+ unset($expectedOptions[0]);
+ foreach ($expectedOptions as $option) {
+ self::assertContains(
+ $this->selectionUidFormatter->encode((int)$attributeId, (int)$option->getValue()),
+ $selectedOptions
+ );
+ }
+ }
+
+ /**
+ * Make fulltext catalog search reindex
+ *
+ * @return void
+ * @throws \Throwable
+ */
+ private function reindexAll(): void
+ {
+ $indexLists = [
+ 'catalog_category_product',
+ 'catalog_product_attribute',
+ 'cataloginventory_stock',
+ 'catalogsearch_fulltext',
+ ];
+
+ foreach ($indexLists as $indexerId) {
+ $indexer = $this->indexerFactory->create();
+ $indexer->load($indexerId)->reindexAll();
+ }
+ }
+
+ /**
+ * Retrieve options UIDs
+ *
+ * @param array $options
+ * @return array
+ */
+ private function getOptionsUids(array $options): array
+ {
+ $uids = [];
+ foreach ($options as $option) {
+ $uids[] = $option['uid'];
+ }
+ return $uids;
+ }
+
+ /**
+ * Assert media gallery fields
+ *
+ * @param array $product
+ */
+ private function assertMediaGallery(array $product): void
+ {
+ self::assertNotEmpty($product['configurable_product_options_selection']['media_gallery']);
+ $image = current($product['configurable_product_options_selection']['media_gallery']);
+ self::assertIsString($image['url']);
+ self::assertEquals(false, $image['disabled']);
+ }
+}
diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Swatches/ProductSwatchDataTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Swatches/ProductSwatchDataTest.php
index ae34ea31f0d51..713a16a6bfaa9 100644
--- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Swatches/ProductSwatchDataTest.php
+++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Swatches/ProductSwatchDataTest.php
@@ -84,12 +84,12 @@ public function testVisualSwatchDataValues()
$color = '#000000';
$query = <<swatchMediaHelper->getSwatchAttributeImage(Swatch::SWATCH_THUMBNAIL_NAME, $imageName)
);
+
+ $configurableProductOptionsSelection =
+ $product['configurable_product_options_selection']['configurable_options'][0];
+
+ $this->assertArrayHasKey('values', $configurableProductOptionsSelection);
+ $this->assertEquals($color, $configurableProductOptionsSelection['values'][0]['swatch']['value']);
+ $this->assertStringContainsString(
+ $configurableProductOptionsSelection['values'][1]['swatch']['value'],
+ $this->swatchMediaHelper->getSwatchAttributeImage(Swatch::SWATCH_IMAGE_NAME, $imageName)
+ );
+ $this->assertEquals(
+ $configurableProductOptionsSelection['values'][1]['swatch']['thumbnail'],
+ $this->swatchMediaHelper->getSwatchAttributeImage(Swatch::SWATCH_THUMBNAIL_NAME, $imageName)
+ );
}
}
diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php
index 24e6010275bac..f26e39ca8e2a2 100644
--- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php
+++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php
@@ -3,10 +3,12 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Model\Indexer\Product\Price\Processor as PriceIndexerProcessor;
use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\Product\Attribute\Source\Status;
+use Magento\Catalog\Model\Product\Media\Config;
use Magento\Catalog\Model\Product\Type;
use Magento\Catalog\Model\Product\Visibility;
use Magento\Catalog\Setup\CategorySetup;
@@ -15,8 +17,11 @@
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
use Magento\Eav\Api\Data\AttributeOptionInterface;
use Magento\Framework\Api\Data\ImageContentInterface;
+use Magento\Framework\Filesystem;
+use Magento\Framework\Filesystem\Directory\WriteInterface;
use Magento\TestFramework\Helper\Bootstrap;
use Magento\TestFramework\Workaround\Override\Fixture\Resolver;
+use Magento\Framework\App\Filesystem\DirectoryList;
Resolver::getInstance()->requireDataFixture(
'Magento/ConfigurableProduct/_files/configurable_attribute_first.php'
@@ -25,18 +30,31 @@
'Magento/ConfigurableProduct/_files/configurable_attribute_second.php'
);
+$objectManager = Bootstrap::getObjectManager();
+
/** @var ProductRepositoryInterface $productRepository */
-$productRepository = Bootstrap::getObjectManager()
- ->get(ProductRepositoryInterface::class);
+$productRepository = $objectManager->get(ProductRepositoryInterface::class);
/** @var $installer CategorySetup */
-$installer = Bootstrap::getObjectManager()->create(CategorySetup::class);
+$installer = $objectManager->create(CategorySetup::class);
/** @var \Magento\Eav\Model\Config $eavConfig */
-$eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class);
+$eavConfig = $objectManager->get(\Magento\Eav\Model\Config::class);
$firstAttribute = $eavConfig->getAttribute(Product::ENTITY, 'test_configurable_first');
$secondAttribute = $eavConfig->getAttribute(Product::ENTITY, 'test_configurable_second');
+/** @var Config $config */
+$config = $objectManager->get(Config::class);
+
+/** @var Filesystem $filesystem */
+$filesystem = $objectManager->get(Filesystem::class);
+
+/** @var WriteInterface $mediaDirectory */
+$mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA);
+$mediaPath = $mediaDirectory->getAbsolutePath();
+$baseTmpMediaPath = $config->getBaseTmpMediaPath();
+$mediaDirectory->create($baseTmpMediaPath);
+
/* Create simple products per each option value*/
/** @var AttributeOptionInterface[] $firstAttributeOptions */
$firstAttributeOptions = $firstAttribute->getOptions();
@@ -48,6 +66,8 @@
$firstAttributeValues = [];
$secondAttributeValues = [];
$testImagePath = __DIR__ . '/magento_image.jpg';
+$mediaImage = $mediaPath . '/' . $baseTmpMediaPath . '/magento_image.jpg';
+copy($testImagePath, $mediaImage);
array_shift($firstAttributeOptions);
array_shift($secondAttributeOptions);
@@ -65,7 +85,10 @@
$qty = 100;
$isInStock = 1;
}
- $product = Bootstrap::getObjectManager()->create(Product::class);
+
+ $image = '/m/a/magento_image.jpg';
+
+ $product = $objectManager->create(Product::class);
$product->setTypeId(Type::TYPE_SIMPLE)
->setAttributeSetId($attributeSetId)
->setWebsiteIds([1])
@@ -79,15 +102,15 @@
->setStockData(
['use_config_manage_stock' => 1, 'qty' => $qty, 'is_qty_decimal' => 0, 'is_in_stock' => $isInStock]
)
- ->setImage('/m/a/magento_image.jpg')
- ->setSmallImage('/m/a/magento_image.jpg')
- ->setThumbnail('/m/a/magento_image.jpg')
+ ->setImage($image)
+ ->setSmallImage($image)
+ ->setThumbnail($image)
->setData(
'media_gallery',
[
'images' => [
[
- 'file' => '/m/a/magento_image.jpg',
+ 'file' => $image,
'position' => 1,
'label' => 'Image Alt Text',
'disabled' => 0,
@@ -113,11 +136,12 @@
foreach ($customAttributes as $attributeCode => $attributeValue) {
$product->setCustomAttributes($customAttributes);
}
+
$product = $productRepository->save($product);
$associatedProductIds[] = $product->getId();
- /** @var \Magento\CatalogInventory\Model\Stock\Item $stockItem */
- $stockItem = Bootstrap::getObjectManager()->create(Item::class);
+ /** @var Item $stockItem */
+ $stockItem = $objectManager->create(Item::class);
$stockItem->load($product->getId(), 'product_id');
if (!$stockItem->getProductId()) {
@@ -135,17 +159,16 @@
'value_index' => $secondAttributeOption->getValue(),
];
}
-
}
-$indexerProcessor = Bootstrap::getObjectManager()->get(PriceIndexerProcessor::class);
+$indexerProcessor = $objectManager->get(PriceIndexerProcessor::class);
$indexerProcessor->reindexList($associatedProductIds, true);
/** @var $product Product */
-$product = Bootstrap::getObjectManager()->create(Product::class);
+$product = $objectManager->create(Product::class);
/** @var Factory $optionsFactory */
-$optionsFactory = Bootstrap::getObjectManager()->create(Factory::class);
+$optionsFactory = $objectManager->create(Factory::class);
$configurableAttributesData = [
[
@@ -180,9 +203,15 @@
->setSku('configurable_12345')
->setVisibility(Visibility::VISIBILITY_BOTH)
->setStatus(Status::STATUS_ENABLED)
- ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]);
+ ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1])
+ ->addImageToMediaGallery(
+ $mediaImage,
+ ['image', 'small_image', 'thumbnail'],
+ false,
+ false
+ );
$productRepository->cleanCache();
$product = $productRepository->save($product);
-$indexerProcessor = Bootstrap::getObjectManager()->get(PriceIndexerProcessor::class);
+$indexerProcessor = $objectManager->get(PriceIndexerProcessor::class);
$indexerProcessor->reindexRow($product->getId(), true);