Add a Dynamic Route in the Magento 2 Admin

Many Magento 2 extensions, like blog modules, allow merchants to configure the URL of the page in the admin within the Stores -> Configuration area. Developers can add this functionality by adding a custom Magento 2 Router class allowing the merchant to add a dynamic route in the Magento 2 admin.

Below will describe the steps necessary in order to allow for a dynamic route to be configured in the Stores -> Configuration section.

Firstly, add the custom module’s registration.php and module.xml files.

<?php
\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    '[Vendor]_[Module]',
    __DIR__
);

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="[Vendor]_[Module]" setup_version="1.0.0" />
</config>

Enable the module and upgrade the Magento 2 database by running the following commands from the Magento 2 root directory.

$ /path/to/php bin/magento module:enable [Vendor]_[Module]
$ /path/to/php bin/magento setup:upgrade

Start with adding the system configuration-related files. Define the module’s system.xml file that will allow the merchant to configure a custom URL from within the admin.

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
    <system>
        <tab id="[vendor]" translate="label" sortOrder="850">
            <label>[Vendor] [Module]</label>
        </tab>
        <section id="[module]" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
            <label>Custom Route</label>
            <tab>[vendor]</tab>
            <resource>[Vendor]_[Module]::[module]</resource>
            <group id="general" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
                <label>General</label>
                <field id="route" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1">
                    <label>Route</label>
                </field>
            </group>
        </section>
    </system>
</config>

Now define the ACL configuration for the section added within the system.xml file.

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/acl.xsd">
    <acl>
        <resources>
            <resource id="Magento_Backend::admin">
                <resource id="Magento_Backend::stores">
                    <resource id="Magento_Backend::stores_settings">
                        <resource id="Magento_Config::config">
                            <resource id="[Vendor]_[Module]::[module]" title="[Vendor] [Module]" translate="title" />
                        </resource>
                    </resource>
                </resource>
            </resource>
        </resources>
    </acl>
</config>

Generally, a ‘default’ custom route is defined which is combined with a controller class to handle a route if the merchant doesn’t define one in the admin.

For example, a blog module may use a blog default route defined in the config.xml along with a blog route defined in routes.xml.

In this example, define a default route used for the system configuration in the module’s config.xml file.

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
    <default>
        <[module]>
            <general>
                <route>customroute</route>
            </general>
        </[module]>
    </default>
</config>

In addition, add a routes.xml file defining the default route.

<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
    <router id="standard">
        <route id="customroute" frontName="customroute">
            <module name="[Vendor]_[Module]"/>
        </route>
    </router>
</config>

Add a Controller class used to handle the route.

<?php
namespace [Vendor]\[Module]\Controller\Index;

use Magento\Framework\App\Action\Context;
use Magento\Framework\View\Result\PageFactory;

class Index extends \Magento\Framework\App\Action\Action
{
    /**
     * @var PageFactory
     */
    protected $resultPageFactory;

    /**
     * Index constructor.
     * @param Context $context
     * @param PageFactory $resultPageFactory
     */
    public function __construct(
        Context $context,
        PageFactory $resultPageFactory
    ) {
        $this->resultPageFactory = $resultPageFactory;
        parent::__construct($context);
    }

    /**
     * @return \Magento\Framework\App\ResponseInterface|\Magento\Framework\Controller\ResultInterface|\Magento\Framework\View\Result\Page
     */
    public function execute()
    {
        $page = $this->resultPageFactory->create();
        $page->getConfig()->getTitle()->set('Custom Route');
        return $page;
    }
}

Now we have the system configuration defined, along with a default value. So if the custom route isn’t changed from the default customroute route, the Controller class will still be able to handle the route.

Add a Dynamic Route in the Magento 2 Admin

Next will be to add the Router class. First of all, the Router should be defined within the module’s di.xml file within the routerList argument.

<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Framework\App\RouterList">
        <arguments>
            <argument name="routerList" xsi:type="array">
                <item name="customroute" xsi:type="array">
                    <item name="class" xsi:type="string">[Vendor]\[Module]\Controller\Router</item>
                    <item name="disable" xsi:type="boolean">false</item>
                    <item name="sortOrder" xsi:type="string">60</item>
                </item>
            </argument>
        </arguments>
    </type>
</config>

Add the Router class, that should implement the interface, and subsequently contain a match() method.

In summary, the module path of the route is checked again the route added in Stores -> Configuration in the admin, and if it matches, set the module, controller and action names of the request to customroute, index and index respectively. This will allow the controller class defined above to handle the route.

<?php
namespace [Vendor]\[Module]\Controller;

use Magento\Framework\App\ActionFactory;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\App\RouterInterface;
use Magento\Framework\DataObject;
use Magento\Framework\Event\ManagerInterface as EventManagerInterface;
use [Vendor]\[Module]\Helper\Data as CustomRouteHelper;

class Router implements RouterInterface
{
    /**
     * @var bool
     */
    private $dispatched = false;

    /**
     * @var ActionFactory
     */
    protected $actionFactory;

    /**
     * @var EventManagerInterface
     */
    protected $eventManager;

    /**
     * @var CustomRouteHelper
     */
    protected $helper;

    /**
     * Router constructor.
     *
     * @param ActionFactory $actionFactory
     * @param EventManagerInterface $eventManager
     * @param CustomRouteHelper $helper
     */
    public function __construct(
        ActionFactory $actionFactory,
        EventManagerInterface $eventManager,
        CustomRouteHelper $helper
    ) {
        $this->actionFactory = $actionFactory;
        $this->eventManager = $eventManager;
        $this->helper = $helper;
    }

    /**
     * @param RequestInterface $request
     * @return \Magento\Framework\App\ActionInterface|null
     */
    public function match(RequestInterface $request)
    {
        /** @var \Magento\Framework\App\Request\Http $request */
        if (!$this->dispatched) {
            $identifier = trim($request->getPathInfo(), '/');
            $this->eventManager->dispatch('core_controller_router_match_before', [
                'router' => $this,
                'condition' => new DataObject(['identifier' => $identifier, 'continue' => true])
            ]);
            
            $route = $this->helper->getModuleRoute();
            
            if ($route !== '' && $identifier === $route) {
                $request->setModuleName('customroute')
                    ->setControllerName('index')
                    ->setActionName('index');
                $request->setAlias(\Magento\Framework\Url::REWRITE_REQUEST_PATH_ALIAS, $identifier);
                $this->dispatched = true;

                return $this->actionFactory->create(
                    'Magento\Framework\App\Action\Forward'
                );
            }
            
            return null;
        }
    }
}

If you haven’t noticed from the above code, a Helper class is defined. This simply contains a method that will return the custom route defined in the Stores -> Configuration area.

<?php
namespace [Vendor]\[Module]\Helper;

use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\Helper\AbstractHelper;
use Magento\Framework\App\Helper\Context;
use Magento\Store\Model\ScopeInterface;

class Data extends AbstractHelper
{
    const XML_PATH_CUSTOMROUTE_ROUTE  = '[module]/general/route';

    /**
     * @var ScopeConfigInterface
     */
    protected $scopeConfig;

    /**
     * Data constructor.
     *
     * @param Context $context
     * @param ScopeConfigInterface $scopeConfig
     */
    public function __construct(
        Context $context,
        ScopeConfigInterface $scopeConfig
    )
    {
        parent::__construct($context);
        $this->scopeConfig = $scopeConfig;

    }

    /**
     * @return string
     */
    public function getModuleRoute()
    {
        return $this->scopeConfig->getValue(self::XML_PATH_CUSTOMROUTE_ROUTE, ScopeInterface::SCOPE_STORE);
    }
}

Lastly, a customroute_index_index.xml layout file will be defined that will render a template with some content.

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceContainer name="content">
            <block class="Magento\Framework\View\Element\Template" name="customroute.default.template"
                   template="[Vendor]_[Module]::customroute/default.phtml" />
        </referenceContainer>
    </body>
</page>
<div class="content">
    <p><?php echo __('Here\'s some content showing regardless of the route configured.'); ?></p>
</div>

And that’s it! Now whatever route you define in the admin, the Controller class will handle it and render the content from the template defined in the layout file.

Add a Dynamic Route in the Magento 2 Admin

Add a Dynamic Route in the Magento 2 Admin

Note: This article is based on Magento Open Source version 2.2.