diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product.php index 40aa54fd9387..86137990cc57 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product.php @@ -7,14 +7,13 @@ namespace Magento\CatalogGraphQl\Model\Resolver; -use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\CatalogGraphQl\Model\Resolver\Product\ProductFieldsSelector; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Deferred\Product as ProductDataProvider; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Exception\GraphQlInputException; -use Magento\Framework\GraphQl\Query\FieldTranslator; -use Magento\Framework\GraphQl\Query\Resolver\Value; use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; /** * @inheritdoc @@ -32,23 +31,23 @@ class Product implements ResolverInterface private $valueFactory; /** - * @var FieldTranslator + * @var ProductFieldsSelector */ - private $fieldTranslator; + private $productFieldsSelector; /** * @param ProductDataProvider $productDataProvider * @param ValueFactory $valueFactory - * @param FieldTranslator $fieldTranslator + * @param ProductFieldsSelector $productFieldsSelector */ public function __construct( ProductDataProvider $productDataProvider, ValueFactory $valueFactory, - FieldTranslator $fieldTranslator + ProductFieldsSelector $productFieldsSelector ) { $this->productDataProvider = $productDataProvider; $this->valueFactory = $valueFactory; - $this->fieldTranslator = $fieldTranslator; + $this->productFieldsSelector = $productFieldsSelector; } /** @@ -60,7 +59,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value throw new GraphQlInputException(__('No child sku found for product link.')); } $this->productDataProvider->addProductSku($value['sku']); - $fields = $this->getProductFields($info); + $fields = $this->productFieldsSelector->getProductFieldsFromInfo($info); $this->productDataProvider->addEavAttributes($fields); $result = function () use ($value) { @@ -86,34 +85,4 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value return $this->valueFactory->create($result); } - - /** - * Return field names for all requested product fields. - * - * @param ResolveInfo $info - * @return string[] - */ - private function getProductFields(ResolveInfo $info) : array - { - $fieldNames = []; - foreach ($info->fieldNodes as $node) { - if ($node->name->value !== 'product') { - continue; - } - foreach ($node->selectionSet->selections as $selectionNode) { - if ($selectionNode->kind === 'InlineFragment') { - foreach ($selectionNode->selectionSet->selections as $inlineSelection) { - if ($inlineSelection->kind === 'InlineFragment') { - continue; - } - $fieldNames[] = $this->fieldTranslator->translate($inlineSelection->name->value); - } - continue; - } - $fieldNames[] = $this->fieldTranslator->translate($selectionNode->name->value); - } - } - - return $fieldNames; - } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php new file mode 100644 index 000000000000..9ddad4e6451f --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php @@ -0,0 +1,61 @@ +fieldTranslator = $fieldTranslator; + } + + /** + * Return field names for all requested product fields. + * + * @param ResolveInfo $info + * @param string $productNodeName + * @return string[] + */ + public function getProductFieldsFromInfo(ResolveInfo $info, string $productNodeName = 'product') : array + { + $fieldNames = []; + foreach ($info->fieldNodes as $node) { + if ($node->name->value !== $productNodeName) { + continue; + } + foreach ($node->selectionSet->selections as $selectionNode) { + if ($selectionNode->kind === 'InlineFragment') { + foreach ($selectionNode->selectionSet->selections as $inlineSelection) { + if ($inlineSelection->kind === 'InlineFragment') { + continue; + } + $fieldNames[] = $this->fieldTranslator->translate($inlineSelection->name->value); + } + continue; + } + $fieldNames[] = $this->fieldTranslator->translate($selectionNode->name->value); + } + } + + return $fieldNames; + } +} diff --git a/app/code/Magento/RelatedProductGraphQl/Model/DataProvider/RelatedProductDataProvider.php b/app/code/Magento/RelatedProductGraphQl/Model/DataProvider/RelatedProductDataProvider.php new file mode 100644 index 000000000000..173c0a94312e --- /dev/null +++ b/app/code/Magento/RelatedProductGraphQl/Model/DataProvider/RelatedProductDataProvider.php @@ -0,0 +1,78 @@ +linkFactory = $linkFactory; + } + + /** + * Related Products Data + * + * @param Product $product + * @param array $fields + * @param int $linkType + * @return array + */ + public function getData(Product $product, array $fields, int $linkType): array + { + $relatedProducts = $this->getRelatedProducts($product, $fields, $linkType); + + $productsData = []; + foreach ($relatedProducts as $relatedProduct) { + $productData = $relatedProduct->getData(); + $productData['model'] = $relatedProduct; + $productsData[] = $productData; + } + return $productsData; + } + + /** + * Get Related Products + * + * @param Product $product + * @param array $fields + * @param int $linkType + * @return Product[] + */ + private function getRelatedProducts(Product $product, array $fields, int $linkType): array + { + /** @var Link $link */ + $link = $this->linkFactory->create([ 'data' => [ + 'link_type_id' => $linkType, + ]]); + + $collection = $link->getProductCollection(); + $collection->setIsStrongMode(); + foreach ($fields as $field) { + $collection->addAttributeToSelect($field); + } + $collection->setProduct($product); + + return $collection->getItems(); + } +} diff --git a/app/code/Magento/RelatedProductGraphQl/Model/Resolver/CrossSellProducts.php b/app/code/Magento/RelatedProductGraphQl/Model/Resolver/CrossSellProducts.php new file mode 100644 index 000000000000..dcaa75b85f59 --- /dev/null +++ b/app/code/Magento/RelatedProductGraphQl/Model/Resolver/CrossSellProducts.php @@ -0,0 +1,59 @@ +productFieldsSelector = $productFieldsSelector; + $this->relatedProductDataProvider = $relatedProductDataProvider; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + $product = $value['model']; + $fields = $this->productFieldsSelector->getProductFieldsFromInfo($info, 'crosssell_products'); + + $data = $this->relatedProductDataProvider->getData($product, $fields, Link::LINK_TYPE_CROSSSELL); + return $data; + } +} diff --git a/app/code/Magento/RelatedProductGraphQl/Model/Resolver/RelatedProducts.php b/app/code/Magento/RelatedProductGraphQl/Model/Resolver/RelatedProducts.php new file mode 100644 index 000000000000..568c3282a6ee --- /dev/null +++ b/app/code/Magento/RelatedProductGraphQl/Model/Resolver/RelatedProducts.php @@ -0,0 +1,59 @@ +productFieldsSelector = $productFieldsSelector; + $this->relatedProductDataProvider = $relatedProductDataProvider; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + $product = $value['model']; + $fields = $this->productFieldsSelector->getProductFieldsFromInfo($info, 'related_products'); + + $data = $this->relatedProductDataProvider->getData($product, $fields, Link::LINK_TYPE_RELATED); + return $data; + } +} diff --git a/app/code/Magento/RelatedProductGraphQl/Model/Resolver/UpSellProducts.php b/app/code/Magento/RelatedProductGraphQl/Model/Resolver/UpSellProducts.php new file mode 100644 index 000000000000..df3a62408c53 --- /dev/null +++ b/app/code/Magento/RelatedProductGraphQl/Model/Resolver/UpSellProducts.php @@ -0,0 +1,59 @@ +productFieldsSelector = $productFieldsSelector; + $this->relatedProductDataProvider = $relatedProductDataProvider; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + $product = $value['model']; + $fields = $this->productFieldsSelector->getProductFieldsFromInfo($info, 'upsell_products'); + + $data = $this->relatedProductDataProvider->getData($product, $fields, Link::LINK_TYPE_UPSELL); + return $data; + } +} diff --git a/app/code/Magento/RelatedProductGraphQl/README.md b/app/code/Magento/RelatedProductGraphQl/README.md new file mode 100644 index 000000000000..5b57af5af988 --- /dev/null +++ b/app/code/Magento/RelatedProductGraphQl/README.md @@ -0,0 +1,3 @@ +# RelatedProductGraphQl + +**RelatedProductGraphQl** provides endpoints for getting Cross Sell / Related/ Up Sell products data. \ No newline at end of file diff --git a/app/code/Magento/RelatedProductGraphQl/composer.json b/app/code/Magento/RelatedProductGraphQl/composer.json new file mode 100644 index 000000000000..0d314d433f17 --- /dev/null +++ b/app/code/Magento/RelatedProductGraphQl/composer.json @@ -0,0 +1,26 @@ +{ + "name": "magento/module-related-product-graph-ql", + "description": "N/A", + "type": "magento2-module", + "require": { + "php": "~7.1.3||~7.2.0", + "magento/module-catalog": "*", + "magento/module-catalog-graph-ql": "*", + "magento/framework": "*" + }, + "suggest": { + "magento/module-graph-ql": "*" + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\RelatedProductGraphQl\\": "" + } + } +} diff --git a/app/code/Magento/RelatedProductGraphQl/etc/module.xml b/app/code/Magento/RelatedProductGraphQl/etc/module.xml new file mode 100644 index 000000000000..e30775c253a4 --- /dev/null +++ b/app/code/Magento/RelatedProductGraphQl/etc/module.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/code/Magento/RelatedProductGraphQl/etc/schema.graphqls b/app/code/Magento/RelatedProductGraphQl/etc/schema.graphqls new file mode 100644 index 000000000000..240fc6c4496f --- /dev/null +++ b/app/code/Magento/RelatedProductGraphQl/etc/schema.graphqls @@ -0,0 +1,8 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +interface ProductInterface { + related_products: [ProductInterface] @doc(description: "RelatedProduct") @resolver(class: "Magento\\RelatedProductGraphQl\\Model\\Resolver\\RelatedProducts") + upsell_products: [ProductInterface] @doc(description: "RelatedProduct") @resolver(class: "Magento\\RelatedProductGraphQl\\Model\\Resolver\\UpSellProducts") + crosssell_products: [ProductInterface] @doc(description: "RelatedProduct") @resolver(class: "Magento\\RelatedProductGraphQl\\Model\\Resolver\\CrossSellProducts") +} \ No newline at end of file diff --git a/app/code/Magento/RelatedProductGraphQl/registration.php b/app/code/Magento/RelatedProductGraphQl/registration.php new file mode 100644 index 000000000000..d18dfc27cc7c --- /dev/null +++ b/app/code/Magento/RelatedProductGraphQl/registration.php @@ -0,0 +1,9 @@ +graphQlQuery($query); + + self::assertArrayHasKey('products', $response); + self::assertArrayHasKey('items', $response['products']); + self::assertEquals(1, count($response['products']['items'])); + self::assertArrayHasKey(0, $response['products']['items']); + self::assertArrayHasKey('related_products', $response['products']['items'][0]); + $relatedProducts = $response['products']['items'][0]['related_products']; + self::assertCount(2, $relatedProducts); + self::assertRelatedProducts($relatedProducts); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/products_crosssell.php + */ + public function testQueryCrossSellProducts() + { + $productSku = 'simple_with_cross'; + + $query = <<graphQlQuery($query); + + self::assertArrayHasKey('products', $response); + self::assertArrayHasKey('items', $response['products']); + self::assertEquals(1, count($response['products']['items'])); + self::assertArrayHasKey(0, $response['products']['items']); + self::assertArrayHasKey('crosssell_products', $response['products']['items'][0]); + $crossSellProducts = $response['products']['items'][0]['crosssell_products']; + self::assertCount(1, $crossSellProducts); + $crossSellProduct = $crossSellProducts[0]; + self::assertArrayHasKey('sku', $crossSellProduct); + self::assertArrayHasKey('name', $crossSellProduct); + self::assertArrayHasKey('url_key', $crossSellProduct); + self::assertArrayHasKey('created_at', $crossSellProduct); + self::assertEquals($crossSellProduct['sku'], 'simple'); + self::assertEquals($crossSellProduct['name'], 'Simple Cross Sell'); + self::assertEquals($crossSellProduct['url_key'], 'simple-cross-sell'); + self::assertNotEmpty($crossSellProduct['created_at']); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/products_upsell.php + */ + public function testQueryUpSellProducts() + { + $productSku = 'simple_with_upsell'; + + $query = <<graphQlQuery($query); + + self::assertArrayHasKey('products', $response); + self::assertArrayHasKey('items', $response['products']); + self::assertEquals(1, count($response['products']['items'])); + self::assertArrayHasKey(0, $response['products']['items']); + self::assertArrayHasKey('upsell_products', $response['products']['items'][0]); + $upSellProducts = $response['products']['items'][0]['upsell_products']; + self::assertCount(1, $upSellProducts); + $upSellProduct = $upSellProducts[0]; + self::assertArrayHasKey('sku', $upSellProduct); + self::assertArrayHasKey('name', $upSellProduct); + self::assertArrayHasKey('url_key', $upSellProduct); + self::assertArrayHasKey('created_at', $upSellProduct); + self::assertEquals($upSellProduct['sku'], 'simple'); + self::assertEquals($upSellProduct['name'], 'Simple Up Sell'); + self::assertEquals($upSellProduct['url_key'], 'simple-up-sell'); + self::assertNotEmpty($upSellProduct['created_at']); + } + + /** + * @param array $relatedProducts + */ + private function assertRelatedProducts(array $relatedProducts): void + { + $expectedData = [ + 'simple' => [ + 'name' => 'Simple Related Product', + 'url_key' => 'simple-related-product', + + ], + 'simple_with_cross_two' => [ + 'name' => 'Simple Product With Related Product Two', + 'url_key' => 'simple-product-with-related-product-two', + ] + ]; + + foreach ($relatedProducts as $product) { + self::assertArrayHasKey('sku', $product); + self::assertArrayHasKey('name', $product); + self::assertArrayHasKey('url_key', $product); + self::assertArrayHasKey('created_at', $product); + + self::assertArrayHasKey($product['sku'], $expectedData); + $productExpectedData = $expectedData[$product['sku']]; + + self::assertEquals($product['name'], $productExpectedData['name']); + self::assertEquals($product['url_key'], $productExpectedData['url_key']); + self::assertNotEmpty($product['created_at']); + } + } +}