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('MagentoFrameworkAppFrontControllerInterface');

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 MagentoFrameworkApp, 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 MagentoFrameworkAppInterface
{
public function launch()
{
...
//Here Application is calling front controller and it's dispatch method
$frontController = $this->_objectManager->get('MagentoFrameworkAppFrontControllerInterface');
$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)
{
MagentoFrameworkProfiler::start('routers_match');
$routingCycleCounter = 0;
$result = null;
while (!$request->isDispatched() && $routingCycleCounter++ < 100) {
/** @var MagentoFrameworkAppRouterInterface $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 (MagentoFrameworkExceptionNotFoundException $e) {
$request->initForward();
$request->setActionName('noroute');
$request->setDispatched(false);
break;
}
}
}
MagentoFrameworkProfiler::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="MagentoFrameworkAppRouterList">
<arguments>
<argument name="routerList" xsi_type="array">
<item name="inchoocustomrouter" xsi_type="array">
<item name="class" xsi_type="string">InchooCustomRouterControllerRouter</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 InchooCustomRouterController;
 
/**
* Inchoo Custom router Controller Router
*
* @author Zoran Salamun <zoran.salamun@inchoo.net>
*/
class Router implements MagentoFrameworkAppRouterInterface
{
/**
* @var MagentoFrameworkAppActionFactory
*/
protected $actionFactory;
 
/**
* Response
*
* @var MagentoFrameworkAppResponseInterface
*/
protected $_response;
 
/**
* @param MagentoFrameworkAppActionFactory $actionFactory
* @param MagentoFrameworkAppResponseInterface $response
*/
public function __construct(
MagentoFrameworkAppActionFactory $actionFactory,
MagentoFrameworkAppResponseInterface $response
) {
$this->actionFactory = $actionFactory;
$this->_response = $response;
}
 
/**
* Validate and Match
*
* @param MagentoFrameworkAppRequestInterface $request
* @return bool
*/
public function match(MagentoFrameworkAppRequestInterface $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(
'MagentoFrameworkAppActionForward',
['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 InchooCustomRouterControllerTest;
 
class Test extends MagentoFrameworkAppActionAction
{
/**
* 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!