Categories


Archives


Recent Posts


Categories


In Depth Magento Dispatch: Standard Router

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!

Last time we talked about what goes on with router objects in Magento’s front controller object. Now it’s time to dig into the router objects themselves.

Notice: While the specifics of this article refer to the 1.6 branch of Magento Community Edition, the general concepts apply to all versions.

The Standard Router

As a reminder, the four stock router objects are

Mage_Core_Controller_Varien_Router_Admin
Mage_Core_Controller_Varien_Router_Standard
Mage_Cms_Controller_Router
Mage_Core_Controller_Varien_Router_Default

Rather than take each router type in order, we’re going to start with the Standard router, as it forms the basis for what most people would consider Magento’s normal MVC URL handling. You can find this file at

File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php

The Standard router is the object used to handle Magento’s main shopping cart application, sometimes known as the frontend Magento area. However, before we begin, we should note that the Admin router has this Standard router class as an ancestor, and the Standard router itself is littered with special case exceptions for Magento’s Admin router. We’ll make a note of these as we go on, but for now try to stay focused on the routing logic itself. We’ll cover the Admin router in full next time.

We’ll be spending most of the tutorial in the Standard router object’s match method. The goal of this method is to examine a request URL, determine which Magento modules might contain an appropriate controller, determine which controller in that module we should use, determine which action on that controller we should call, and then tell the controller to dispatch that action. Calling the controller action will eventually achieve our previously stated goals to

  1. Provide a match method which examines the request object and returns true if the router wishes to “claim” a request
  2. Mark the request object as dispatched, or through inaction fail to mark it as dispatched
  3. Set the body of the request object

If a suitable module/controller/action is not found, the method returns false and the front controller object moves on to the next router’s match method.

Enough of theoretical interfaces, let’s get started! We’ll be going line by line through the match method. It’s pretty dense in there, but remember it’s all just code, and anyone can break down what code is doing.

Here’s the start of our match method

#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
public function match(Zend_Controller_Request_Http $request)
{
    //checking before even try to find out that current module
    //should use this router
    if (!$this->_beforeModuleMatch()) {
        return false;
    }
    ...
}

The call to _beforeModuleMatch is one of those special Admin router conditionals we were talking about, and you can ignore it for now. Next up are the following two lines

#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
$this->fetchDefault();

$front = $this->getFront();

Tackling them in reverse order, $front = $this->getFront(); fetches a reference to the single Magento front controller object. The match method needs this reference because, at certain points and under certain conditions, it’s going to ask the front controller object what the default action controller or action controller action are, as well as ask for something called the module string. In a default system these are are index, index, and core, respectively.

So what about $this->fetchDefault();? The fetchDefault method gets a reference to the front controller object and – sets the default module, action controller and controller action names.

#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
public function fetchDefault()
{
    $this->getFront()->setDefault(array(
        'module' => 'core',
        'controller' => 'index',
        'action' => 'index'
    ));
}

If you didn’t follow that: the match method will eventually ask the front controller object for some default values, allowing the front controller object to control this information. However, earlier, the match method sets these very defaults on the front controller object. I can only assume this is all a bit of legacy, and remind everyone of the Good Code flowchart. If you’re playing the Magento drinking game, take a sip.

Follow the Path

Next up, the match method asks the request object for the path information, which is then split into its component parts, which in turn are placed in the $p variable.

#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
$path = trim($request->getPathInfo(), '/');

if ($path) {
    $p = explode('/', $path);
} else {
    $p = explode('/', $this->_getDefaultPath());
}

Now we’re getting to down to it. The path information is going to be a normalized URL path, meaning the pathInfo for all the following URLs

http://magento1point6rc2.dev/index.php/catalog/category/view/id/8
http://magento1point6rc2.dev/catalog/category/view/id/8
http://magento1point6rc2.dev/electronics/cell-phones.html

//if magento's installed in a sub directory
http://magento1point6rc2.dev/magento/catalog/category/view/id/8
http://magento1point6rc2.dev/magento/index.php/catalog/category/view/id/8

will be normalized by the request object as the string.

/catalog/category/view/id/8

and then this string is split into its competent parts

$p = array (
0 => 'catalog',
1 => 'category',
2 => 'view',
3 => 'id',
4 => '8',);

Notice we never directly accesed $_GET, $_POST, or $_SERVER to fetch this information. Magento abstracts these responsibilities to the request object, allowing us to concentrate on our routing task instead of deciding the best way to normalize a path.

There’s also the trailing bit of code, $p = explode('/', $this->_getDefaultPath()); to consider. If $pathInfo is an empty string (generated by a url like http://magento.example.com/) we need some sort of default path information. Confusingly, this is not the same as the defaults previously discussed from the front controller object. Instead, Magento looks to the system configuration

#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
protected function _getDefaultPath()
{
    return Mage::getStoreConfig('web/default/front');
}

The config path web/default/front corresponds to the setting at

System -> Configuration -> Web -> Default Pages -> Default Web URL

In a stock Magento system with default factory settings, this will result in a $p that looks like

array
    0 => string 'cms' (length=3)

At the risk of getting ahead of ourselves, this is how Magento knows to use the CMS Index Controller for the default homepage. This is another case where, arguably, the routing system has failed to keep its concerns separate. Your natural inclination would be to assume the Cms router object would handle the CMS home page. Take a sip.

Finding the Modules

Alright, path information from our URL in hand, we’re ready to start our search for which modules might contain our controller. Step one is to get a string representing the module name from the path information. That’s handled by this bit of code

#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
if ($request->getModuleName()) {
    $module = $request->getModuleName();
} else {
    if (!empty($p[0])) {
        $module = $p[0];
    } else {
        $module = $this->getFront()->getDefault('module');
        $request->setAlias(Mage_Core_Model_Url_Rewrite::REWRITE_REQUEST_PATH_ALIAS, '');
    }
}
if (!$module) {
    if (Mage::app()->getStore()->isAdmin()) {
        $module = 'admin';
    } else {
        return false;
    }
}

Breaking that code block down, first Magento asks the request object for a module name.

#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
if ($request->getModuleName()) {
    $module = $request->getModuleName();
}

This is the request object that was passed into our router object by the front controller object, and the same object we asked to normalize our URL, presumably so we can inspect it. So why are we asking the request object for the module name? Didn’t we already agree to inspect the path information ourselves to and extract a module name? This seems like a redundant effort.

With a stock system during a normal request, the request object is not going to have a module name set. However, it’s possible that events attached to the Magento system may manipulate the request object and set a module name before the first router object has its match method called. Also, if you remember our previous article, then you know the Default router jiggers the request object to produce 404 pages and then kicks off a second run through the match methods. That jiggering includes setting a module name. Later articles will explore this further, for now just put it out of mind. This is another example of multiple, conflicting metaphors at work in the Magento core. Take a sip.

For now we’ll stay focused on the normal case, which is handled by the other leaf of the conditional

#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
if (!empty($p[0])) {
    $module = $p[0];
} else {
    $module = $this->getFront()->getDefault('module');
    $request->setAlias(Mage_Core_Model_Url_Rewrite::REWRITE_REQUEST_PATH_ALIAS, '');
}

Here, magento looks at the first section of our path information ($p[0]) to pull out a module name. So in the case of

/catalog/category/view/id/8

that would be the string

catalog

If this isn’t set, Magento will use one of those previously mentioned front controller object defaults

$module = $this->getFront()->getDefault('module');

In a stock system, the default module string from the front controller object is core. This is another one of those code paths you don’t normally have to worry about. It will only be triggered if the path information is empty and the system isn’t configured with a value at

System -> Configuration -> Web -> Default Pages -> Default Web URL

Of course, it’s possible that the $module variable is still empty. That’s what one final if statement is for

#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
if (!$module) {
    if (Mage::app()->getStore()->isAdmin()) {
        $module = 'admin';
    } else {
        return false;
    }
}

We’ve stumbled upon another one of those hard coded Admin router object special cases. Let’s ignore that for now, and note that for a standard/frontend URL request, if $module isn’t set at this point the method will return false;, indicating the Standard router has decided that this URL is better handled by the next router object.

Give me a Name, I’ll give you a Module

Alight, we now have a module name. The next step is to link up this short, URL, SEO friendly string (catalog) with a list of potential Magento module names (Mage_Catalog).

This is all done with the following line

$modules = $this->getModuleByFrontName($module);

In a simpler system, you might expect the getModuleByFrontName method to look something like

return 'Mage_' . ucfirst($module);

but you know that’s not how Magento rolls. If we take a look at the definition for getModuleByFrontName, you’ll see the following

#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
public function getModuleByFrontName($frontName)
{
    if (isset($this->_modules[$frontName])) {
        return $this->_modules[$frontName];
    }
    return false;
}

Well, its a simple method, but now we’re left wondering what populated $this->_modules. If we search the current class file for the string _modules, we find the addModule method that looks promising

#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
public function addModule($frontName, $moduleName, $routeName)
{
    $this->_modules[$frontName] = $moduleName;
    $this->_routes[$routeName] = $frontName;
    return $this;
}

Of course, now we don’t know what called the addModule method. If we search our current class file again, this time for the string addModule, we find this

#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
$this->addModule($frontName, $modules, $routerName); 

inside the larger collectRoutes method. Ah ha! Faithful readers will remember that the front controller object called this method on the Standard and Admin routers immediately after they were instantiated.

At this point we don’t know that this is where the _modules property was populated, but to save us some time and brain explodo, I’ll break the fourth wall and let you know this is the method we want.

Sometimes Magento requires you do a little digging and take an intelligent guess as to where data is coming from. The only way you’ll find out you’re wrong is to try.

Collecting the Routes

The collectRoutes method is a beast of a thing, and is a perfect example of what people gripe about when it comes to parsing XML in PHP. We’re not going to break this one apart line by line, but instead we’ll just describe, in plain english, what’s going on. Then we’ll follow up with an example, as some plain english is plainer than others.

The collectRoutes method will go through the combined config.xml tree and look for <router /> nodes. Where it looks for router nodes depends on the $configArea parameter that’s passed in when the router object is added to the front controller object.

With a stock system, this is either the string frontend, or the string admin. This string is used to create the node path that we’ll search for our <router /> nodes in. So this

$routersConfigNode = Mage::getConfig()->getNode($configArea.'/routers');

could look like the following (if we had variable x-ray specs)

$routersConfigNode = Mage::getConfig()->getNode('frontend/routers');
$routersConfigNode = Mage::getConfig()->getNode('admin/routers');

When Magento finds a child node of the <router /> node, it looks for a sub-node at

args/use

If the value at use doesn’t match the value of the $useRouterName parameter (identifying the router type as standard or admin) passed into collectRoutes, we skip it. If not, our next step is to examine the node for any module names and place them in an array.

Module names are pulled from one of two locations. The first is the value of the code at

args/module

Next, we search for any children nodes of

args/modules

IMPORTANT: Note the plural s. This is a separate node from singular <module>.

If we find any nodes, we add them to our array of modules. If these child nodes have a before or after attribute, that attribute drives the position of the module name in the array. The <modules> feature was added in Magento 1.3 to enable “Real” Controller Overrides. We’ll talk more about this later.

With our array of $modules in hand, we look at the value of the

args/frontName

and place it in the $frontName variable. Finally, after all this, a call is made to the addModule method.

#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
$this->addModule($frontName, $modules, $routerName);

The $routerName value comes from our container node (the child of <routers>).

An Example

As that’s twice as confusing to read as it was to write, some examples are in order. Consider the following bit of config.xml from the Catalog module

#File: app/code/core/Mage/Catalog/etc/config.xml
<frontend>
    <!-- ... -->

    <routers>
        <catalog>
            <use>standard</use>
            <args>
                <module>Mage_Catalog</module>
                <frontName>catalog</frontName>
            </args>
        </catalog>
    </routers>

    <!-- ... -->
</frontend>

There’s one child of <routers>, named <catalog>. The first step is to look at

<use>standard</use>

The value of this node is standard, which means if this were the Admin router we’d skip it, but since it’s the Standard router, we continue.

Next, we look at

<module>Mage_Catalog</module>

and pluck out the value, and place it in $modules. Then, we grab our $frontName from

<frontName>catalog</frontName>

Finally, the $routeInfo variable will contain the string catalog, not because of <frontName>, but rather because the entire node is named <catalog>.

With all of that done, our addModule method ends up looking like this

$this->addModule('catalog', array('Mage_Catalog'), 'catalog');  

Multiple Module Example

Here’s another, more complex example. With the Real Controller Overrides feature, your final merged config node might look like this

<catalog>
    <use>standard</use>
    <args>
        <module>Mage_Catalog</module>
        <modules>
            <sintax before="Mage_Catalog">Mage_Sintax</sintax>                        
        </modules>
        <frontName>catalog</frontName>                    
    </args>
</catalog>

This is almost the same as before, but with an additional <modules> node. In this case, our final call would look like

$this->addModule('catalog', array('Mage_Sintax','Mage_Catalog'), 'catalog');

That’s because, in addition to <module> for values, we’re also looking through <modules>,

The order of the array values is determined by the before parameter. That is, because there IS a before parameter, Mage_Sintax is inserted into the array before Mage_Catalog.

Adding the Module

Now that we know how addModule is called, let’s take another look at the method definition itself.

#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
public function addModule($frontName, $moduleName, $routeName)
{
    $this->_modules[$frontName] = $moduleName;
    $this->_routes[$routeName] = $frontName;
    return $this;
}

We see that adding a module means adding an entry to the $_modules object property which keeps track of modules indexed by frontName, as well as adding an entry to the $_routes property, which keeps track of $frontNames indexed by $routeName (the route name, again, being the name of the XML node itself).

That means in our examples above, we’d end up with something like

$this->_modules['catalog'] = array('Mage_Sintax','Mage_Catalog');
$this->_routes['catalog'] = 'catalog';

The big take away from this is you have three different strings

  1. A short module string
  2. A full module name
  3. A route name

and each of these strings takes on a different meaning depending on the context they’re used in, including aping the identity of each other. Confusing? Take a sip.

Back up Top

Still with us? Good, we’ve put in for a congressional medal. Back before our detour into collecting routes, we had extracted a module string from our request and were trying to get a list of real module names from the $_module object property (populated by collectRoutes)

#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
$modules = $this->getModuleByFrontName($module);

...

public function getModuleByFrontName($frontName)
{
    if (isset($this->_modules[$frontName])) {
        return $this->_modules[$frontName];
    }
    return false;
}

Now that we’re educated in how Magento collects its routes, we can say that what we’re doing here is examining the $module string we extracted from the request and matching it against <frontName> tags in the config to find a <routers> node, and then returning an array of real modules names (array('Mage_Sintax','Mage_Catalog');) that are also in that <routers> node.

Phew!

No Module

Next up in our match method there’s the possibility that we’ve found NO module names. There’s a code block for that

#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
$modules = $this->getModuleByFrontName($module);

/**
 * If we did not found anything  we searching exact this module
 * name in array values
 */
if ($modules === false) {
    if ($moduleFrontName = $this->getModuleByName($module, $this->_modules)) {
        $modules = array($module);            
    } else {
        return false;
    }
}

Here the code is making one last ditch effort to find a module name via the getModuleByName method

#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
public function getModuleByName($moduleName, $modules)
{
    foreach ($modules as $module) {
        if ($moduleName === $module || (is_array($module)
                && $this->getModuleByName($moduleName, $module))) {
            return true;
        }
    }
    return false;
}

This appears to be a bit of legacy code that’s looking for entires added to the internal module array in a different, pre Magento 1.3 format. Whatever it’s for, it looks to be legacy code that shouldn’t impact modules built using modern recommendations.

If this last ditch effort fails, we bail on the the match attempt by returning false, passing control back to the front controller object

After all that we have one last bit of code before moving on to controller searching. It’s another one of the hinky Admin router special cases, which we’ll come back to later.

#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
//checkings after we foundout that this router should be used for current module
if (!$this->_afterModuleMatch()) {
    return false;
}

Consequences of Module Name Searching

Before we move on to finding the controller, there’s another important take away from the above. When you’re configuring Magento, only one config.xml file (meaning on Magento module) may “claim” a particular <frontName>, and somewhat counter intuitively it will be the last config.xml parsed that wins. While Magento won’t force its will on you here, don’t hack around with existing <frontNames> in your own modules, use the multiple <modules> mechanism instead.

Or if you do, don’t email me about it.

Finding a Controller

Alright, we’ve got a list of candidate modules, let’s find a controller to dispatch. Magento is going to loop over our list of candidate modules (often a list of one, such as array('Mage_Catalog')) with a foreach

#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
foreach ($modules as $realModule) {
    $request->setRouteName($this->getRouteByFrontName($module));
    ...
}

The first thing that happens within the loop is we manipulate the request object by setting its route name. If you take a look at the getRouteByFrontName method

#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
public function getRouteByFrontName($frontName)
{
    return array_search($frontName, $this->_routes);
}

you can see we’re reaching into the $_routes object property and pulling out the route name, (array_seach looks through an array for a value, and then returns that value’s corresponding key).

In case you can’t recall, the route name is the name of the sub-<routers> node from config.xml. Next, let’s take a look at the request object’s setRouteName method

#File: app/code/core/Mage/Core/Controller/Request/Http.php
public function setRouteName($route)
{
    $this->_route = $route;
    $router = Mage::app()->getFrontController()->getRouterByRoute($route);
    if (!$router) return $this;
    $module = $router->getFrontNameByRoute($route);
    if ($module) {
        $this->setModuleName($module);
    }
    return $this;
}    

You can see that, in addition to setting an internal object property on the request object ($_route), Magento also goes back to our Standard router object (via. getRouterByRoute) to find the current <frontName> for the route. Further confusing things, it then sets the “module name” on the request object as well, using the looked up value of the <frontName>.

So what does “module name” mean in this context? Well, the setModuleName is on the request object’s ancestor class, Zend_Controller_Request_Abstract. This is the Zend module name, not the Magento module name. Except maybe, at the start of the project, the intention was for them to be the same thing. Additionally, as we’ll see later in match we end up resetting the module name near the end of the method. Take a sip.

Back up top, our next bit of code is similar to that block that fetched us a list of modules, except this time it’s looking for a controller name

#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
// get controller name
if ($request->getControllerName()) {
    $controller = $request->getControllerName();
} else {
    if (!empty($p[1])) {
        $controller = $p[1];
    } else {
        $controller = $front->getDefault('controller');
        $request->setAlias(
            Mage_Core_Model_Url_Rewrite::REWRITE_REQUEST_PATH_ALIAS,
            ltrim($request->getOriginalPathInfo(), '/')
        );
    }
}

Again, we first ask the request object if a custom controller has been set elsewhere. If not (the normal state of affairs first time through), we look at the second part of our path information ($p[1]). If there’s nothing in there, we go to the default set on the front controller object (in a normal operating system, that’s index)

So, for the following path information

catalog/category/view/id/8

the string category will be stored in the $controller variable.

A similar thing happens next, this time for the action name. The following code would gives us the string view in the $action variable

#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
// get action name
if (empty($action)) {
    if ($request->getActionName()) {
        $action = $request->getActionName();
    } else {
        $action = !empty($p[2]) ? $p[2] : $front->getDefault('action');
    }
}

Again, we see the same pattern

  1. Ask the request,
  2. Check $p[2]
  3. Ask the front controller object.

Before we move on to using these two strings to find a controller class and action method, there’s this line of code to consider

#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
$this->_checkShouldBeSecure($request, '/'.$module.'/'.$controller.'/'.$action);

The _checkShouldBeSecure method is responsible for automatically redirecting http pages to https in certain contexts. If your store is doing this unexpectedly, this is the part of the core you’ll want to inspect.

Getting Some Class

At this point in the method, we’ve got a controller and action string. Next up, we’re going to search the current $realModule (remember, we’re in a foreach loop) and see if there’s a controller class file that corresponds to the string in that variable

#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
$controllerClassName = $this->_validateControllerClassName($realModule, $controller);

Using our previous examples and x-ray specs, this might look something like

#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
$this->_validateControllerClassName('Mage_Catalog', 'category');

Taking a closer look at the _validateControllerClassName method reveals a bunch of interesting and important logic.

#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
protected function _validateControllerClassName($realModule, $controller)
{
    $controllerFileName = $this->getControllerFileName($realModule, $controller);
    if (!$this->validateControllerFileName($controllerFileName)) {
        return false;
    }

    $controllerClassName = $this->getControllerClassName($realModule, $controller);
    if (!$controllerClassName) {
        return false;
    }

    // include controller file if needed
    if (!$this->_includeControllerClass($controllerFileName, $controllerClassName)) {
        return false;
    }

    return $controllerClassName;
}

We can see this is a three stage process. First, we generate a controller file name. Next, if that file ends up existing, we generate a controller class name. Then, using both the class name and file name, we ensure the controller file is included and the class exists. If you’re ever wondered why controllers aren’t auto-loaded classes, wonder no more.

We’ll leave the specifics of all this as an exercise for the reader. The main take away here is that this method contains the name of the controller file and class that Magento will be looking for, and I’ve found this is the best place for a few temporary strategic var_dumps when debugging routing problems.

Back up top again, our next action is

#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
if (!$controllerClassName) {
    continue;
}

This conditional ensures that if any of the previous three steps failed, we stop trying to match this $realModule, and move on to the next one (if there is a next one). That is, continue tells PHP to jump to the next loop iteration immediately.

Alternately, if everything is well, we instantiate a controller object with the following code

#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
$controllerInstance = Mage::getControllerInstance($controllerClassName, $request, $front->getResponse());

and then check if our controller has an action method that matches our $action string. (Notice that the logic for this falls to the controller object)

#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
if (!$controllerInstance->hasAction($action)) {
    continue;
}

If not, the foreach moves on to the next $realModule. If the controller does have an action that corresponds to what we’ve pulled from the request/url, we mark a flag variable, and then break out of the loop.

#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
$found = true;
break;

Almost Ready for Dispatch

Alright! We’re almost there. Just another round of failure checking to go and we’ll be ready to dispatch.

If we got through all our $realModules and didn’t find anything, proper steps need to be taken. So, we check the $found flag that was set (or not) earlier, and if its false …

#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
if (!$found) {
    if ($this->_noRouteShouldBeApplied()) {
        $controller = 'index';
        $action = 'noroute';

        $controllerClassName = $this->_validateControllerClassName($realModule, $controller);
        if (!$controllerClassName) {
            return false;
        }

        // instantiate controller class
        $controllerInstance = Mage::getControllerInstance($controllerClassName, $request, $front->getResponse());

        if (!$controllerInstance->hasAction($action)) {
            return false;
        }
    } else {
        return false;
    }
}

When performing standard routing, the long and the short of this block is that match returns false. The _noRouteShouldBeApplied method is hard coded to return true. This block of code will get more interesting when we explore the Admin router.

A few final things to do to the request object. We’ll set a module name property (again, using our <frontName> value, the controller name, action name, as well as the final $realModule).

// set values only after all the checks are done
$request->setModuleName($module);
$request->setControllerName($controller);
$request->setActionName($action);
$request->setControllerModule($realModule);

Remember, with the exception of the $realModule variable, these are all the values found in the path information, NOT the final class, method, or module names.

$request->setModuleName('catalog');
$request->setControllerName('category');
$request->setActionName('view');
$request->setControllerModule('Mage_Catalog');

Finally, starting at the fourth index position, we parse the remainder of the path information variable $p into key/value parameter pairs for the request object.

#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php    
// set parameters from pathinfo
for ($i=3, $l=sizeof($p); $i<$l; $i+=2) {
    $request->setParam($p[$i], isset($p[$i+1]) ? urldecode($p[$i+1]) : '');
}

So, an example URL such as

catalog/category/view/id/8/this/that

means code something like the following is run

$request->setParam('id', urldecode(8));
$request->setParam('this', urldecode('that'));

That means when using a $request object from a controller action, you can fetch the value with a line of code like this

public function viewAction()
{
    $this->getRequest()->getParam('id')        
}

Dispatch Time!

Finally, we’re ready to dispatch the controller, which will kick off the MVC system and eventually result in the request object’s body being set, and then return true. In doing so, we fulfill our contract as a router object.

Compared to the rest of the match method, this is simplicity defined

#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
$request->setDispatched(true);
$controllerInstance->dispatch($action);

return true;

The controller’s dispatching logic is it’s own thing, we just pass it the $action and let it do whatever it wants to the response object (such as setting a body). You’ll also notice we called $request->setDispatched(true); before we dispatched the controller. Since the controller also has access to the request object, this means a controller can decide it hasn’t dispatched itself, and signal to the front controller object that all the router objects should be looped over again.

Believe it or not, we’re done!

10,000 Foot View

Alright, that was a lot to digest, but remember that this is some of the oldest code in the Magento system, and code whose functionality can change the least without breaking third party modules.

All the specifics aside, what’s really going on is relatively simple.

  1. The URL is parsed for a string representing the module, controller, and action for a specific request

  2. Those items found, we search the modules for a specific controller file that contains an action method matching what we found

  3. If those items are found, we tell the controller to dispatch a particular action, if not we pass the request on to the next router object.

Wrap Up

Congratulations, you’ve gotten through the worst of it. If you were able to follow us all the way through that, then you’ll find the rest of Magento routing easy as pie. Next time we’ll cover the logic of the remaining router objects, at which point you’ll be a true Magento routing master.

Originally published August 31, 2011
Series Navigation<< In Depth Magento Dispatch: Top Level RoutersIn Depth Magento Dispatch: Stock Routers >>

Copyright © Alan Storm 1975 – 2017 All Rights Reserved

Originally Posted: 31st August 2011