How to add new route in Magento 2.4 the correct way

In this article lets explore how to add a new route to Magento 2. Before we dive in we need to understand how the routes are built. This will allow you to easily read the code and directory structure and understand what needs to be created in order to create a specific url.

Lets assume we want to build a route:

tfc_route_id/index/index

In Magento 2 there are 3 parts that define the structure of the url.

  1. frontName - tfc_route_id
  2. controller name - index
  3. action name - index

Magento uses a principle one action for one controller. This is a large contrast in comparison to Magento 1 where you could define infinite amount of actions in the same controller file. This means that we will need to define a single action file. The location of the file should be Techflarestudio/Routes/Controller/Index/Index.php.

To make sure that Magento knows about our new controller we should define the frontname.

Define route and set a frontName

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
    <router id="standard">
        <route id="tfc_route_id" frontName="tfc_route_id">
            <module name="Techflarestudio_Routes"/>
        </route>
    </router>
</config>
Techflarestudio/Routes/etc/frontend/routes.xml

In our example we are defining the route in frontend area by placing the routes.xml file in etc/frontend. There are 2 properties id and frontName.

  • ID and frontName do not have to be equal. It is general practice in most native modules but this is not a requirement.
  • ID should simply be a unique value that can be used to identify the router
  • frontName will be used to register the first part of the url. In our example we will set it to be tfc_route_id

Define the controller and action

In order to define our controller and action we simply need to create our file structure. Since we want our url to be tfc_route_id/index/index we need to create a file in this path: Controller/Index/Index.php

All controllers that are defined in the module will use the frontName that is defined in the modules routes.xml file. To understand this better here is another example. To create a url tfct_route_id/awesome/tip we would need to create a file in location Controller/Awesome/Tip.php. For a Magento 1 developer this may seem too easy because it is way easier than what it was to declare and map routes in Magento 1.

Define the action class

Up until Magento 2.3 you would define your class the following way.

<?php

namespace Techflarestudio\Routes\Controller\Index;

/**
 * Class Index
 * @package Techflarestudio\Routes\Controller\Index
 */
class Index extends \Magento\Framework\App\Action\Action
{

    protected $resultPageFactory;

    public function __construct(
        \Magento\Framework\App\Action\Context $context,
        \Magento\Framework\View\Result\PageFactory $resultPageFactory)
    {
        $this->resultPageFactory = $resultPageFactory;
        parent::__construct($context);
    }

    public function execute()
    {
        return $this->resultPageFactory->create();
    }
}
Techflarestudio/Routes/Controller/Index/Index.php

In Magento 2.4 \Magento\Framework\App\Action\Action class has been deprecated with the following goal in mind:

Inheritance in controllers should be avoided in favor of composition

This is a movement that was started years ago but due to its backward incompatible nature was introduced only in Magento 2.4. The reasoning for this change is to avoid extending abstract classes in controllers. It makes sense because each type of action (GET, POST, PUT) is more of a part of the action object than an extension of it. Inheritence vs composition is rather a preference for programming techniques developers use to establish relationships. Magento is moving towards composition where possible.

You can see the post from one of the core contributors.

First implication of this is the fact that core documentation is not up to date. You should not continue using Action class but instead should implement the interface. Here is the updated version:

<?php

namespace Techflarestudio\Routes\Controller\Index;

use Magento\Framework\View\Result\PageFactory;
use Magento\Framework\App\ActionInterface;

/**
 * Class Index
 * @package Techflarestudio\Routes\Controller\Index
 */
class Index implements ActionInterface
{
    protected $resultPageFactory;

    public function __construct(
        PageFactory $resultPageFactory
    ) {
        $this->resultPageFactory = $resultPageFactory;
    }

    public function execute()
    {
        return $this->resultPageFactory->create();
    }
}
Techflarestudio/Routes/Controller/Index/Index.php

This will render an empty page where you are free to do whatever magic you can imagine.

In addition you can implement specific types of action interfaces to define what type of requests are allowed for you action. You can implement as many interfaces as you need. For GET and POST actions you can use the following.

  • \Magento\Framework\App\Action\HttpGetActionInterface
  • \Magento\Framework\App\Action\HttpPostActionInterface

In addition keep in mind that POST actions by default have form key validation, this is to ensure that actions are secure. However in order to skip form key validation you can implement CsrfAwareActionInterface which will allow you to define your own validation for the request. This can let you skip the form key validation for any action.

Here is the code that is responsible:

if ($action instanceof CsrfAwareActionInterface) {
    $valid = $action->validateForCsrf($request);
}
\Magento\Framework\App\Request\CsrfValidator::validateRequest

Afterwards implement the method in your action class like this:

public function validateForCsrf(RequestInterface $request): ?bool
{
    // security first?
    return true;
}

Hopefully this helps understand how the routes are built, how to use the new action interfaces and how form key validation can be bypassed.