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,