Custom Module Routes
Understanding Magento's routing system, standard routes, and creating custom routers for unique URL schemas.
Routing System Overview
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 identifierfrontName="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-123instead ofcatalog/product/view/id/123product-configuration-ML-12-size-XL-color-REDblog/2024/10/my-post-titlebrand/nike/category/shoes
Front Controller Workflow
How Magento Processes Requests
Request Processing Flow:
- Request hits
index.phporpub/index.php - Bootstrap initializes - Application setup and configuration
- Front Controller runs -
\Magento\Framework\App\FrontController - Obtains router list - All routers registered via
di.xml - Loops through routers - In priority order
- Router processes request - First matching router handles it
- Returns ActionInterface - Action is executed
- Response sent - HTML, JSON, redirect, etc.
Front Controller Class:
\Magento\Framework\App\FrontController
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 |
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
ActionInterfaceinstance if matched - Return
nullif not matched (next router tries) - Use
ActionFactoryto 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 identifierclass- Your router class pathdisable- Enable/disable the routersortOrder- 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:
ActionInterfaceif matched,nullotherwise - 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:
RouterInterfacewithmatch()method - Match method: Returns
ActionInterfaceornull - 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