Categories


Archives


Recent Posts


Categories


Magento 2: Advanced Routing

astorm

Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

No Frills Magento Layout is the only Magento front end book you'll ever need. Get your copy today!

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 the adminhtml 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

  1. When Magento 1 was created, you could only create Controller files for a URL starting with /foo/... in a single module

  2. 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

  1. You do not configure a frontName attribute in the <route/> tag
  2. You need (or are strongly advised) to use a before or after 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 an id of catalog? 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

  1. Identify the module that first added the frontName="admin"
  2. Identify that <module/>‘s <route/> id
  3. Use that ID in their own modules
  4. Ensure a proper before or after 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.

Originally published May 23, 2016
Series Navigation<< Magento 2: Admin Menu ItemsMagento 2: Admin MVC/MVVM Endpoints >>

Copyright © Alan Storm 1975 – 2017 All Rights Reserved

Originally Posted: 23rd May 2016