diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/etc/module.xml b/app/code/Magento/CatalogUrlRewriteGraphQl/etc/module.xml index be4bb9fcd701..e6e0f6cf7210 100644 --- a/app/code/Magento/CatalogUrlRewriteGraphQl/etc/module.xml +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/etc/module.xml @@ -6,5 +6,10 @@ */ --> - + + + + + + diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogUrlRewriteGraphQl/etc/schema.graphqls index b96cfcb03d41..f4ad2e930dda 100644 --- a/app/code/Magento/CatalogUrlRewriteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/etc/schema.graphqls @@ -4,6 +4,7 @@ interface ProductInterface { url_key: String @doc(description: "The part of the URL that identifies the product") url_path: String @doc(description: "The part of the URL that precedes the url_key") + url_rewrites: [UrlRewrite] @doc(description: "URL rewrites list") @resolver(class: "Magento\\UrlRewriteGraphQl\\Model\\Resolver\\UrlRewrite") } input ProductFilterInput { diff --git a/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php b/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php new file mode 100644 index 000000000000..1c25ffd1e9ff --- /dev/null +++ b/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php @@ -0,0 +1,148 @@ +urlFinder = $urlFinder; + $this->storeManager = $storeManager; + $this->customUrlLocator = $customUrlLocator; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($args['url']) || empty(trim($args['url']))) { + throw new GraphQlInputException(__('"url" argument should be specified and not empty')); + } + + $result = null; + $url = $args['url']; + if (substr($url, 0, 1) === '/' && $url !== '/') { + $url = ltrim($url, '/'); + } + $customUrl = $this->customUrlLocator->locateUrl($url); + $url = $customUrl ?: $url; + $urlRewrite = $this->findCanonicalUrl($url); + if ($urlRewrite) { + $result = [ + 'id' => $urlRewrite->getEntityId(), + 'canonical_url' => $urlRewrite->getTargetPath(), + 'type' => $this->sanitizeType($urlRewrite->getEntityType()) + ]; + } + return $result; + } + + /** + * Find the canonical url passing through all redirects if any + * + * @param string $requestPath + * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite|null + */ + private function findCanonicalUrl(string $requestPath) : ?\Magento\UrlRewrite\Service\V1\Data\UrlRewrite + { + $urlRewrite = $this->findUrlFromRequestPath($requestPath); + if ($urlRewrite && $urlRewrite->getRedirectType() > 0) { + while ($urlRewrite && $urlRewrite->getRedirectType() > 0) { + $urlRewrite = $this->findUrlFromRequestPath($urlRewrite->getTargetPath()); + } + } + if (!$urlRewrite) { + $urlRewrite = $this->findUrlFromTargetPath($requestPath); + } + + return $urlRewrite; + } + + /** + * Find a url from a request url on the current store + * + * @param string $requestPath + * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite|null + */ + private function findUrlFromRequestPath(string $requestPath) : ?\Magento\UrlRewrite\Service\V1\Data\UrlRewrite + { + return $this->urlFinder->findOneByData( + [ + 'request_path' => $requestPath, + 'store_id' => $this->storeManager->getStore()->getId() + ] + ); + } + + /** + * Find a url from a target url on the current store + * + * @param string $targetPath + * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite|null + */ + private function findUrlFromTargetPath(string $targetPath) : ?\Magento\UrlRewrite\Service\V1\Data\UrlRewrite + { + return $this->urlFinder->findOneByData( + [ + 'target_path' => $targetPath, + 'store_id' => $this->storeManager->getStore()->getId() + ] + ); + } + + /** + * Sanitize the type to fit schema specifications + * + * @param string $type + * @return string + */ + private function sanitizeType(string $type) : string + { + return strtoupper(str_replace('-', '_', $type)); + } +} diff --git a/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/UrlRewrite.php b/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/UrlRewrite.php index 5bdaf3627253..0c4c78b58294 100644 --- a/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/UrlRewrite.php +++ b/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/UrlRewrite.php @@ -11,12 +11,12 @@ use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; -use Magento\Store\Model\StoreManagerInterface; +use Magento\Framework\Model\AbstractModel; use Magento\UrlRewrite\Model\UrlFinderInterface; -use Magento\UrlRewriteGraphQl\Model\Resolver\UrlRewrite\CustomUrlLocatorInterface; +use Magento\UrlRewrite\Service\V1\Data\UrlRewrite as UrlRewriteDTO; /** - * UrlRewrite field resolver, used for GraphQL request processing. + * Returns URL rewrites list for the specified product */ class UrlRewrite implements ResolverInterface { @@ -25,29 +25,13 @@ class UrlRewrite implements ResolverInterface */ private $urlFinder; - /** - * @var StoreManagerInterface - */ - private $storeManager; - - /** - * @var CustomUrlLocatorInterface - */ - private $customUrlLocator; - /** * @param UrlFinderInterface $urlFinder - * @param StoreManagerInterface $storeManager - * @param CustomUrlLocatorInterface $customUrlLocator */ public function __construct( - UrlFinderInterface $urlFinder, - StoreManagerInterface $storeManager, - CustomUrlLocatorInterface $customUrlLocator + UrlFinderInterface $urlFinder ) { $this->urlFinder = $urlFinder; - $this->storeManager = $storeManager; - $this->customUrlLocator = $customUrlLocator; } /** @@ -59,90 +43,51 @@ public function resolve( ResolveInfo $info, array $value = null, array $args = null - ) { - if (!isset($args['url']) || empty(trim($args['url']))) { - throw new GraphQlInputException(__('"url" argument should be specified and not empty')); + ): array { + if (!isset($value['model'])) { + throw new GraphQlInputException(__('"model" value should be specified')); } - $result = null; - $url = $args['url']; - if (substr($url, 0, 1) === '/' && $url !== '/') { - $url = ltrim($url, '/'); - } - $customUrl = $this->customUrlLocator->locateUrl($url); - $url = $customUrl ?: $url; - $urlRewrite = $this->findCanonicalUrl($url); - if ($urlRewrite) { - $result = [ - 'id' => $urlRewrite->getEntityId(), - 'relative_url' => $urlRewrite->getTargetPath(), - 'type' => $this->sanitizeType($urlRewrite->getEntityType()) - ]; - } - return $result; - } + /** @var AbstractModel $entity */ + $entity = $value['model']; + $entityId = $entity->getEntityId(); - /** - * Find the canonical url passing through all redirects if any - * - * @param string $requestPath - * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite|null - */ - private function findCanonicalUrl(string $requestPath) : ?\Magento\UrlRewrite\Service\V1\Data\UrlRewrite - { - $urlRewrite = $this->findUrlFromRequestPath($requestPath); - if ($urlRewrite && $urlRewrite->getRedirectType() > 0) { - while ($urlRewrite && $urlRewrite->getRedirectType() > 0) { - $urlRewrite = $this->findUrlFromRequestPath($urlRewrite->getTargetPath()); + $urlRewriteCollection = $this->urlFinder->findAllByData([UrlRewriteDTO::ENTITY_ID => $entityId]); + $urlRewrites = []; + + /** @var UrlRewriteDTO $urlRewrite */ + foreach ($urlRewriteCollection as $urlRewrite) { + if ($urlRewrite->getRedirectType() !== 0) { + continue; } + + $urlRewrites[] = [ + 'url' => $urlRewrite->getRequestPath(), + 'parameters' => $this->getUrlParameters($urlRewrite->getTargetPath()) + ]; } - if (!$urlRewrite) { - $urlRewrite = $this->findUrlFromTargetPath($requestPath); - } - - return $urlRewrite; - } - /** - * Find a url from a request url on the current store - * - * @param string $requestPath - * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite|null - */ - private function findUrlFromRequestPath(string $requestPath) : ?\Magento\UrlRewrite\Service\V1\Data\UrlRewrite - { - return $this->urlFinder->findOneByData( - [ - 'request_path' => $requestPath, - 'store_id' => $this->storeManager->getStore()->getId() - ] - ); + return $urlRewrites; } /** - * Find a url from a target url on the current store + * Parses target path and extracts parameters * * @param string $targetPath - * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite|null + * @return array */ - private function findUrlFromTargetPath(string $targetPath) : ?\Magento\UrlRewrite\Service\V1\Data\UrlRewrite + private function getUrlParameters(string $targetPath): array { - return $this->urlFinder->findOneByData( - [ - 'target_path' => $targetPath, - 'store_id' => $this->storeManager->getStore()->getId() - ] - ); - } + $urlParameters = []; + $targetPathParts = explode('/', trim($targetPath, '/')); - /** - * Sanitize the type to fit schema specifications - * - * @param string $type - * @return string - */ - private function sanitizeType(string $type) : string - { - return strtoupper(str_replace('-', '_', $type)); + for ($i = 3; ($i < sizeof($targetPathParts) - 1); $i += 2) { + $urlParameters[] = [ + 'name' => $targetPathParts[$i], + 'value' => $targetPathParts[$i + 1] + ]; + } + + return $urlParameters; } } diff --git a/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls b/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls index e9a39617774c..dae695c69a33 100644 --- a/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls @@ -1,15 +1,25 @@ # Copyright © Magento, Inc. All rights reserved. # See COPYING.txt for license details. -type EntityUrl @doc(description: "EntityUrl is an output object containing the `id`, `relative_url`, and `type` attributes") { +type EntityUrl @doc(description: "EntityUrl is an output object containing the `id`, `canonical_url`, and `type` attributes") { id: Int @doc(description: "The ID assigned to the object associated with the specified url. This could be a product ID, category ID, or page ID.") - relative_url: String @doc(description: "The internal relative URL. If the specified url is a redirect, the query returns the redirected URL, not the original.") + canonical_url: String @doc(description: "The internal relative URL. If the specified url is a redirect, the query returns the redirected URL, not the original.") type: UrlRewriteEntityTypeEnum @doc(description: "One of PRODUCT, CATEGORY, or CMS_PAGE.") } type Query { - urlResolver(url: String!): EntityUrl @resolver(class: "Magento\\UrlRewriteGraphQl\\Model\\Resolver\\UrlRewrite") @doc(description: "The urlResolver query returns the relative URL for a specified product, category or CMS page") + urlResolver(url: String!): EntityUrl @resolver(class: "Magento\\UrlRewriteGraphQl\\Model\\Resolver\\EntityUrl") @doc(description: "The urlResolver query returns the relative URL for a specified product, category or CMS page") } enum UrlRewriteEntityTypeEnum { } + +type UrlRewrite @doc(description: "The object contains URL rewrite details") { + url: String @doc(description: "Request URL") + parameters: [HttpQueryParameter] @doc(description: "Request parameters") +} + +type HttpQueryParameter @doc(description: "The object details of target path parameters") { + name: String @doc(description: "Parameter name") + value: String @doc(description: "Parameter value") +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/UrlRewritesTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/UrlRewritesTest.php new file mode 100644 index 000000000000..c39b32e4bfa4 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/UrlRewritesTest.php @@ -0,0 +1,149 @@ +graphQlQuery($query); + + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); + $product = $productRepository->get('virtual-product', false, null, true); + + $urlFinder = ObjectManager::getInstance()->get(UrlFinderInterface::class); + + $rewritesCollection = $urlFinder->findAllByData([UrlRewriteDTO::ENTITY_ID => $product->getId()]); + + /* There should be only one rewrite */ + /** @var UrlRewriteDTO $urlRewrite */ + $urlRewrite = current($rewritesCollection); + + $this->assertArrayHasKey('url_rewrites', $response['products']['items'][0]); + $this->assertCount(1, $response['products']['items'][0]['url_rewrites']); + + $this->assertResponseFields( + $response['products']['items'][0]['url_rewrites'][0], + [ + "url" => $urlRewrite->getRequestPath(), + "parameters" => $this->getUrlParameters($urlRewrite->getTargetPath()) + ] + ); + } + + /** + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function testProductWithOneCategoryAssigned() + { + $productSku = 'simple'; + $query + = <<graphQlQuery($query); + + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); + $product = $productRepository->get('simple', false, null, true); + + $urlFinder = ObjectManager::getInstance()->get(UrlFinderInterface::class); + + $rewritesCollection = $urlFinder->findAllByData([UrlRewriteDTO::ENTITY_ID => $product->getId()]); + $rewritesCount = count($rewritesCollection); + + $this->assertArrayHasKey('url_rewrites', $response['products']['items'][0]); + $this->assertCount($rewritesCount, $response['products']['items'][0]['url_rewrites']); + + for ($i = 0; $i < $rewritesCount; $i++) { + $urlRewrite = $rewritesCollection[$i]; + $this->assertResponseFields( + $response['products']['items'][0]['url_rewrites'][$i], + [ + "url" => $urlRewrite->getRequestPath(), + "parameters" => $this->getUrlParameters($urlRewrite->getTargetPath()) + ] + ); + } + } + + /** + * Parses target path and extracts parameters + * + * @param string $targetPath + * @return array + */ + private function getUrlParameters(string $targetPath): array + { + $urlParameters = []; + $targetPathParts = explode('/', trim($targetPath, '/')); + + for ($i = 3; ($i < sizeof($targetPathParts) - 1); $i += 2) { + $urlParameters[] = [ + 'name' => $targetPathParts[$i], + 'value' => $targetPathParts[$i + 1] + ]; + } + + return $urlParameters; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/UrlRewrite/UrlResolverTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/UrlRewrite/UrlResolverTest.php index 370121a1dad7..c70b1631e85c 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/UrlRewrite/UrlResolverTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/UrlRewrite/UrlResolverTest.php @@ -31,7 +31,7 @@ protected function setUp() } /** - * Tests if target_path(relative_url) is resolved for Product entity + * Tests if target_path(canonical_url) is resolved for Product entity * * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php */ @@ -60,7 +60,7 @@ public function testProductUrlResolver() urlResolver(url:"{$urlPath}") { id - relative_url + canonical_url type } } @@ -68,12 +68,12 @@ public function testProductUrlResolver() $response = $this->graphQlQuery($query); $this->assertArrayHasKey('urlResolver', $response); $this->assertEquals($product->getEntityId(), $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); + $this->assertEquals($targetPath, $response['urlResolver']['canonical_url']); $this->assertEquals(strtoupper($expectedType), $response['urlResolver']['type']); } /** - * Tests the use case where relative_url is provided as resolver input in the Query + * Tests the use case where canonical_url is provided as resolver input in the Query * * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php */ @@ -104,7 +104,7 @@ public function testProductUrlWithCanonicalUrlInput() urlResolver(url:"{$canonicalPath}") { id - relative_url + canonical_url type } } @@ -112,7 +112,7 @@ public function testProductUrlWithCanonicalUrlInput() $response = $this->graphQlQuery($query); $this->assertArrayHasKey('urlResolver', $response); $this->assertEquals($product->getEntityId(), $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); + $this->assertEquals($targetPath, $response['urlResolver']['canonical_url']); $this->assertEquals(strtoupper($expectedType), $response['urlResolver']['type']); } @@ -147,7 +147,7 @@ public function testCategoryUrlResolver() urlResolver(url:"{$urlPath2}") { id - relative_url + canonical_url type } } @@ -155,7 +155,7 @@ public function testCategoryUrlResolver() $response = $this->graphQlQuery($query); $this->assertArrayHasKey('urlResolver', $response); $this->assertEquals($categoryId, $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); + $this->assertEquals($targetPath, $response['urlResolver']['canonical_url']); $this->assertEquals(strtoupper($expectedType), $response['urlResolver']['type']); } @@ -183,14 +183,14 @@ public function testCMSPageUrlResolver() urlResolver(url:"{$requestPath}") { id - relative_url + canonical_url type } } QUERY; $response = $this->graphQlQuery($query); $this->assertEquals($cmsPageId, $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); + $this->assertEquals($targetPath, $response['urlResolver']['canonical_url']); $this->assertEquals(strtoupper(str_replace('-', '_', $expectedEntityType)), $response['urlResolver']['type']); } @@ -226,7 +226,7 @@ public function testProductUrlRewriteResolver() urlResolver(url:"{$urlPath}") { id - relative_url + canonical_url type } } @@ -234,7 +234,7 @@ public function testProductUrlRewriteResolver() $response = $this->graphQlQuery($query); $this->assertArrayHasKey('urlResolver', $response); $this->assertEquals($product->getEntityId(), $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); + $this->assertEquals($targetPath, $response['urlResolver']['canonical_url']); $this->assertEquals(strtoupper($expectedType), $response['urlResolver']['type']); } @@ -266,7 +266,7 @@ public function testInvalidUrlResolverInput() urlResolver(url:"{$urlPath}") { id - relative_url + canonical_url type } } @@ -307,7 +307,7 @@ public function testCategoryUrlWithLeadingSlash() urlResolver(url:"/{$urlPath}") { id - relative_url + canonical_url type } } @@ -315,7 +315,7 @@ public function testCategoryUrlWithLeadingSlash() $response = $this->graphQlQuery($query); $this->assertArrayHasKey('urlResolver', $response); $this->assertEquals($categoryId, $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); + $this->assertEquals($targetPath, $response['urlResolver']['canonical_url']); $this->assertEquals(strtoupper($expectedType), $response['urlResolver']['type']); } @@ -344,7 +344,7 @@ public function testResolveSlash() urlResolver(url:"/") { id - relative_url + canonical_url type } } @@ -352,7 +352,7 @@ public function testResolveSlash() $response = $this->graphQlQuery($query); $this->assertArrayHasKey('urlResolver', $response); $this->assertEquals($homePageId, $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); + $this->assertEquals($targetPath, $response['urlResolver']['canonical_url']); $this->assertEquals('CMS_PAGE', $response['urlResolver']['type']); } }