URL Rewrites allow to display catalog URLs (/catalog/product/id/123) in a comprehensive manner (/black-tshirt.html). UrlRewrite Router is responsible for their processing (\Magento\UrlRewrite\Controller\Router). It looks for a necessary URL in a database and, if found successfully, sets module, controller and action names, as well as product or category id.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
public function match(\Magento\Framework\App\RequestInterface $request) { //If we're in the process of switching stores then matching rewrite //rule from previous store because the URL was not changed yet from //old store's format. if ($fromStore = $request->getParam('___from_store')) { $oldStoreId = $this->storeManager->getStore($fromStore)->getId(); $oldRewrite = $this->getRewrite( $request->getPathInfo(), $oldStoreId ); if ($oldRewrite && $oldRewrite->getRedirectType() === 0) { //If there is a match and it's a correct URL then just //redirecting to current store's URL equivalent, //otherwise just continuing finding a rule within current store. $currentRewrite = $this->urlFinder->findOneByData( [ UrlRewrite::ENTITY_TYPE => $oldRewrite->getEntityType(), UrlRewrite::ENTITY_ID => $oldRewrite->getEntityId(), UrlRewrite::STORE_ID => $this->storeManager->getStore()->getId(), UrlRewrite::REDIRECT_TYPE => 0, ] ); if ($currentRewrite && $currentRewrite->getRequestPath() !== $oldRewrite->getRequestPath() ) { return $this->redirect( $request, $this->url->getUrl( '', ['_direct' => $currentRewrite->getRequestPath()] ), OptionProvider::TEMPORARY ); } } } $rewrite = $this->getRewrite( $request->getPathInfo(), $this->storeManager->getStore()->getId() ); if ($rewrite === null) { //No rewrite rule matching current URl found, continuing with //processing of this URL. return null; } if ($rewrite->getRedirectType()) { //Rule requires the request to be redirected to another URL //and cannot be processed further. return $this->processRedirect($request, $rewrite); } //Rule provides actual URL that can be processed by a controller. $request->setAlias( UrlInterface::REWRITE_REQUEST_PATH_ALIAS, $rewrite->getRequestPath() ); $request->setPathInfo('/' . $rewrite->getTargetPath()); return $this->actionFactory->create( \Magento\Framework\App\Action\Forward::class ); } |
Finding a matching URL rewrite
\Magento\UrlRewrite\Controller\Router::getRewrite method is responsible for searching a URL in the database.
1 2 3 4 5 6 7 |
protected function getRewrite($requestPath, $storeId) { return $this->urlFinder->findOneByData([ UrlRewrite::REQUEST_PATH => ltrim($requestPath, '/'), UrlRewrite::STORE_ID => $storeId, ]); } |
This method looks for a suitable URL Rewrite in the url_rewrites table, based on the request path and current store id. If found, the object \Magento\UrlRewrite\Service\V1\Data\UrlRewrite is returned.
Generating a URL rewrite
Creation, modification and removal of URL rewrites for catalog entities are done using the module Magento_CatalogUrlRewrite. This module contains the observers responsible for the changes in the catalog entities.
Example: processing URL Rewrite while saving the product.
The process starts in observer method \Magento\CatalogUrlRewrite\Observer\ProductProcessUrlRewriteSavingObserver which is called on the event catalog_product_save_after.
Events.xml
1 2 3 |
<event name="catalog_product_save_after"> <observer name="process_url_rewrite_saving" instance="Magento\CatalogUrlRewrite\Observer\ProductProcessUrlRewriteSavingObserver"/> </event> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public function execute(\Magento\Framework\Event\Observer $observer) { /** @var Product $product */ $product = $observer->getEvent()->getProduct(); if ($product->dataHasChangedFor('url_key') || $product->getIsChangedCategories() || $product->getIsChangedWebsites() || $product->dataHasChangedFor('visibility') ) { $this->urlPersist->deleteByData([ UrlRewrite::ENTITY_ID => $product->getId(), UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, UrlRewrite::REDIRECT_TYPE => 0, UrlRewrite::STORE_ID => $product->getStoreId() ]); if ($product->isVisibleInSiteVisibility()) { $this->urlPersist->replace($this->productUrlRewriteGenerator->generate($product)); } } } |
After obtaining the product model from the observer, it is checked to find out whether the product’s url_key and visibility have changed, as well as product’s relation to the categories or stores.
If anything has changed, then \Magento\UrlRewrite\Model\Storage\DbStorage::deleteByData method is used to delete the existing URL rewrites for this product. If the product settings allow it to be displayed, the \Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator::generate method is called.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public function generate(Product $product, $rootCategoryId = null) { if ($product->getVisibility() == Visibility::VISIBILITY_NOT_VISIBLE) { return []; } $storeId = $product->getStoreId(); $productCategories = $product->getCategoryCollection() ->addAttributeToSelect('url_key') ->addAttributeToSelect('url_path'); $urls = $this->isGlobalScope($storeId) ? $this->generateForGlobalScope($productCategories, $product, $rootCategoryId) : $this->generateForSpecificStoreView($storeId, $productCategories, $product, $rootCategoryId); return $urls; } |
After checking the product visibility, this method gets a collection of all the categories associated with the product, and then, depending on the current scope, calls the \Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator::generateForGlobalScope or \Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator::generateForSpecificStoreView methods.
Let’s take a look at the situation with the global scope.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
protected function generateForGlobalScope($productCategories, $product = null, $rootCategoryId = null) { return $this->getProductScopeRewriteGenerator()->generateForGlobalScope( $productCategories, $product, $rootCategoryId ); } public function generateForGlobalScope($productCategories, Product $product, $rootCategoryId = null) { $productId = $product->getEntityId(); $mergeDataProvider = clone $this->mergeDataProviderPrototype; foreach ($product->getStoreIds() as $id) { if (!$this->isGlobalScope($id) && !$this->storeViewService->doesEntityHaveOverriddenUrlKeyForStore( $id, $productId, Product::ENTITY )) { $mergeDataProvider->merge( $this->generateForSpecificStoreView($id, $productCategories, $product, $rootCategoryId) ); } } return $mergeDataProvider->getData(); } |
For each of the available stores, the \Magento\CatalogUrlRewrite\Model\ProductScopeRewriteGenerator::generateForGlobalScope method checks to see if the url_key of the product duplicates an existing one. If url_key is unique, it calls the generation of the URL rewrite for this store. After creating URL rewrites for all the existing stores, they are combined into a common array using the \Magento\UrlRewrite\Model\MergeDataProvider class object.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
<b>public function </b>generateForSpecificStoreView($storeId, $productCategories, Product $product, $rootCategoryId = <b>null</b>) { $mergeDataProvider = clone $this->mergeDataProviderPrototype; $categories = []; foreach ($productCategories as $category) { if (!$this->isCategoryProperForGenerating($category, $storeId)) { continue; } // category should be loaded per appropriate store if category's URL key has been changed $categories[] = $this->getCategoryWithOverriddenUrlKey($storeId, $category); } $productCategories = $this->objectRegistryFactory->create(['entities' => $categories]); $mergeDataProvider->merge( $this->canonicalUrlRewriteGenerator->generate($storeId, $product) ); $mergeDataProvider->merge( $this->categoriesUrlRewriteGenerator->generate($storeId, $product, $productCategories) ); $mergeDataProvider->merge( $this->currentUrlRewritesRegenerator->generate( $storeId, $product, $productCategories, $rootCategoryId ) ); $mergeDataProvider->merge( $this->anchorUrlRewriteGenerator->generate($storeId, $product, $productCategories) ); $mergeDataProvider->merge( $this->currentUrlRewritesRegenerator->generateAnchor( $storeId, $product, $productCategories, $rootCategoryId ) ); return $mergeDataProvider->getData(); } |
The method generateForSpecificStoreView of class \Magento\CatalogUrlRewrite\Model\ProductScopeRewriteGenerator gets a specific url_key for each product category of the current store, and then uses the \Magento\UrlRewrite\Model\MergeDataProvider instance to combine the results of several calls:
- \Magento\CatalogUrlRewrite\Model\Product\CanonicalUrlRewriteGenerator::generate — creates URL Rewrite not containing categories;
- \Magento\CatalogUrlRewrite\Model\Product\CategoriesUrlRewriteGenerator::generate — creates URL Rewrite including possible categories;
- \Magento\CatalogUrlRewrite\Model\Product\CurrentUrlRewritesRegenerator::generate — creates URL Rewrites considering the existing URL rewrites for this entity (e.g. custom URL rewrites);
- \Magento\CatalogUrlRewrite\Model\Product\AnchorUrlRewriteGenerator::generate — generates URL Rewrites for anchor categories;
- The method generateAnchor of class \Magento\CatalogUrlRewrite\Model\Product\CurrentUrlRewritesRegenerator — generates URL Rewrites for anchor categories considering the existing URL rewrites.
The result of all these methods is combined into an array and returned to the \Magento\UrlRewrite\Model\Storage\AbstractStorage::replace method, which is responsible for the URL Rewrites persistence.
This is how URL rewrites work in Magento 2. You can also check out the comparison between Magento 1 and Magento 2 URL rewrites, which is among the topics of the developer certification exam.
Need experienced Magento developers? BelVG can help!
If you need to modify the URL of a product, catalog or any CMS page or redirect one page to another, URL rewrite comes into play. You can automatically enable the Magento 2 URL rewrite in a few clicks.
Hi, David! It’s great that you like your work! Stay tuned for more helpful articles.
This tutorial is helpful!. Thanks.
Hi, Damon!
I am happy to hear that my article is helpful!
A very informative article, enabling URL rewrite’s really contributes in improving search engine optimization of Magento store, there’s another really good tutorial for this magenticians. I found it really helpful and to the point, do have a look at it too. Thanks