diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Options/BundleItemOptionUid.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Options/BundleItemOptionUid.php new file mode 100644 index 0000000000000..ce5c12ce69675 --- /dev/null +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Options/BundleItemOptionUid.php @@ -0,0 +1,68 @@ +///" format + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return string + * + * @throws GraphQlInputException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['option_id']) || empty($value['option_id'])) { + throw new GraphQlInputException(__('"option_id" value should be specified.')); + } + + if (!isset($value['selection_id']) || empty($value['selection_id'])) { + throw new GraphQlInputException(__('"selection_id" value should be specified.')); + } + + $optionDetails = [ + self::OPTION_TYPE, + $value['option_id'], + $value['selection_id'], + (int) $value['selection_qty'] + ]; + + $content = implode('/', $optionDetails); + + // phpcs:ignore Magento2.Functions.DiscouragedFunction + return base64_encode($content); + } +} diff --git a/app/code/Magento/BundleGraphQl/etc/schema.graphqls b/app/code/Magento/BundleGraphQl/etc/schema.graphqls index 0eff0e086180e..5f5d48e1ae45c 100644 --- a/app/code/Magento/BundleGraphQl/etc/schema.graphqls +++ b/app/code/Magento/BundleGraphQl/etc/schema.graphqls @@ -66,6 +66,7 @@ type BundleItemOption @doc(description: "BundleItemOption defines characteristic price_type: PriceTypeEnum @doc(description: "One of FIXED, PERCENT, or DYNAMIC.") can_change_quantity: Boolean @doc(description: "Indicates whether the customer can change the number of items for this option.") product: ProductInterface @doc(description: "Contains details about this product option.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\Options\\BundleItemOptionUid") # A Base64 string that encodes option details. } type BundleProduct implements ProductInterface, PhysicalProductInterface, CustomizableProductInterface @doc(description: "BundleProduct defines basic features of a bundle product and contains multiple BundleItems.") { diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CustomizableEnteredOptionValueUid.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CustomizableEnteredOptionValueUid.php new file mode 100644 index 0000000000000..69fafc49c9137 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CustomizableEnteredOptionValueUid.php @@ -0,0 +1,62 @@ +/" format + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return string + * + * @throws GraphQlInputException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['option_id']) || empty($value['option_id'])) { + throw new GraphQlInputException(__('"option_id" value should be specified.')); + } + + $optionDetails = [ + self::OPTION_TYPE, + $value['option_id'] + ]; + + $content = implode('/', $optionDetails); + + // phpcs:ignore Magento2.Functions.DiscouragedFunction + return base64_encode($content); + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CustomizableSelectedOptionValueUid.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CustomizableSelectedOptionValueUid.php new file mode 100644 index 0000000000000..5fbd8a56bb570 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CustomizableSelectedOptionValueUid.php @@ -0,0 +1,67 @@ +//" format + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return string + * + * @throws GraphQlInputException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['option_id']) || empty($value['option_id'])) { + throw new GraphQlInputException(__('"option_id" value should be specified.')); + } + + if (!isset($value['option_type_id']) || empty($value['option_type_id'])) { + throw new GraphQlInputException(__('"option_type_id" value should be specified.')); + } + + $optionDetails = [ + self::OPTION_TYPE, + $value['option_id'], + $value['option_type_id'] + ]; + + $content = implode('/', $optionDetails); + + // phpcs:ignore Magento2.Functions.DiscouragedFunction + return base64_encode($content); + } +} diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls index a9720bf17445b..63deb254059dd 100644 --- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls @@ -132,6 +132,7 @@ type CustomizableAreaValue @doc(description: "CustomizableAreaValue defines the price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC.") sku: String @doc(description: "The Stock Keeping Unit for this option.") max_characters: Int @doc(description: "The maximum number of characters that can be entered for this customizable option.") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableEnteredOptionValueUid") # A Base64 string that encodes option details. } type CategoryTree implements CategoryInterface @doc(description: "Category Tree implementation.") { @@ -153,6 +154,7 @@ type CustomizableDateValue @doc(description: "CustomizableDateValue defines the price: Float @doc(description: "The price assigned to this option.") price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC.") sku: String @doc(description: "The Stock Keeping Unit for this option.") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableEnteredOptionValueUid") # A Base64 string that encodes option details. } type CustomizableDropDownOption implements CustomizableOptionInterface @doc(description: "CustomizableDropDownOption contains information about a drop down menu that is defined as part of a customizable option.") { @@ -166,6 +168,7 @@ type CustomizableDropDownValue @doc(description: "CustomizableDropDownValue defi sku: String @doc(description: "The Stock Keeping Unit for this option.") title: String @doc(description: "The display name for this option.") sort_order: Int @doc(description: "The order in which the option is displayed.") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableSelectedOptionValueUid") # A Base64 string that encodes option details. } type CustomizableMultipleOption implements CustomizableOptionInterface @doc(description: "CustomizableMultipleOption contains information about a multiselect that is defined as part of a customizable option.") { @@ -179,6 +182,7 @@ type CustomizableMultipleValue @doc(description: "CustomizableMultipleValue defi sku: String @doc(description: "The Stock Keeping Unit for this option.") title: String @doc(description: "The display name for this option.") sort_order: Int @doc(description: "The order in which the option is displayed.") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableSelectedOptionValueUid") } type CustomizableFieldOption implements CustomizableOptionInterface @doc(description: "CustomizableFieldOption contains information about a text field that is defined as part of a customizable option.") { @@ -191,6 +195,7 @@ type CustomizableFieldValue @doc(description: "CustomizableFieldValue defines th price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC.") sku: String @doc(description: "The Stock Keeping Unit for this option.") max_characters: Int @doc(description: "The maximum number of characters that can be entered for this customizable option.") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableEnteredOptionValueUid") # A Base64 string that encodes option details. } type CustomizableFileOption implements CustomizableOptionInterface @doc(description: "CustomizableFileOption contains information about a file picker that is defined as part of a customizable option.") { @@ -205,6 +210,7 @@ type CustomizableFileValue @doc(description: "CustomizableFileValue defines the file_extension: String @doc(description: "The file extension to accept.") image_size_x: Int @doc(description: "The maximum width of an image.") image_size_y: Int @doc(description: "The maximum height of an image.") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableEnteredOptionValueUid") # A Base64 string that encodes option details. } interface MediaGalleryInterface @doc(description: "Contains basic information about a product image or video.") @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\MediaGalleryTypeResolver") { @@ -274,6 +280,7 @@ type CustomizableRadioValue @doc(description: "CustomizableRadioValue defines th sku: String @doc(description: "The Stock Keeping Unit for this option.") title: String @doc(description: "The display name for this option.") sort_order: Int @doc(description: "The order in which the radio button is displayed.") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableSelectedOptionValueUid") # A Base64 string that encodes option details. } type CustomizableCheckboxOption implements CustomizableOptionInterface @doc(description: "CustomizableCheckbbixOption contains information about a set of checkbox values that are defined as part of a customizable option.") { @@ -287,6 +294,7 @@ type CustomizableCheckboxValue @doc(description: "CustomizableCheckboxValue defi sku: String @doc(description: "The Stock Keeping Unit for this option.") title: String @doc(description: "The display name for this option.") sort_order: Int @doc(description: "The order in which the checkbox value is displayed.") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableSelectedOptionValueUid") # A Base64 string that encodes option details. } type VirtualProduct implements ProductInterface, CustomizableProductInterface @doc(description: "A virtual product is non-tangible product that does not require shipping and is not kept in inventory.") { diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes.php index faf666144422c..3a064f3399255 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes.php @@ -60,7 +60,8 @@ public function resolve( $option['options_map'] ?? [], $code, (int) $optionId, - (int) $model->getData($code) + (int) $model->getData($code), + (int) $option['attribute_id'] ); if (!empty($optionsFromMap)) { $data[] = $optionsFromMap; @@ -77,14 +78,20 @@ public function resolve( * @param string $code * @param int $optionId * @param int $attributeCodeId + * @param int $attributeId * @return array */ - private function getOptionsFromMap(array $optionMap, string $code, int $optionId, int $attributeCodeId): array - { + private function getOptionsFromMap( + array $optionMap, + string $code, + int $optionId, + int $attributeCodeId, + int $attributeId + ): array { $data = []; if (isset($optionMap[$optionId . ':' . $attributeCodeId])) { $optionValue = $optionMap[$optionId . ':' . $attributeCodeId]; - $data = $this->getOptionsArray($optionValue, $code); + $data = $this->getOptionsArray($optionValue, $code, $attributeId); } return $data; } @@ -94,15 +101,17 @@ private function getOptionsFromMap(array $optionMap, string $code, int $optionId * * @param array $optionValue * @param string $code + * @param int $attributeId * @return array */ - private function getOptionsArray(array $optionValue, string $code): array + private function getOptionsArray(array $optionValue, string $code, int $attributeId): array { return [ 'label' => $optionValue['label'] ?? null, 'code' => $code, 'use_default_value' => $optionValue['use_default_value'] ?? null, 'value_index' => $optionValue['value_index'] ?? null, + 'attribute_id' => $attributeId, ]; } } diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes/ConfigurableAttributeUid.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes/ConfigurableAttributeUid.php new file mode 100644 index 0000000000000..13f31e7e2ce10 --- /dev/null +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes/ConfigurableAttributeUid.php @@ -0,0 +1,67 @@ +//" format + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return string + * + * @throws GraphQlInputException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['attribute_id']) || empty($value['attribute_id'])) { + throw new GraphQlInputException(__('"attribute_id" value should be specified.')); + } + + if (!isset($value['value_index']) || empty($value['value_index'])) { + throw new GraphQlInputException(__('"value_index" value should be specified.')); + } + + $optionDetails = [ + self::OPTION_TYPE, + $value['attribute_id'], + $value['value_index'] + ]; + + $content = implode('/', $optionDetails); + + // phpcs:ignore Magento2.Functions.DiscouragedFunction + return base64_encode($content); + } +} diff --git a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls index 2e9576b35e6e8..6e85653380acc 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls +++ b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls @@ -18,6 +18,7 @@ type ConfigurableAttributeOption @doc(description: "ConfigurableAttributeOption label: String @doc(description: "A string that describes the configurable attribute option") code: String @doc(description: "The ID assigned to the attribute") value_index: Int @doc(description: "A unique index number assigned to the configurable product option") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\Variant\\Attributes\\ConfigurableAttributeUid") # A Base64 string that encodes option details. } type ConfigurableProductOptions @doc(description: "ConfigurableProductOptions defines configurable attributes for the specified product") { diff --git a/app/code/Magento/DownloadableGraphQl/Resolver/Product/DownloadableLinksValueUid.php b/app/code/Magento/DownloadableGraphQl/Resolver/Product/DownloadableLinksValueUid.php new file mode 100644 index 0000000000000..03727597104fd --- /dev/null +++ b/app/code/Magento/DownloadableGraphQl/Resolver/Product/DownloadableLinksValueUid.php @@ -0,0 +1,49 @@ +getQuery($productSku); + $response = $this->graphQlQuery($query); + $responseProduct = $response['products']['items'][0]; + self::assertNotEmpty($responseProduct['options']); + + foreach ($responseProduct['options'] as $option) { + if (isset($option['entered_option'])) { + $enteredOption = $option['entered_option']; + $uid = $this->getUidForEnteredValue($option['option_id']); + + self::assertEquals($uid, $enteredOption['uid']); + } elseif (isset($option['selected_option'])) { + $this->assertNotEmpty($option['selected_option']); + + foreach ($option['selected_option'] as $selectedOption) { + $uid = $this->getUidForSelectedValue($option['option_id'], $selectedOption['option_type_id']); + self::assertEquals($uid, $selectedOption['uid']); + } + } + } + } + + /** + * Get uid for entered option + * + * @param int $optionId + * + * @return string + */ + private function getUidForEnteredValue(int $optionId): string + { + return base64_encode('custom-option/' . $optionId); + } + + /** + * Get uid for selected option + * + * @param int $optionId + * @param int $optionValueId + * + * @return string + */ + private function getUidForSelectedValue(int $optionId, int $optionValueId): string + { + return base64_encode('custom-option/' . $optionId . '/' . $optionValueId); + } + + /** + * Get query + * + * @param string $sku + * + * @return string + */ + private function getQuery(string $sku): string + { + return <<eavAttribute = $objectManager->get(Attribute::class); + } + + /** + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_product_with_one_simple.php + */ + public function testQueryUidForConfigurableSuperAttributes() + { + $productSku = 'configurable'; + $query = $this->getQuery($productSku); + $response = $this->graphQlQuery($query); + $responseProduct = $response['products']['items'][0]; + self::assertNotEmpty($responseProduct['variants']); + + foreach ($responseProduct['variants'] as $variant) { + self::assertNotEmpty($variant['attributes']); + + foreach ($variant['attributes'] as $attribute) { + $attributeId = (int) $this->eavAttribute->getIdByCode(Product::ENTITY, $attribute['code']); + $uid = $this->getUidByOptionIds($attributeId, $attribute['value_index']); + self::assertEquals($uid, $attribute['uid']); + } + } + } + + /** + * Get Uid + * + * @param int $optionId + * @param int $optionValueId + * + * @return string + */ + private function getUidByOptionIds(int $optionId, int $optionValueId): string + { + return base64_encode('configurable/' . $optionId . '/' . $optionValueId); + } + + /** + * Get query + * + * @param string $sku + * + * @return string + */ + private function getQuery(string $sku): string + { + return <<getQuery($productSku); + $response = $this->graphQlQuery($query); + $responseProduct = $response['products']['items'][0]; + + self::assertNotEmpty($responseProduct['downloadable_product_links']); + + foreach ($responseProduct['downloadable_product_links'] as $productLink) { + $uid = $this->getUidByLinkId((int) $productLink['id']); + self::assertEquals($uid, $productLink['uid']); + } + } + + /** + * Get uid by link id + * + * @param int $linkId + * + * @return string + */ + private function getUidByLinkId(int $linkId): string + { + return base64_encode('downloadable/' . $linkId); + } + + /** + * Get query + * + * @param string $sku + * + * @return string + */ + private function getQuery(string $sku): string + { + return <<