diff --git a/app/code/Magento/Catalog/Test/Fixture/CategoryAttribute.php b/app/code/Magento/Catalog/Test/Fixture/CategoryAttribute.php
new file mode 100644
index 000000000000..303ddd723d6c
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Fixture/CategoryAttribute.php
@@ -0,0 +1,116 @@
+ false,
+ 'is_html_allowed_on_front' => true,
+ 'used_for_sort_by' => false,
+ 'is_filterable' => false,
+ 'is_filterable_in_search' => false,
+ 'is_used_in_grid' => true,
+ 'is_visible_in_grid' => true,
+ 'is_filterable_in_grid' => true,
+ 'position' => 0,
+ 'is_searchable' => '0',
+ 'is_visible_in_advanced_search' => '0',
+ 'is_comparable' => '0',
+ 'is_used_for_promo_rules' => '0',
+ 'is_visible_on_front' => '0',
+ 'used_in_product_listing' => '0',
+ 'is_visible' => true,
+ 'scope' => 'store',
+ 'attribute_code' => 'category_attribute%uniqid%',
+ 'frontend_input' => 'text',
+ 'entity_type_id' => '3',
+ 'is_required' => false,
+ 'is_user_defined' => true,
+ 'default_frontend_label' => 'Category Attribute%uniqid%',
+ 'backend_type' => 'varchar',
+ 'is_unique' => '0',
+ 'apply_to' => [],
+ ];
+
+ /**
+ * @var DataMerger
+ */
+ private DataMerger $dataMerger;
+
+ /**
+ * @var ProcessorInterface
+ */
+ private ProcessorInterface $processor;
+
+ /**
+ * @var AttributeFactory
+ */
+ private AttributeFactory $attributeFactory;
+
+ /**
+ * @var ResourceModelAttribute
+ */
+ private ResourceModelAttribute $resourceModelAttribute;
+
+ /**
+ * @var AttributeRepositoryInterface
+ */
+ private AttributeRepositoryInterface $attributeRepository;
+
+ /**
+ * @param DataMerger $dataMerger
+ * @param ProcessorInterface $processor
+ * @param AttributeRepositoryInterface $attributeRepository
+ * @param AttributeFactory $attributeFactory
+ * @param ResourceModelAttribute $resourceModelAttribute
+ */
+ public function __construct(
+ DataMerger $dataMerger,
+ ProcessorInterface $processor,
+ AttributeRepositoryInterface $attributeRepository,
+ AttributeFactory $attributeFactory,
+ ResourceModelAttribute $resourceModelAttribute
+ ) {
+ $this->dataMerger = $dataMerger;
+ $this->processor = $processor;
+ $this->attributeFactory = $attributeFactory;
+ $this->resourceModelAttribute = $resourceModelAttribute;
+ $this->attributeRepository = $attributeRepository;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function apply(array $data = []): ?DataObject
+ {
+ /** @var Attribute $attr */
+ $attr = $this->attributeFactory->createAttribute(Attribute::class, self::DEFAULT_DATA);
+ $mergedData = $this->processor->process($this, $this->dataMerger->merge(self::DEFAULT_DATA, $data));
+ $attr->setData($mergedData);
+ $this->resourceModelAttribute->save($attr);
+ return $attr;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function revert(DataObject $data): void
+ {
+ $this->attributeRepository->deleteById($data['attribute_id']);
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Output/AttributeMetadata.php b/app/code/Magento/CatalogGraphQl/Model/Output/AttributeMetadata.php
new file mode 100644
index 000000000000..47e9bfdedec7
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/Model/Output/AttributeMetadata.php
@@ -0,0 +1,73 @@
+entityType = $entityType;
+ }
+
+ /**
+ * Retrieve formatted attribute data
+ *
+ * @param Attribute $attribute
+ * @param string $entityType
+ * @param int $storeId
+ * @return array
+ * @throws LocalizedException
+ * @throws NoSuchEntityException
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function execute(
+ AttributeInterface $attribute,
+ string $entityType,
+ int $storeId
+ ): array {
+ if ($entityType !== $this->entityType) {
+ return [];
+ }
+
+ $metadata = [
+ 'is_searchable' => $attribute->getIsSearchable() === "1",
+ 'is_filterable' => $attribute->getIsFilterable() === "1",
+ 'is_comparable' => $attribute->getIsComparable() === "1",
+ 'is_html_allowed_on_front' => $attribute->getIsHtmlAllowedOnFront() === "1",
+ 'is_used_for_price_rules' => $attribute->getIsUsedForPriceRules() === "1",
+ 'is_filterable_in_search' => $attribute->getIsFilterableInSearch() === "1",
+ 'used_in_product_listing' => $attribute->getUsedInProductListing() === "1",
+ 'is_wysiwyg_enabled' => $attribute->getIsWysiwygEnabled() === "1",
+ 'is_used_for_promo_rules' => $attribute->getIsUsedForPromoRules() === "1",
+ 'apply_to' => null,
+ ];
+
+ if (!empty($attribute->getApplyTo())) {
+ $metadata['apply_to'] = array_map('strtoupper', $attribute->getApplyTo());
+ }
+
+ return $metadata;
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductCustomAttributes.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductCustomAttributes.php
new file mode 100644
index 000000000000..c8bc26897a46
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductCustomAttributes.php
@@ -0,0 +1,155 @@
+attributeRepository = $attributeRepository;
+ $this->searchCriteriaBuilder = $searchCriteriaBuilder;
+ $this->getAttributeValue = $getAttributeValue;
+ $this->productDataProvider = $productDataProvider;
+ $this->attributeFilter = $attributeFilter;
+ $this->filterCustomAttribute = $filterCustomAttribute;
+ }
+
+ /**
+ * @inheritdoc
+ *
+ * @param Field $field
+ * @param ContextInterface $context
+ * @param ResolveInfo $info
+ * @param array|null $value
+ * @param array|null $args
+ * @throws \Exception
+ * @return array
+ */
+ public function resolve(
+ Field $field,
+ $context,
+ ResolveInfo $info,
+ array $value = null,
+ array $args = null
+ ) {
+ $filterArgs = $args['filter'] ?? [];
+
+ $searchCriteriaBuilder = $this->attributeFilter->execute($filterArgs, $this->searchCriteriaBuilder);
+
+ $searchCriteriaBuilder = $searchCriteriaBuilder
+ ->addFilter('is_visible', true)
+ ->addFilter('backend_type', 'static', 'neq')
+ ->create();
+
+ $productCustomAttributes = $this->attributeRepository->getList(
+ ProductAttributeInterface::ENTITY_TYPE_CODE,
+ $searchCriteriaBuilder
+ )->getItems();
+
+ $attributeCodes = array_map(
+ function (AttributeInterface $customAttribute) {
+ return $customAttribute->getAttributeCode();
+ },
+ $productCustomAttributes
+ );
+
+ $filteredAttributeCodes = $this->filterCustomAttribute->execute(array_flip($attributeCodes));
+
+ /** @var Product $product */
+ $product = $value['model'];
+ $productData = $this->productDataProvider->getProductDataById((int)$product->getId());
+
+ $customAttributes = [];
+ foreach ($filteredAttributeCodes as $attributeCode => $value) {
+ if (!array_key_exists($attributeCode, $productData)) {
+ continue;
+ }
+ $attributeValue = $productData[$attributeCode];
+ if (is_array($attributeValue)) {
+ $attributeValue = implode(',', $attributeValue);
+ }
+ $customAttributes[] = [
+ 'attribute_code' => $attributeCode,
+ 'value' => $attributeValue
+ ];
+ }
+
+ return array_map(
+ function (array $customAttribute) {
+ return $this->getAttributeValue->execute(
+ ProductAttributeInterface::ENTITY_TYPE_CODE,
+ $customAttribute['attribute_code'],
+ $customAttribute['value']
+ );
+ },
+ $customAttributes
+ );
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml
index 139bf61f8e76..9fc1a4759445 100644
--- a/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml
+++ b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml
@@ -211,7 +211,8 @@
- - CatalogAttributeMetadata
+ - CatalogAttributeMetadata
+ - CatalogAttributeMetadata
@@ -220,8 +221,27 @@
-
- catalog_product
+ - catalog_category
+
+
+
+ - GetCatalogProductAttributesMetadata
+ - GetCatalogCategoryAttributesMetadata
+
+
+
+
+
+ catalog_product
+
+
+
+
+ catalog_category
+
+
diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls
index 70c7608deccd..d8262f951798 100644
--- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls
+++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls
@@ -125,6 +125,7 @@ interface ProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\
categories: [CategoryInterface] @doc(description: "The categories assigned to a product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Categories") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoriesIdentity")
canonical_url: String @doc(description: "The relative canonical URL. This value is returned only if the system setting 'Use Canonical Link Meta Tag For Products' is enabled.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CanonicalUrl")
media_gallery: [MediaGalleryInterface] @doc(description: "An array of media gallery objects.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\MediaGallery")
+ custom_attributes(filter: AttributeFilterInput): [AttributeValueInterface] @doc(description: "Product custom attributes.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductCustomAttributes")
}
interface PhysicalProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\ProductInterfaceTypeResolverComposite") @doc(description: "Contains attributes specific to tangible products.") {
@@ -535,4 +536,47 @@ type VirtualWishlistItem implements WishlistItemInterface @doc(description: "Con
enum AttributeEntityTypeEnum {
CATALOG_PRODUCT
+ CATALOG_CATEGORY
+}
+
+type CatalogAttributeMetadata implements CustomAttributeMetadataInterface @doc(description: "Catalog attribute metadata.") {
+ is_filterable_in_search: Boolean! @doc(description: "Whether a product or category attribute can be filtered in search or not.")
+ used_in_product_listing: Boolean! @doc(description: "Whether a product or category attribute is used in product listing or not.")
+ is_searchable: Boolean! @doc(description: "Whether a product or category attribute can be searched or not.")
+ is_filterable: Boolean! @doc(description: "Whether a product or category attribute can be filtered or not.")
+ is_comparable: Boolean! @doc(description: "Whether a product or category attribute can be compared against another or not.")
+ is_html_allowed_on_front: Boolean! @doc(description: "Whether a product or category attribute can use HTML on front or not.")
+ is_used_for_price_rules: Boolean! @doc(description: "Whether a product or category attribute can be used for price rules or not.")
+ is_wysiwyg_enabled: Boolean! @doc(description: "Whether a product or category attribute has WYSIWYG enabled or not.")
+ is_used_for_promo_rules: Boolean! @doc(description: "Whether a product or category attribute is used for promo rules or not.")
+ apply_to: [CatalogAttributeApplyToEnum] @doc(description: "To which catalog types an attribute can be applied.")
+}
+
+enum CatalogAttributeApplyToEnum {
+ SIMPLE
+ VIRTUAL
+ BUNDLE
+ DOWNLOADABLE
+ CONFIGURABLE
+ GROUPED
+ CATEGORY
+}
+
+input AttributeFilterInput @doc(description: "An input object that specifies the filters used for product.") {
+ is_comparable: Boolean @doc(description: "Whether a product or category attribute can be compared against another or not.")
+ is_filterable_in_search: Boolean @doc(description: "Whether a product or category attribute can be filtered in search or not.")
+ is_searchable: Boolean @doc(description: "Whether a product or category attribute can be searched or not.")
+ is_filterable: Boolean @doc(description: "Whether a product or category attribute can be filtered or not.")
+ is_html_allowed_on_front: Boolean @doc(description: "Whether a product or category attribute can use HTML on front or not.")
+ is_used_for_price_rules: Boolean @doc(description: "Whether a product or category attribute can be used for price rules or not.")
+ is_visible_in_advanced_search: Boolean @doc(description: "Whether a product or category attribute is visible in advanced search or not.")
+ is_wysiwyg_enabled: Boolean @doc(description: "Whether a product or category attribute has WYSIWYG enabled or not.")
+ is_used_for_promo_rules: Boolean @doc(description: "Whether a product or category attribute is used for promo rules or not.")
+ used_in_product_listing: Boolean @doc(description: "Whether a product or category attribute is used in product listing or not.")
+ is_visible_on_front: Boolean @doc(description: "Whether a product or category attribute is visible on front or not.")
+ used_for_sort_by: Boolean @doc(description: "Whether a product or category attribute is used for sort or not.")
+ is_required_in_admin_store: Boolean @doc(description: "Whether a product or category attribute is required in admin store or not.")
+ is_used_in_grid: Boolean @doc(description: "Whether a product or category attribute is used in grid or not.")
+ is_visible_in_grid: Boolean @doc(description: "Whether a product or category attribute is visible in grid or not.")
+ is_filterable_in_grid: Boolean @doc(description: "Whether a product or category attribute is filterable in grid or not.")
}
diff --git a/app/code/Magento/Customer/Test/Fixture/CustomerAttribute.php b/app/code/Magento/Customer/Test/Fixture/CustomerAttribute.php
index a4db22971d7d..4d191182b565 100644
--- a/app/code/Magento/Customer/Test/Fixture/CustomerAttribute.php
+++ b/app/code/Magento/Customer/Test/Fixture/CustomerAttribute.php
@@ -41,7 +41,10 @@ class CustomerAttribute implements RevertibleDataFixtureInterface
'attribute_group_id' => null,
'input_filter' => null,
'multiline_count' => 0,
- 'validate_rules' => null
+ 'validate_rules' => null,
+ 'website_id' => null,
+ 'is_visible' => 1,
+ 'scope_is_visible' => 1,
];
/**
@@ -110,6 +113,9 @@ public function apply(array $data = []): ?DataObject
$attr = $this->attributeFactory->createAttribute(Attribute::class, self::DEFAULT_DATA);
$mergedData = $this->processor->process($this, $this->dataMerger->merge(self::DEFAULT_DATA, $data));
$attr->setData($mergedData);
+ if (isset($data['website_id'])) {
+ $attr->setWebsite($data['website_id']);
+ }
$this->resourceModelAttribute->save($attr);
return $attr;
}
diff --git a/app/code/Magento/EavGraphQl/Model/GetAttributesMetadata.php b/app/code/Magento/EavGraphQl/Model/GetAttributesMetadata.php
index efb2202fef33..67b2692e5579 100644
--- a/app/code/Magento/EavGraphQl/Model/GetAttributesMetadata.php
+++ b/app/code/Magento/EavGraphQl/Model/GetAttributesMetadata.php
@@ -75,8 +75,7 @@ public function execute(array $attributesInputs, int $storeId): array
foreach ($codes as $entityType => $attributeCodes) {
$builder = $this->searchCriteriaBuilderFactory->create();
$builder
- ->addFilter('attribute_code', $attributeCodes, 'in')
- ->addFilter('is_visible', true);
+ ->addFilter('attribute_code', $attributeCodes, 'in');
try {
$attributes = $this->attributeRepository->getList($entityType, $builder->create())->getItems();
} catch (LocalizedException $exception) {
@@ -95,6 +94,9 @@ public function execute(array $attributesInputs, int $storeId): array
];
}
foreach ($attributes as $attribute) {
+ if (method_exists($attribute, 'getIsVisible') && !$attribute->getIsVisible()) {
+ continue;
+ }
$items[] = $this->getAttributeData->execute($attribute, $entityType, $storeId);
}
}
diff --git a/app/code/Magento/EavGraphQl/Model/Output/GetAttributeData.php b/app/code/Magento/EavGraphQl/Model/Output/GetAttributeData.php
index 8316036ff8b3..76ef9408859b 100644
--- a/app/code/Magento/EavGraphQl/Model/Output/GetAttributeData.php
+++ b/app/code/Magento/EavGraphQl/Model/Output/GetAttributeData.php
@@ -73,6 +73,7 @@ public function execute(
$entityType
),
'frontend_input' => $this->getFrontendInput($attribute),
+ 'frontend_class' => $attribute->getFrontendClass(),
'is_required' => $attribute->getIsRequired(),
'default_value' => $attribute->getDefaultValue(),
'is_unique' => $attribute->getIsUnique(),
diff --git a/app/code/Magento/EavGraphQl/Model/Output/Value/Options/GetCustomSelectedOptionAttributes.php b/app/code/Magento/EavGraphQl/Model/Output/Value/Options/GetCustomSelectedOptionAttributes.php
index b126dc7958b2..756d4b0905a3 100644
--- a/app/code/Magento/EavGraphQl/Model/Output/Value/Options/GetCustomSelectedOptionAttributes.php
+++ b/app/code/Magento/EavGraphQl/Model/Output/Value/Options/GetCustomSelectedOptionAttributes.php
@@ -51,7 +51,7 @@ public function execute(string $entity, string $code, string $value): ?array
continue;
}
$result[] = [
- 'uid' => $this->uid->encode($option->getValue()),
+ 'uid' => $this->uid->encode((string)$option->getValue()),
'value' => $option->getValue(),
'label' => $option->getLabel()
];
diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/AttributeFilter.php b/app/code/Magento/EavGraphQl/Model/Resolver/AttributeFilter.php
new file mode 100644
index 000000000000..f037c3086439
--- /dev/null
+++ b/app/code/Magento/EavGraphQl/Model/Resolver/AttributeFilter.php
@@ -0,0 +1,32 @@
+ $value) {
+ $searchCriteriaBuilder->addFilter($key, $value);
+ }
+
+ return $searchCriteriaBuilder;
+ }
+}
diff --git a/app/code/Magento/EavGraphQl/etc/schema.graphqls b/app/code/Magento/EavGraphQl/etc/schema.graphqls
index 51eed5e07afd..673640db9ce0 100644
--- a/app/code/Magento/EavGraphQl/etc/schema.graphqls
+++ b/app/code/Magento/EavGraphQl/etc/schema.graphqls
@@ -76,6 +76,7 @@ interface CustomAttributeMetadataInterface @typeResolver(class: "Magento\\EavGra
label: String @doc(description: "The label assigned to the attribute.")
entity_type: AttributeEntityTypeEnum! @doc(description: "The type of entity that defines the attribute.")
frontend_input: AttributeFrontendInputEnum @doc(description: "The frontend input type of the attribute.")
+ frontend_class: String @doc(description: "The frontend class of the attribute.")
is_required: Boolean! @doc(description: "Whether the attribute value is required.")
default_value: String @doc(description: "Default attribute value.")
is_unique: Boolean! @doc(description: "Whether the attribute value must be unique.")
diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/GetProductWithCustomAttributesTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/GetProductWithCustomAttributesTest.php
new file mode 100644
index 000000000000..c9dadfaf06e8
--- /dev/null
+++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/GetProductWithCustomAttributesTest.php
@@ -0,0 +1,388 @@
+ CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID,
+ 'attribute_code' => 'product_custom_attribute',
+ 'is_visible_on_front' => 1
+ ],
+ 'varchar_custom_attribute'
+ ),
+ DataFixture(
+ MultiselectAttribute::class,
+ [
+ 'entity_type_id' => CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID,
+ 'source_model' => Table::class,
+ 'backend_model' => ArrayBackend::class,
+ 'attribute_code' => 'product_custom_attribute_multiselect'
+ ],
+ 'multiselect_custom_attribute'
+ ),
+ DataFixture(
+ AttributeOptionFixture::class,
+ [
+ 'entity_type' => CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID,
+ 'attribute_code' => '$multiselect_custom_attribute.attribute_code$',
+ 'label' => 'red',
+ 'sort_order' => 20
+ ],
+ 'multiselect_custom_attribute_option_1'
+ ),
+ DataFixture(
+ AttributeOptionFixture::class,
+ [
+ 'entity_type' => CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID,
+ 'attribute_code' => '$multiselect_custom_attribute.attribute_code$',
+ 'sort_order' => 10,
+ 'label' => 'white',
+ 'is_default' => true
+ ],
+ 'multiselect_custom_attribute_option_2'
+ ),
+ DataFixture(
+ ProductFixture::class,
+ [
+ 'custom_attributes' => [
+ [
+ 'attribute_code' => '$varchar_custom_attribute.attribute_code$',
+ 'value' => 'test_value'
+ ],
+ [
+ 'attribute_code' => '$multiselect_custom_attribute.attribute_code$',
+ 'selected_options' => [
+ ['value' => '$multiselect_custom_attribute_option_1.value$'],
+ ['value' => '$multiselect_custom_attribute_option_2.value$']
+ ],
+ ],
+ ],
+ ],
+ 'product'
+ ),
+]
+class GetProductWithCustomAttributesTest extends GraphQlAbstract
+{
+ /**
+ * @var ObjectManagerInterface
+ */
+ private $objectManager;
+
+ /**
+ * @var AttributeInterface|null
+ */
+ private $varcharCustomAttribute;
+
+ /**
+ * @var AttributeInterface|null
+ */
+ private $multiselectCustomAttribute;
+
+ /**
+ * @var AttributeOptionInterface|null
+ */
+ private $multiselectCustomAttributeOption1;
+
+ /**
+ * @var AttributeOptionInterface|null
+ */
+ private $multiselectCustomAttributeOption2;
+
+ /**
+ * @var Product|null
+ */
+ private $product;
+
+ /**
+ * @var EAVUid $eavUid
+ */
+ private $eavUid;
+
+ /**
+ * @var Uid $uid
+ */
+ private $uid;
+
+ /**
+ * @inheridoc
+ */
+ protected function setUp(): void
+ {
+ parent::setUp();
+
+ $this->objectManager = Bootstrap::getObjectManager();
+ $this->uid = $this->objectManager->get(Uid::class);
+ $this->eavUid = $this->objectManager->get(EAVUid::class);
+ $this->varcharCustomAttribute = DataFixtureStorageManager::getStorage()->get(
+ 'varchar_custom_attribute'
+ );
+ $this->multiselectCustomAttribute = DataFixtureStorageManager::getStorage()->get(
+ 'multiselect_custom_attribute'
+ );
+ $this->multiselectCustomAttributeOption1 = DataFixtureStorageManager::getStorage()->get(
+ 'multiselect_custom_attribute_option_1'
+ );
+ $this->multiselectCustomAttributeOption2 = DataFixtureStorageManager::getStorage()->get(
+ 'multiselect_custom_attribute_option_2'
+ );
+
+ $this->product = DataFixtureStorageManager::getStorage()->get('product');
+ }
+
+ public function testGetProductWithCustomAttributes()
+ {
+ $productSku = $this->product->getSku();
+
+ $query = <<graphQlQuery($query);
+ $this->assertArrayHasKey('items', $response['products'], 'Query result does not contain products');
+ $this->assertGreaterThanOrEqual(2, count($response['products']['items'][0]['custom_attributes']));
+
+ $this->assertResponseFields(
+ $response['products']['items'][0],
+ [
+ 'sku' => $this->product->getSku(),
+ 'name' => $this->product->getName()
+ ]
+ );
+
+ $this->assertResponseFields(
+ $this->getAttributeByCode(
+ $response['products']['items'][0]['custom_attributes'],
+ $this->varcharCustomAttribute->getAttributeCode()
+ ),
+ [
+ 'uid' => $this->eavUid->encode(
+ ProductAttributeInterface::ENTITY_TYPE_CODE,
+ $this->varcharCustomAttribute->getAttributeCode()
+ ),
+ 'code' => $this->varcharCustomAttribute->getAttributeCode(),
+ 'value' => 'test_value'
+ ]
+ );
+
+ $this->assertResponseFields(
+ $this->getAttributeByCode(
+ $response['products']['items'][0]['custom_attributes'],
+ $this->multiselectCustomAttribute->getAttributeCode()
+ ),
+ [
+ 'uid' => $this->eavUid->encode(
+ ProductAttributeInterface::ENTITY_TYPE_CODE,
+ $this->multiselectCustomAttribute->getAttributeCode()
+ ),
+ 'code' => $this->multiselectCustomAttribute->getAttributeCode(),
+ 'selected_options' => [
+ [
+ 'uid' => $this->uid->encode($this->multiselectCustomAttributeOption2->getValue()),
+ 'label' => $this->multiselectCustomAttributeOption2->getLabel(),
+ 'value' => $this->multiselectCustomAttributeOption2->getValue(),
+ ],
+ [
+ 'uid' => $this->uid->encode($this->multiselectCustomAttributeOption1->getValue()),
+ 'label' => $this->multiselectCustomAttributeOption1->getLabel(),
+ 'value' => $this->multiselectCustomAttributeOption1->getValue(),
+ ]
+ ]
+ ]
+ );
+ }
+
+ public function testGetNoResultsWhenFilteringByNotExistingSku()
+ {
+ $query = <<graphQlQuery($query);
+ $this->assertArrayHasKey('items', $response['products'], 'Query result must not contain products');
+ $this->assertCount(0, $response['products']['items']);
+ }
+
+ public function testGetProductCustomAttributesFiltered()
+ {
+ $productSku = $this->product->getSku();
+
+ $query = <<graphQlQuery($query);
+ $this->assertEquals(
+ [
+ 'products' => [
+ 'items' => [
+ 0 => [
+ 'sku' => $this->product->getSku(),
+ 'name' => $this->product->getName(),
+ 'custom_attributes' => [
+ [
+ 'uid' => $this->eavUid->encode(
+ ProductAttributeInterface::ENTITY_TYPE_CODE,
+ $this->varcharCustomAttribute->getAttributeCode()
+ ),
+ 'code' => $this->varcharCustomAttribute->getAttributeCode(),
+ 'value' => 'test_value'
+ ]
+ ]
+ ]
+ ]
+ ]
+ ],
+ $response
+ );
+ }
+
+ public function testGetProductCustomAttributesFilteredByNotExistingField()
+ {
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage('Field "not_existing_filter" is not defined by type "AttributeFilterInput"');
+ $productSku = $this->product->getSku();
+
+ $query = <<graphQlQuery($query);
+ }
+
+ /**
+ * Finds attribute in query result
+ *
+ * @param array $items
+ * @param string $attribute_code
+ * @return array
+ */
+ private function getAttributeByCode(array $items, string $attribute_code): array
+ {
+ $attribute = array_filter($items, function ($item) use ($attribute_code) {
+ return $item['code'] == $attribute_code;
+ });
+
+ return array_merge(...$attribute);
+ }
+}
diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogGraphQl/AttributesMetadataTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogGraphQl/AttributesMetadataTest.php
new file mode 100644
index 000000000000..46916c62859c
--- /dev/null
+++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogGraphQl/AttributesMetadataTest.php
@@ -0,0 +1,186 @@
+ 'multiselect',
+ 'is_filterable_in_search' => true,
+ 'position' => 4,
+ 'apply_to' => 'category'
+ ],
+ 'category_attribute'
+ ),
+ DataFixture(
+ Attribute::class,
+ [
+ 'frontend_input' => 'multiselect',
+ 'is_filterable_in_search' => true,
+ 'position' => 5,
+ ],
+ 'product_attribute'
+ ),
+]
+class AttributesMetadataTest extends GraphQlAbstract
+{
+ private const QUERY = <<get('product_attribute');
+
+ $productUid = Bootstrap::getObjectManager()->get(Uid::class)->encode(
+ ProductAttributeInterface::ENTITY_TYPE_CODE,
+ $productAttribute->getAttributeCode()
+ );
+
+ $result = $this->graphQlQuery(
+ sprintf(
+ self::QUERY,
+ $productAttribute->getAttributeCode(),
+ ProductAttributeInterface::ENTITY_TYPE_CODE
+ )
+ );
+
+ $this->assertEquals(
+ [
+ 'customAttributeMetadataV2' => [
+ 'items' => [
+ [
+ 'uid' => $productUid,
+ 'code' => $productAttribute->getAttributeCode(),
+ 'label' => $productAttribute->getDefaultFrontendLabel(),
+ 'entity_type' => strtoupper(ProductAttributeInterface::ENTITY_TYPE_CODE),
+ 'frontend_input' => 'MULTISELECT',
+ 'is_required' => false,
+ 'default_value' => $productAttribute->getDefaultValue(),
+ 'is_unique' => false,
+ 'is_filterable_in_search' => true,
+ 'is_searchable' => false,
+ 'is_filterable' => false,
+ 'is_comparable' => false,
+ 'is_html_allowed_on_front' => true,
+ 'is_used_for_price_rules' => false,
+ 'is_wysiwyg_enabled' => false,
+ 'is_used_for_promo_rules' => false,
+ 'used_in_product_listing' => false,
+ 'apply_to' => null,
+ ]
+ ],
+ 'errors' => []
+ ]
+ ],
+ $result
+ );
+ }
+
+ /**
+ * @return void
+ * @throws \Exception
+ */
+ public function testMetadataCategory(): void
+ {
+ /** @var CategoryAttributeInterface $categoryAttribute */
+ $categoryAttribute = DataFixtureStorageManager::getStorage()->get('category_attribute');
+
+ $categoryUid = Bootstrap::getObjectManager()->get(Uid::class)->encode(
+ CategoryAttributeInterface::ENTITY_TYPE_CODE,
+ $categoryAttribute->getAttributeCode()
+ );
+
+ $result = $this->graphQlQuery(
+ sprintf(
+ self::QUERY,
+ $categoryAttribute->getAttributeCode(),
+ CategoryAttributeInterface::ENTITY_TYPE_CODE
+ )
+ );
+
+ $this->assertEquals(
+ [
+ 'customAttributeMetadataV2' => [
+ 'items' => [
+ [
+ 'uid' => $categoryUid,
+ 'code' => $categoryAttribute->getAttributeCode(),
+ 'label' => $categoryAttribute->getDefaultFrontendLabel(),
+ 'entity_type' => strtoupper(CategoryAttributeInterface::ENTITY_TYPE_CODE),
+ 'frontend_input' => 'MULTISELECT',
+ 'is_required' => false,
+ 'default_value' => $categoryAttribute->getDefaultValue(),
+ 'is_unique' => false,
+ 'is_filterable_in_search' => true,
+ 'is_searchable' => false,
+ 'is_filterable' => false,
+ 'is_comparable' => false,
+ 'is_html_allowed_on_front' => true,
+ 'is_used_for_price_rules' => false,
+ 'is_wysiwyg_enabled' => false,
+ 'is_used_for_promo_rules' => false,
+ 'used_in_product_listing' => false,
+ 'apply_to' => ['CATEGORY'],
+ ]
+ ],
+ 'errors' => []
+ ]
+ ],
+ $result
+ );
+ }
+}
diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/AttributesFormTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/AttributesFormTest.php
index a086f833fc0a..2a95f28bbe39 100644
--- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/AttributesFormTest.php
+++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/AttributesFormTest.php
@@ -10,6 +10,10 @@
use Magento\Customer\Api\AddressMetadataInterface;
use Magento\Customer\Test\Fixture\CustomerAttribute;
use Magento\Eav\Api\Data\AttributeInterface;
+use Magento\Store\Api\Data\StoreInterface;
+use Magento\Store\Test\Fixture\Group as StoreGroupFixture;
+use Magento\Store\Test\Fixture\Store as StoreFixture;
+use Magento\Store\Test\Fixture\Website as WebsiteFixture;
use Magento\TestFramework\Fixture\DataFixture;
use Magento\TestFramework\Fixture\DataFixtureStorageManager;
use Magento\TestFramework\TestCase\GraphQlAbstract;
@@ -123,4 +127,58 @@ public function testAttributesFormDoesNotExist(): void
$this->graphQlQuery(sprintf(self::QUERY, 'not_existing_form'))
);
}
+
+ #[
+ DataFixture(WebsiteFixture::class, as: 'website2'),
+ DataFixture(StoreGroupFixture::class, ['website_id' => '$website2.id$'], 'store_group2'),
+ DataFixture(StoreFixture::class, ['store_group_id' => '$store_group2.id$'], 'store2'),
+ DataFixture(
+ CustomerAttribute::class,
+ [
+ 'entity_type_id' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS,
+ 'used_in_forms' => ['customer_register_address'],
+ 'website_id' => '$website2.id$',
+ 'scope_is_visible' => 1,
+ 'is_visible' => 0,
+ ],
+ 'attribute_1'
+ ),
+ ]
+ public function testAttributesFormScope(): void
+ {
+ /** @var AttributeInterface $attribute1 */
+ $attribute1 = DataFixtureStorageManager::getStorage()->get('attribute_1');
+
+ $result = $this->graphQlQuery(sprintf(self::QUERY, 'customer_register_address'));
+
+ foreach ($result['attributesForm']['items'] as $item) {
+ if (array_contains($item, $attribute1->getAttributeCode())) {
+ $this->fail(
+ sprintf("Attribute '%s' found in query response in global scope", $attribute1->getAttributeCode())
+ );
+ }
+ }
+
+ /** @var StoreInterface $store */
+ $store = DataFixtureStorageManager::getStorage()->get('store2');
+
+ $result = $this->graphQlQuery(
+ sprintf(self::QUERY, 'customer_register_address'),
+ [],
+ '',
+ ['Store' => $store->getCode()]
+ );
+
+ foreach ($result['attributesForm']['items'] as $item) {
+ if (array_contains($item, $attribute1->getAttributeCode())) {
+ return;
+ }
+ }
+ $this->fail(
+ sprintf(
+ "Attribute '%s' not found in query response in website scope",
+ $attribute1->getAttributeCode()
+ )
+ );
+ }
}
diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/CustomerAddressAttributesTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/CustomerAddressAttributesTest.php
index e2131e820a4c..8c4e7afd26fd 100644
--- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/CustomerAddressAttributesTest.php
+++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/CustomerAddressAttributesTest.php
@@ -30,6 +30,7 @@ class CustomerAddressAttributesTest extends GraphQlAbstract
label
entity_type
frontend_input
+ frontend_class
is_required
default_value
is_unique
@@ -55,6 +56,7 @@ class CustomerAddressAttributesTest extends GraphQlAbstract
[
'entity_type_id' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS,
'frontend_input' => 'date',
+ 'frontend_class' => 'hidden-for-virtual',
'default_value' => '2023-03-22 00:00:00',
'input_filter' => 'DATE',
'validate_rules' =>
@@ -91,6 +93,7 @@ public function testMetadata(): void
'label' => $attribute->getFrontendLabel(),
'entity_type' => 'CUSTOMER_ADDRESS',
'frontend_input' => 'DATE',
+ 'frontend_class' => 'hidden-for-virtual',
'is_required' => false,
'default_value' => $attribute->getDefaultValue(),
'is_unique' => false,