- In Depth Magento Dispatch: Top Level Routers
- In Depth Magento Dispatch: Standard Router
- In Depth Magento Dispatch: Stock Routers
- In Depth Magento Dispatch: Rewrites
- In Depth Magento Dispatch: Advanced Rewrites
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
- Provide a match method which examines the request object and returns true if the router wishes to “claim” a request
- Mark the request object as dispatched, or through inaction fail to mark it as dispatched
- 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 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
- A short module string
- A full module name
- 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
- Ask the request,
- Check
$p[2]
- 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_dump
s 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 $realModule
s 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.
- The URL is parsed for a string representing the module, controller, and action for a specific request
- Those items found, we search the modules for a specific controller file that contains an action method matching what we found
-
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.