Routing in Magento 2

Routing in Magento 2

We can say that routing in Magento is one of the most important parts. Complete application (Magento 2) flow depends on processing URL request and router classes which are responsible for matching and processing that requests. This article covers routers flow in Magento 2 and analyses some routers that come with default installation. Also it will be shown how to create one custom router. It will be mentioned how routers match action (Controller class), and more info about Controllers will be covered in a separated article.

Routing flow

First, we need to analyze complete routing flow, so we can get in more details for every part later. As we already know, Magento 2 creates HTTP application in which class (launch method) request flow will start. Our flow starts with creating front controller:

$frontController = $this->_objectManager->get('Magento\Framework\App\FrontControllerInterface');

Front controller is responsible for looping trough all available routers and matching responsible router for current request. We’ll cover Front Controller in details little later. For now, to understand complete flow it’s important to know how application matches routers. Routers list is created in RouterList (called in Front Controller for looping on routers) class, located in Magento\Framework\App, and this class is responsible for ordering and iteration on routers list. Router class is responsible for matching if router is responsible for current request. Let’s have a look at Magento 2 flow:

index.php (runs bootstrap and create HTTP application) → HTTP app → FrontController → Routing → Controller processing → etc

Now we’ll analyze every part of routing flow for better understanding of routing in Magento 2.

Front Controller

Same as in Magento 1, this is entry routing point which is called at HTTP application launch (launch method). It’s responsible for matching router which is responsible for current request. It’s located under lib/internal/Magento/Framework/App/FrontController.php. You can see that at HTTP launch method FrontControllerInterface is used. Let’s look at that in code:

class Http implements \Magento\Framework\AppInterface
{
public function launch()
{
...
//Here Application is calling front controller and it's dispatch method
$frontController = $this->_objectManager->get('Magento\Framework\App\FrontControllerInterface');
$result = $frontController->dispatch($this->_request);
...
}
}

Now, when we know how and when front controller is called, let’s take a look at Front controller class and dispatch method itself:

lib/internal/Magento/Framework/App/FrontController.php
class FrontController implements FrontControllerInterface
{
public function dispatch(RequestInterface $request)
{
\Magento\Framework\Profiler::start('routers_match');
$routingCycleCounter = 0;
$result = null;
while (!$request->isDispatched() && $routingCycleCounter++ < 100) {
/** @var \Magento\Framework\App\RouterInterface $router */
foreach ($this->_routerList as $router) {
try {
$actionInstance = $router->match($request);
if ($actionInstance) {
$request->setDispatched(true);
$actionInstance->getResponse()->setNoCacheHeaders();
$result = $actionInstance->dispatch($request);
break;
}
} catch (\Magento\Framework\Exception\NotFoundException $e) {
$request->initForward();
$request->setActionName('noroute');
$request->setDispatched(false);
break;
}
}
}
\Magento\Framework\Profiler::stop('routers_match');
if ($routingCycleCounter > 100) {
throw new \LogicException('Front controller reached 100 router match iterations');
}
return $result;
}
}

As we can see, dispatch method will loop trough all routers (enabled, we’ll cover this later in router configuration) until one router is matched and request is dispatched ($request→setDispatched(true);) or routing cycle counter exceeds 100. Router can be matched, but if there is no dispatch it will repeat the loop trough routers (action forward). Also, router can be redirected and dispatched or it can be matched and processed. Routers list class is explained at request flow. Now, we can move forward to see how router matching (match method) works and what exactly are routers.

Router

Shortly, router is PHP class responsible for matching and processing URL request. By default, there are some routers in Magento framework and Magento core like; Base, DefaultRouter, Cms and UrlRewrite. We’ll cover them all, explaining their purpose and how they work. Routers are implementing RouterInterface. Now, let’s take a look at the flow of default routers:

Base Router → CMS Router → UrlRewrite Router → Default Router

(this is routers loop – FrontController::dispatch())

Base Router

Located at lib/internal/Magento/Framework/App/Router/Base.php, it’s the first one in the loop and if you are Magento 1 developer you know it as the Standard Router. Match method will parseRequest and matchAction, and in second method it will set module front name, controller path name, action name, controller module and route name. At base router standard Magento URL (front name/action path/action/param 1/etc params/) is matched.

CMS Router

CMS Router is located in app/code/Magento/Cms/Controller/Router.php, it’s used for handling CMS pages, and it sets module name (module front name) to “cms”, controller name (controller path name) to “page” and action name to “view” – app/code/Magento/Cms/Controller/Page/View.php controller. After setting format for Base controller it will set page ID and forward it, but it will not dispatch it. Forwarding means that it will break current routers loop and start the loop again (it can do that 100 times max). That router loop will match base router which will activate View controller in Cms/Controller/Page and show saved page ID (found page ID depending on url).

UrlRewrite Router

In Magento 2 UrlRewrite has it’s own router, and if you are familiar with Magento 1 then you’ll know that Url Rewrite is part of the Standard router. It’s located in: app/code/Magento/UrlRewrite/Controller/Router.php and it’s using Url Finder to get url rewrite that matches url from the database:

$rewrite = $this->urlFinder->findOneByData(
[
UrlRewrite::ENTITY_TYPE => $oldRewrite->getEntityType(),
UrlRewrite::ENTITY_ID => $oldRewrite->getEntityId(),
UrlRewrite::STORE_ID => $this->storeManager->getStore()->getId(),
UrlRewrite::IS_AUTOGENERATED => 1,
]
);

It will forward action like Cms router.

Default Router

It’s located in lib/internal/Magento/Framework/App/Router/DefaultRouter.php and it’s last in the routers loop. It’s used when every other router doesn’t match. In Magento 2 we can create custom handle for “Not found” page to display custom content. Here is the loop in DefaultRouter for no route handler list:

foreach ($this->noRouteHandlerList->getHandlers() as $noRouteHandler) {
if ($noRouteHandler->process($request)) {
break;
}
}

Custom Router (with an example)

Front Controller will go through all routers in routersList (created from configuration in routes.xml), so we need to add custom router in lib/internal/Magento/Framework/App/RouterList.php by adding our configuration for new router in di.xml module. We’ll create new module (let’s call it Inchoo/CustomRouter), then we’ll add new router in routersList and lastly, create router class.

Custom router is just an example in which you can see how to match and forward request for Base router to match. First, we need to create folder structure for our module which is located in app/code/Inchoo/CustomRouter, and then we’ll create module.xml in etc folder and composer.json in module root with module informations. Now, we can create custom router by adding configuration to di.xml in etc/frontend folder because we want to have custom router only for frontend. Lastly, we’ll create Router.php in Controller folder with logic for matching router. We will search the URL and check if there is specific word in URL and then, depending on that word, we will set module front name, controller path name, action name and then forward request for base controller. We’ll search for two words: “examplerouter” and “exampletocms”. On “examplerouter” match, we will forward to Base router match format (by setting module front name to “inchootest”, controller path name to “test”, action name to “test”), and on “exampletocms”, we will forward to Base router to display About us page.

di.xml (located in etc/frontend)

<?xml version="1.0"?>
<!--
/**
* Copyright © 2015 Inchoo d.o.o.
* created by Zoran Salamun(zoran.salamun@inchoo.net)
* Module is created for Custom Router demonstration
*/
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../lib/internal/Magento/Framework/ObjectManager/etc/config.xsd">
<type name="Magento\Framework\App\RouterList">
<arguments>
<argument name="routerList" xsi:type="array">
<item name="inchoocustomrouter" xsi:type="array">
<item name="class" xsi:type="string">Inchoo\CustomRouter\Controller\Router</item>
<item name="disable" xsi:type="boolean">false</item>
<item name="sortOrder" xsi:type="string">22</item>
</item>
</argument>
</arguments>
</type>
</config>

Router.php (located in Controller folder)

<?php
namespace Inchoo\CustomRouter\Controller;
 
/**
* Inchoo Custom router Controller Router
*
* @author Zoran Salamun <zoran.salamun@inchoo.net>
*/
class Router implements \Magento\Framework\App\RouterInterface
{
/**
* @var \Magento\Framework\App\ActionFactory
*/
protected $actionFactory;
 
/**
* Response
*
* @var \Magento\Framework\App\ResponseInterface
*/
protected $_response;
 
/**
* @param \Magento\Framework\App\ActionFactory $actionFactory
* @param \Magento\Framework\App\ResponseInterface $response
*/
public function __construct(
\Magento\Framework\App\ActionFactory $actionFactory,
\Magento\Framework\App\ResponseInterface $response
) {
$this->actionFactory = $actionFactory;
$this->_response = $response;
}
 
/**
* Validate and Match
*
* @param \Magento\Framework\App\RequestInterface $request
* @return bool
*/
public function match(\Magento\Framework\App\RequestInterface $request)
{
/*
* We will search “examplerouter” and “exampletocms” words and make forward depend on word
* -examplerouter will forward to base router to match inchootest front name, test controller path and test controller class
* -exampletocms will set front name to cms, controller path to page and action to view
*/
$identifier = trim($request->getPathInfo(), '/');
if(strpos($identifier, 'exampletocms') !== false) {
/*
* We must set module, controller path and action name + we will set page id 5 witch is about us page on
* default magento 2 installation with sample data.
*/
$request->setModuleName('cms')->setControllerName('page')->setActionName('view')->setParam('page_id', 5);
} else if(strpos($identifier, 'examplerouter') !== false) {
/*
* We must set module, controller path and action name for our controller class(Controller/Test/Test.php)
*/
$request->setModuleName('inchootest')->setControllerName('test')->setActionName('test');
} else {
//There is no match
return;
}
 
/*
* We have match and now we will forward action
*/
return $this->actionFactory->create(
'Magento\Framework\App\Action\Forward',
['request' => $request]
);
}
}

routes.xml (located in etc/frontend)

<?xml version="1.0"?>
 
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../lib/internal/Magento/Framework/App/etc/routes.xsd">
<router id="standard">
<route id="inchootest" frontName="inchootest">
<module name="Inchoo_CustomRouter" />
</route>
</router>
</config>

Test.php (test controller action class)

<?php
/**
* Copyright © 2015 Inchoo d.o.o.
* created by Zoran Salamun(zoran.salamun@inchoo.net)
*/
namespace Inchoo\CustomRouter\Controller\Test;
 
class Test extends \Magento\Framework\App\Action\Action
{
/**
* Listing all images in gallery
* -@param gallery id
*/
public function execute()
{
die("Inchoo\\CustomRouter\\Controller\\Test\\Test controller execute()");
}
}

You can see example module on:
https://github.com/zoransalamun/magento2-custom-router

Installation:

First add repository to composer configuration:

composer config repositories.inchoocustomrouter vcs git@github.com:zoransalamun/magento2-custom-router.git

Require new package with composer:

composer require inchoo/custom-router:dev-master

Enable Inchoo CustomRouter module

php bin/magento module:enable Inchoo_CustomRouter

Flush everything:

php bin/magento setup:upgrade

What are your opinions on routers and more specifically, experiences with routers in Magento 2?

Feel free to share them in the comment section below.

In case you feel you need some extra help, we can offer you a detailed custom report based on our technical audit – feel free to get in touch and see what we can do for you!

You made it all the way down here so you must have enjoyed this post! You may also like:

3 best open-source eCommerce platforms in 2021 Zrinka Antolovic
Zrinka Antolovic, | 8

3 best open-source eCommerce platforms in 2021

Introducing BrexitCart by Inchoo – a new cart abandonment solution Aron Stanic
Aron Stanic, | 1

Introducing BrexitCart by Inchoo – a new cart abandonment solution

Ready for 2nd Meet Magento Croatia with agenda focused on PWA? Book the dates! Maja Kardum
Maja Kardum, | 0

Ready for 2nd Meet Magento Croatia with agenda focused on PWA? Book the dates!

16 comments

  1. Hi guys, thanks for the tutorial.

    I was hitting the 100 limit no matter what, which was strange because I had done this in Magento 2.1 with no issues. This time I was on Magento 2.2.3, and one thing had changed, the router list order. In 2.1, the standard router has sortOrder = 20, in 2.2 (2.2.3 at least) it’s sortOrder = 30. This is a huge deal because it’s the standard router the one that will take the request.

    After changing the sortOrder in my di.xml to 31, it started working.

    Hope this helps.

  2. What if I get error :
    Exception #0 (LogicException): Front controller reached 100 router match iterations

    1. It’s because your router has matched but not dispatched…

  3. I have below error

    Front controller reached 100 router match iterations

    #0 [internal function]: Magento\Framework\App\FrontController->dispatch(Object(Magento\Framework\App\Request\Http))

    when I try to catch in foreach loop:

    if(strpos($identifier, $brandprefix) !== false) {

    foreach ($brandCollection as $key => $_manu) {
    if(strpos($identifier, $_manu->getData('url_key')) !== false )
    {
    /*
    * We must set module, controller path and action name for our controller class(Controller/Test/Test.php)
    */
    $request->setModuleName('catalogsearch')->setControllerName('advanced')->setActionName('index');
    }
    }
    }
    .

  4. Bit of confused about the purpose of different routers, specifically between Base and Cms router. Won’t Base router itself is enough to handle the request, what is the purpose of Cms router if base router could parse the request to fetch modulename/actionname/methodname?

    1. Hi Sajal, purpose for different routers is to handle different url formats, for example cms router is for cms pages and for SEO you don’t want url in format yoursite.com/cms/page/id/2/ (this is only example). Reason why cms router exist is to search in database URI string and if founds set front name/action path/action/ (in magento 2 you don’t have modulename/actioname/methodname), continue loop and in next loop base router will match your cms page in format front name/action path/action/. Hope this example will help you to understand routers.

  5. Hey, great tutorial.

    I have a question though…

    @ [Magento2 Root]/app/code///etc/frontend/routes.xml

    the route “id” [vendor_module_front] and the “frontName” [module_front]
    can be different from eachother.

    <router id="standard">
        <route id="vendor_module_front" frontName="module_front">
            <module name="Vendor_Module" />
        </route>
    </router>

    BUT

    @ [Magento2 Root]/app/code///etc/adminhtml/routes.xml

    the route “id” [vendor_module_admin] and the “frontName” [vendor_module_admin]
    can NOT be different from eachother. [they must match]

    <router id="admin">
        <route id="vendor_module_admin" frontName="vendor_module_admin">
            <module name="Vendor_Module" before="Magento_Backend" />
        </route>
    </router>

    I tried many combinations, can you please explain why?

    Thanks
    David

  6. Hello,

    Thanks for your great blog, but one thing notice when we forward to custom router, all messages are destroy,is there any other way?

    1. Hi Ashish, you can use what you want, but Magento 2 is working with Composer and i don’t have reason to use additional package managers. Also you need composer to install vendors before Magento 2 installation so you need to have Composer installed.

    2. There are different package managers for different languages. composer is the most abundant php package manager. npm is for node/js packages, and bower is the front end counter part of npm. So yes, if you’re writing php code you have to use composer, and cannot use npm for that purpose.

  7. Hi Zoran,

    Great post about Magento 2 routing. Especially I enjoyed going through available Routers. By the way, with respect to your custom Router example, it might be even some RouterComposite which will aggregate all your custom match rules.

    Thanks,
    Max

    1. Hi Max, thank you.

      Good proposal, i will make sure to update example module with similar router in future.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <blockquote cite=""> <code> <del datetime=""> <em> <s> <strike> <strong>. You may use following syntax for source code: <pre><code>$current = "Inchoo";</code></pre>.

Tell us about your project

Drop us a line. We'd love to know more about your project.