- Introduction to Magento 2 — No More MVC
- Magento 2: Serving Frontend Files
- Magento 2: Adding Frontend Files to your Module
- Magento 2: Code Generation with Pestle
- Magento 2: Adding Frontend Assets via Layout XML
- Magento 2 and RequireJS
- Magento 2 and the Less CSS Preprocessor
- Magento 2: CRUD Models for Database Access
- Magento 2: Understanding Object Repositories
- Magento 2: Understanding Access Control List Rules
- Magento 2: Admin Menu Items
- Magento 2: Advanced Routing
- Magento 2: Admin MVC/MVVM Endpoints
Today we’re going to cover a few of the advanced features of Magento’s routing system, and discuss the history of some seemingly acute sharp edges. While all the techniques available in this article may not be the best way to achieve your goals, as a working Magento developer you’ll need to be aware they’re possible, and that other developers (including Magento’s core engineering team) may have used them.
This article assumes you have solid Magento 2 module experience, and it may gloss over details that are critical for a beginner. If that’s you, use the comments to ask your question or point to your Stack Exchange question.
Magento URL Action Strings
As a PHP programmer, when you need to create a URL in Magento, you use the getUrl
method of a block or URL model object.
Mage::getUrl('foo/baz/bar');
$urlModel->getUrl('foo/baz/bar');
$this->getUrl('foo/baz/bar');
This is true in Magento 1 and Magento 2. In Magento 2 you don’t have access to the global Mage::getUrl
method, so you’ll need to inject the specific URL model you want via automatic constructor dependency injection. URL models allow PHP programmers to treat URLs as a simple action string, and then have the core system code construct the final rendered URL. If you’ve only ever done front end work, this may seem like overkill, but it’s advantageous if the URL structure needs to change in the future or accommodate multiple contexts. Most web programming frameworks consider this a necessary feature.
We’re not going to dive too deeply into URL models today — what we are interested in is the action string these URL methods accept. At first glance, you may think the foo/baz/baz
structure maps directly to the frontName/controllerName/actionName
structure of a Magento URL. You’d almost be right. The actual structure is routeId
/controllerName/actionName
.
When you setup a routes.xml file for your module, you use an XML structure that looks like this
<!-- File: app/code/Package/Module/etc/[area]/routes.xml -->
<route id="routerId" frontName="urlFrontName">
<module name="Package_Module" />
</route>
That is, you setup a <route/>
node with an id
and frontName
attributes. The id
identifies the route node uniquely in the Magento system, and the frontName
defines the first segment of your URL.
When Magento generates a URL from an actions string like foo/baz/bar
, it uses the first segment to lookup a <route/>
node in the merged XML tree, and then use that route node’s frontName
as the first URL segment.
This isn’t an obvious thing, and you may be developing on Magento for years without realizing it. That’s because most modules use route IDs and front names that are identical.
For example, you can see this in the catalog
module
<!-- File: app/code/Magento/Catalog/etc/frontend/routes.xml -->
<route id="catalog" frontName="catalog">
<module name="Magento_Catalog" />
</route>
both the id
and frontName
attributes are catalog
. This convention also existed in Magento 1.
<!-- File: app/code/core/Mage/Catalog/etc/catalog.xml -->
<frontend>
<routers>
<catalog>
<use>standard</use>
<args>
<module>Mage_Catalog</module>
<frontName>catalog</frontName>
</args>
</catalog>
</routers>
<!-- ... -->
</frontend>
The above is Magento 1’s catalog router configuration. A single <catalog/>
node under <routers/>
configures a frontName
named catalog
. In Magento 2, that node under <routers/>
has been turned in <module/>
, with the id
attribute replacing the Magento 1 node name. (i.e. <catalog/>
becomes <module id="catalog"/>
.
This convention makes it easy to look at a URL action string and get a rough idea of what the final URL will look like. However, there’s one huge exception: Magento’s admin URLs.
Magento Admin URLs
In Magento 1, the main admin route was configured with the following (in the Magento core)
<!-- File: app/code/core/Mage/Adminhtml/etc/config.xml -->
<admin>
<routers>
<adminhtml>
<use>admin</use>
<args>
<module>Mage_Adminhtml</module>
<frontName>admin</frontName>
</args>
</adminhtml>
</routers>
</admin>
That is, the single node in <routers/>
named <adminhtml/>
sets up a frontName
named admin
. This carries over into Magento 2.
#File: vendor/magento/module-backend/etc/adminhtml/routes.xml
<route id="adminhtml" frontName="admin">
<module name="Magento_Backend" />
</route>
This means, in Magento 1, an action string like adminhtml/foo/bar
will translate into a full URL that looks something like http://example.magento.com/admin/foo/bar
. That is, the first URL segment, and front name, will be admin
instead of adminhtml
.
This is an odd exception to Magento’s general convention of matching front name and router id, and is likely less a deliberate design decision than it is Magento 1 launching with a half finished concept of areas (adminhtml
, frontend
) implemented.
Regardless, you can still find evidence of this non-decision in Magento 2’s codebase.
Magento 2 Admin URLs
In Magento 1, the adminhtml
router ID is part of what ensured admin URLs began with the string /admin
, and that the Magento area be set to adminhtml
.
However, in Magento 2, every route setup in etc/adminhtml/routes.xml
files will automatically be prepended with the string admin
. This is what lets us use our own frontName
in these adminhtml/routes.xml
files.
This ends up having a curious side effect on legacy admin URLs that still use the adminhtml
router ID. Consider the URL rewrite module
<!-- File: vendor/magento/module-url-rewrite/etc/adminhtml/menu.xml -->
<add id="Magento_UrlRewrite::urlrewrite"
title="URL Rewrites" module="Magento_UrlRewrite"
sortOrder="20"
parent="Magento_Backend::marketing_seo"
action="adminhtml/url_rewrite/index"
resource="Magento_UrlRewrite::urlrewrite"
/>
The above menu.xml file uses an action string of adminhtml/url_rewrite/index
. Magento ends up generating a URL like this
http://magento.example.com/admin/admin/url_rewrite/index
That’s a URL that begins with /admin/admin
. That’s two admin strings. The first comes from the /admin
URL segment that Magento prepends to every admin URL. The second comes from Magento using the adminhtml
route ID to lookup a frontName
attribute.
These appear to be legacy URLs automatically converted into menu items, although when I asked Magento’s architects about this back in January they were initially confused by the question, and then stated both admin URL formats (adminhtml
route IDs/front names and custom route IDs/front names) were valid, but that custom route ID and front names were preferred.
Given the lack of clear initial rules, and the lack of the core team’s adherence to these later decreed rules, working Magento developers will want to be familiar with both URL formats, and be ready to debug them as needed.
URL Route Sharing
If you have not dug too deeply into Magento’s core code, you may be wondering
Wait — I thought each Magento module claimed a specific front name — how can multiple modules claim the
admin
front name via theadminhtml
router?
This brings us to another feature from Magento 1 that made the jump to Magento 2: Route sharing.
Magento’s “1 module, 1 front name” policy traces its roots back to the Zend Framework’s early routing/MVC system. Magento 1, while a framework all its own, based a lot of its work on core Zend Framework classes, and this “1 module, 1 front name” feature came along for the ride. Module developers quickly noticed how limiting this was, and Magento introduced the ability for multiple modules to claim a particular front name.
If that didn’t make sense
- When Magento 1 was created, you could only create Controller files for a URL starting with
/foo/...
in a single module - Magento introduced the ability for multiple modules to have controller files for URLs starting with
/foo/
in Magento 1.3
The ability persists in Magento 2, and syntax for it has been greatly simplified. For example, if you wanted to go back to the first module in this series (Pulsestorm_HelloWorldMVVM
) and add a front end URL endpoint at catalog/foo/bar
, all you’d need to do is add the following configuration
<!-- File: app/code/Pulsestorm/HelloWorldMVVM/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="hello_mvvm" frontName="hello_mvvm">
<module name="Pulsestorm_HelloWorldMVVM"/>
</route>
<!-- START: new configuration -->
<route id="catalog">
<module name="Pulsestorm_HelloWorldMVVM" after="Magento_Catalog"/>
</route>
<!-- END: new configuration -->
</router>
</config>
and the following controller file.
#File: app/code/Pulsestorm/HelloWorldMVVM/Controller/Foo/Bar.php
<?php namespace Pulsestorm\HelloWorldMVVM\Controller\Foo;
class Bar extends \Magento\Framework\App\Action\Action
{
public function execute()
{
var_dump("Proof of life");
}
}
with the above in place, you now have a second module with controller files for the catalog
front name, and Magento will use your controller when you load the catalog/foo/bar
URI.
Unlike Magento 1, the configuration for this is almost identical to setting up routing for a single module
<!-- File: app/code/Pulsestorm/HelloWorldMVVM/etc/frontend/routes.xml-->
<route id="catalog">
<module name="Pulsestorm_HelloWorldMVVM" after="Magento_Catalog"/>
</route>
The two main differences are
- You do not configure a
frontName
attribute in the<route/>
tag -
You need (or are strongly advised) to use a
before
orafter
tag to control the order Magento will check for matches in
When you use the above configuration, you’re telling Magento
Hey, you know that
<route/>
tag with anid
ofcatalog
? I want you to merge in an extra<module/>
node.
When Magento encounters multiple <module/>
nodes in its global configuration, it will check each module for a controller match until it finds one. Using the after
tag above ensures Magento checks our module after the Magento_Catalog
module. Without this, it would be possible for us to accidentally create a controller file that replaced the core controllers in vendor/magento/module-catalog/Controller
.
It’s important to note that you’re looking for the <route/>
id
attribute, and not the frontName
attribute here. For example, when a module wants to add to the admin
front name, a Magento core developer will
- Identify the module that first added the
frontName="admin"
- Identify that
<module/>
‘s<route/>
id - Use that ID in their own modules
- Ensure a proper
before
orafter
tag is in place
So, step one — the module that initially added the admin
front name is Magento_Backend
#File: vendor/magento/module-backend/etc/adminhtml/routes.xml
<route id="adminhtml" frontName="admin">
<module name="Magento_Backend" />
</route>
This <route/>
node’s id
is adminhtml
. So, the other Magento modules that use the admin
front name configure themselves with id="adminhtml"
.
<!-- File: vendor/magento/module-variable/etc/adminhtml/routes.xml -->
<route id="adminhtml">
<module name="Magento_Variable" before="Magento_Backend" />
</route>
<!-- File: vendor/magento/module-widget/etc/adminhtml/routes.xml -->
<route id="adminhtml">
<module name="Magento_Widget" before="Magento_Backend" />
</route>
Default Action String Segments
Before we move on from advanced routing, there’s a few last things to mention. You may occasionally see an action string that’s missing its second or third segment.
<!-- File: vendor/magento/module-tax/etc/adminhtml/menu.xml -->
<add
id="Magento_Tax::sales_tax_rules"
title="Tax Rules"
module="Magento_Tax"
sortOrder="10"
parent="Magento_Tax::sales_tax"
action="tax/rule"
resource="Magento_Tax::manage_tax"/>
When Magento encounters an action string with a missing segment, it will substitute the string index. In other words, the action string of tax/rule
used above is functionally equivalent to an action string of tax/rule/index
.
Also, in PHP code, you may occasionally see the *
character in a URL action string.
$this->getUrl('*/*/*')
These asterisks will be translated as the current front name, controller name, or action name. In other words, they create context dependent URLs, and are useful in base UI classes meant to be used in multiple modules.
Wrap Up
Like a lot of the “in the trenches” decisions made by Magento 2’s non-architects (i.e. the programmers actually implementing the features), it’s not 100% clear why these admin/admin
URLs stuck around. As third party developers, it’s probably best for us to create our own URL front names, and only rely on the admin
front name if there’s some feature that makes it absolutely necessary.