1.10

Custom Module Routes

Understanding Magento's routing system, standard routes, and creating custom routers for unique URL schemas.

Topic Overview: Understanding Magento's routing system, standard routes, and creating custom routers for unique URL schemas.

Routing System Overview

mindmap root((Routing)) Types Standard Routing URL Rewrites Custom Routers Standard Route routes.xml Three-chunk URL Controllers Custom Router RouterInterface Front Controller Custom URL schema Components Front Name Controller Path Action Name Registration di.xml Router list Priority

Magento Routing System

Two Parts of Routing

1. Standard Routing
  • Three-chunk URL structure
  • Format: frontname/controller/action
  • Configured in routes.xml
  • Example: catalog/product/view

Covered: This topic (1.10)

2. URL Rewrites
  • SEO-friendly URLs
  • Database-driven rewrites
  • Maps custom URLs to standard routes
  • Example: product-name.html

Covered: Topic 2.11

Standard Routing

Three-Chunk URL Structure

Magento uses a three-part URL structure for standard routing:

URL Format:
https://example.com/frontname/controller/action/param1/value1/param2/value2
  • frontname - Route identifier (e.g., catalog, customer, checkout)
  • controller - Controller directory name (e.g., product, account, cart)
  • action - Action class name (e.g., view, edit, add)
  • parameters - Optional key/value pairs
Examples:
URL Front Name Controller Action
catalog/product/view/id/123 catalog product view
customer/account/login customer account login
checkout/cart/add checkout cart add

Declaring Routes

routes.xml Configuration

To handle requests, a module must declare a route in routes.xml:

File Locations:
  • Frontend: etc/frontend/routes.xml
  • Admin: etc/adminhtml/routes.xml
Example: Frontend Route

File: app/code/Bonlineco/Catalog/etc/frontend/routes.xml

<?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="catalog" frontName="catalog">
            <module name="Bonlineco_Catalog"/>
        </route>
    </router>
</config>
Configuration Elements:
  • <router id="standard"> - Router type (standard for frontend/admin)
  • <route id="catalog"> - Unique route identifier
  • frontName="catalog" - URL front name (first chunk)
  • <module name="..."> - Module that handles this route
Example: Admin Route

File: app/code/Bonlineco/Catalog/etc/adminhtml/routes.xml

<?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="admin">
        <route id="bonlineco_catalog" frontName="bonlineco_catalog">
            <module name="Bonlineco_Catalog" before="Magento_Backend"/>
        </route>
    </router>
</config>

Controller Mapping

Controller Structure

After declaring a route, create controllers that correspond to the URL structure:

URL to File Mapping:
URL: catalog/product/view
     ↓
Route: catalog (from routes.xml)
     ↓
Module: Bonlineco_Catalog
     ↓
Controller File: app/code/Bonlineco/Catalog/Controller/Product/View.php
     ↓
Class: Bonlineco\Catalog\Controller\Product\View
Directory Structure:
app/code/Bonlineco/Catalog/
├── Controller/
│   ├── Product/
│   │   ├── View.php        ← catalog/product/view
│   │   ├── Edit.php        ← catalog/product/edit
│   │   └── Delete.php      ← catalog/product/delete
│   ├── Category/
│   │   └── View.php        ← catalog/category/view
│   └── Index/
│       └── Index.php       ← catalog/index/index (or just catalog/)
└── etc/
    └── frontend/
        └── routes.xml
Naming Conventions:
  • Controller directory = URL second chunk (capitalized)
  • Action class = URL third chunk (capitalized)
  • Default controller: Index
  • Default action: Index

Custom URL Schemas

Beyond Standard Routing

Sometimes you need custom URL patterns that don't fit the three-chunk structure:

Examples of Custom URLs:
  • product-123 instead of catalog/product/view/id/123
  • product-configuration-ML-12-size-XL-color-RED
  • blog/2024/10/my-post-title
  • brand/nike/category/shoes
Important: Custom URL schemas require creating a Custom Router.

Front Controller Workflow

How Magento Processes Requests

Request Processing Flow:
  1. Request hits index.php or pub/index.php
  2. Bootstrap initializes - Application setup and configuration
  3. Front Controller runs - \Magento\Framework\App\FrontController
  4. Obtains router list - All routers registered via di.xml
  5. Loops through routers - In priority order
  6. Router processes request - First matching router handles it
  7. Returns ActionInterface - Action is executed
  8. Response sent - HTML, JSON, redirect, etc.
Front Controller Class:
\Magento\Framework\App\FrontController
Key Concept: The Front Controller loops through registered routers until one returns an ActionInterface instance. That router "wins" and its action handles the request.

Default Routers

Built-in Router Types

Magento includes several default routers that handle different types of requests:

Router Purpose Priority
Base Router Handles admin and frontend standard routes 20
Default Router Fallback for unmatched routes (404) 100
CMS Router Handles CMS pages and blocks 60
UrlRewrite Router Processes URL rewrites from database 40
Priority Order: Lower numbers execute first. Custom routers typically use priority 30-50 to run between base and URL rewrite routers.

Creating a Custom Router

Custom Router Implementation

To create a custom router for handling non-standard URLs:

Step 1: Create Router Class

File: app/code/Bonlineco/CustomUrl/Controller/Router.php

<?php
namespace Bonlineco\CustomUrl\Controller;

use Magento\Framework\App\ActionFactory;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\App\RouterInterface;

class Router implements RouterInterface
{
    protected $actionFactory;
    
    public function __construct(
        ActionFactory $actionFactory
    ) {
        $this->actionFactory = $actionFactory;
    }
    
    /**
     * Match application action by request
     *
     * @param RequestInterface $request
     * @return \Magento\Framework\App\ActionInterface|null
     */
    public function match(RequestInterface $request)
    {
        $identifier = trim($request->getPathInfo(), '/');
        
        // Example: Handle URLs like "product-123"
        if (preg_match('/^product-(\d+)$/', $identifier, $matches)) {
            $productId = $matches[1];
            
            // Set request parameters
            $request->setModuleName('catalog')
                    ->setControllerName('product')
                    ->setActionName('view')
                    ->setParam('id', $productId);
            
            // Return action instance
            return $this->actionFactory->create(
                \Magento\Framework\App\Action\Forward::class
            );
        }
        
        // Return null if this router doesn't handle the URL
        return null;
    }
}
Key Points:
  • Implement RouterInterface
  • Define match(RequestInterface $request) method
  • Return ActionInterface instance if matched
  • Return null if not matched (next router tries)
  • Use ActionFactory to create action instances

Registering Custom Router

di.xml Configuration

Register your custom router with the Front Controller:

File Location:
  • Frontend: etc/frontend/di.xml
  • Admin: etc/adminhtml/di.xml
  • Both: etc/di.xml
Example Registration:

File: app/code/Bonlineco/CustomUrl/etc/frontend/di.xml

<?xml version="1.0"?>
<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="bonlineco_custom" xsi:type="array">
                    <item name="class" xsi:type="string">
                        Bonlineco\CustomUrl\Controller\Router
                    </item>
                    <item name="disable" xsi:type="boolean">false</item>
                    <item name="sortOrder" xsi:type="string">30</item>
                </item>
            </argument>
        </arguments>
    </type>
</config>
Configuration Options:
  • name="bonlineco_custom" - Unique router identifier
  • class - Your router class path
  • disable - Enable/disable the router
  • sortOrder - Priority (lower = earlier execution)

Advanced Router Examples

Real-World Custom Router

Example: CMS Router (Simplified)

Here's how Magento's CMS router works (simplified):

<?php
namespace Magento\Cms\Controller;

use Magento\Framework\App\RouterInterface;
use Magento\Framework\App\RequestInterface;

class Router implements RouterInterface
{
    protected $actionFactory;
    protected $pageFactory;
    
    public function __construct(
        \Magento\Framework\App\ActionFactory $actionFactory,
        \Magento\Cms\Model\PageFactory $pageFactory
    ) {
        $this->actionFactory = $actionFactory;
        $this->pageFactory = $pageFactory;
    }
    
    public function match(RequestInterface $request)
    {
        $identifier = trim($request->getPathInfo(), '/');
        
        // Try to load CMS page by URL key
        $page = $this->pageFactory->create();
        $pageId = $page->checkIdentifier($identifier, $request->getStoreId());
        
        if (!$pageId) {
            return null; // Not a CMS page, let another router try
        }
        
        // Set request to CMS page controller
        $request->setModuleName('cms')
                ->setControllerName('page')
                ->setActionName('view')
                ->setParam('page_id', $pageId);
        
        return $this->actionFactory->create(
            \Magento\Framework\App\Action\Forward::class
        );
    }
}
Example: Blog Date-Based URLs
<?php
namespace Bonlineco\Blog\Controller;

class Router implements \Magento\Framework\App\RouterInterface
{
    public function match(\Magento\Framework\App\RequestInterface $request)
    {
        $identifier = trim($request->getPathInfo(), '/');
        
        // Match pattern: blog/2024/10/post-title
        $pattern = '/^blog\/(\d{4})\/(\d{2})\/([a-z0-9-]+)$/';
        if (preg_match($pattern, $identifier, $matches)) {
            $year = $matches[1];
            $month = $matches[2];
            $slug = $matches[3];
            
            $request->setModuleName('blog')
                    ->setControllerName('post')
                    ->setActionName('view')
                    ->setParam('year', $year)
                    ->setParam('month', $month)
                    ->setParam('slug', $slug);
            
            return $this->actionFactory->create(
                \Magento\Framework\App\Action\Forward::class
            );
        }
        
        return null;
    }
}

Router Interface

RouterInterface Contract

All routers must implement \Magento\Framework\App\RouterInterface:

<?php
namespace Magento\Framework\App;

interface RouterInterface
{
    /**
     * Match application action by request
     *
     * @param RequestInterface $request
     * @return ActionInterface|null
     */
    public function match(RequestInterface $request);
}
Match Method Requirements:
  • Parameter: Receives RequestInterface $request
  • Returns: ActionInterface if matched, null otherwise
  • Side effects: Can modify request (setModuleName, setParam, etc.)
  • Performance: Should be fast to not slow down all requests

Action Types

Available Action Classes

When returning an action from your router, you can use:

Action Type Class Purpose
Forward Magento\Framework\App\Action\Forward Forward to another controller/action
Redirect Magento\Framework\App\Action\Redirect HTTP redirect to another URL
Custom Your own ActionInterface implementation Custom logic and response
Using Forward Action:
// Set the target controller
$request->setModuleName('catalog')
        ->setControllerName('product')
        ->setActionName('view')
        ->setParam('id', $productId);

// Return forward action
return $this->actionFactory->create(
    \Magento\Framework\App\Action\Forward::class
);

Best Practices

Do's
  • Use standard routing when possible
  • Keep router match() method fast
  • Return null if URL doesn't match
  • Set appropriate sortOrder priority
  • Document custom URL patterns
  • Test with various URL formats
  • Consider URL rewrite as alternative
  • Handle edge cases gracefully
Don'ts
  • Don't create custom routers unnecessarily
  • Don't perform heavy operations in match()
  • Don't forget to return null for non-matches
  • Don't use too high/low sortOrder
  • Don't duplicate existing router functionality
  • Don't forget to register in di.xml
  • Don't hardcode store/website logic

Common Use Cases

When to Use Custom Routers

Use Case Example URL Solution
Product SKU URLs product/SKU-123 Custom router matching SKU pattern
Date-based archives blog/2024/10/ Custom router with date parsing
Multi-level categories shop/electronics/phones/ Custom router with category tree lookup
Short URLs p/123, c/456 Custom router with prefix matching
Legacy URL compatibility Old site URL patterns Custom router for backwards compatibility

Debugging Routers

Troubleshooting Tips

Enable Developer Mode:
bin/magento deploy:mode:set developer
Check Router Registration:
bin/magento dev:di:info Magento\Framework\App\RouterList
Add Logging:
public function match(RequestInterface $request)
{
    $this->logger->debug('Custom Router: ' . $request->getPathInfo());
    
    // Your matching logic
    
    if ($matched) {
        $this->logger->debug('Custom Router: MATCHED');
        return $action;
    }
    
    $this->logger->debug('Custom Router: Not matched');
    return null;
}
Clear Cache:
bin/magento cache:clean config
bin/magento cache:clean full_page

Exam Tips

Key Points to Remember

  • Two routing types: Standard routing and URL Rewrites
  • Standard URL format: frontname/controller/action
  • Route configuration: etc/frontend/routes.xml (or adminhtml)
  • Front Controller: Loops through routers to find match
  • Custom router interface: RouterInterface with match() method
  • Match method: Returns ActionInterface or null
  • Registration: Register router in di.xml with sortOrder
  • Default routers: Base (20), UrlRewrite (40), CMS (60), Default (100)
  • Action types: Forward, Redirect, or custom ActionInterface
  • Controller path: Vendor/Module/Controller/Path/Action.php
  • Request modification: Use setModuleName(), setControllerName(), setActionName()
  • Priority matters: Lower sortOrder = earlier execution