Magento Dispatch Process

Following on from the Magento Front Controller article, we now take a look at the Magento dispatch process within the dispatch() method. Let’s take a look at it.

public function dispatch()
{
    $request = $this->getRequest();

    // If pre-configured, check equality of base URL and requested URL
    $this->_checkBaseUrl($request);

    $request->setPathInfo()->setDispatched(false);

    $this->_getRequestRewriteController()->rewrite();

    Varien_Profiler::start('mage::dispatch::routers_match');
    $i = 0;
    while (!$request->isDispatched() && $i++ < 100) {
        foreach ($this->_routers as $router) {
            /** @var $router Mage_Core_Controller_Varien_Router_Abstract */
            if ($router->match($request)) {
                break;
            }
        }
    }
    Varien_Profiler::stop('mage::dispatch::routers_match');
    if ($i>100) {
        Mage::throwException('Front controller reached 100 router match iterations');
    }
    // This event gives possibility to launch something before sending output (allow cookie setting)
    Mage::dispatchEvent('controller_front_send_response_before', array('front'=>$this));
    Varien_Profiler::start('mage::app::dispatch::send_response');
    $this->getResponse()->sendResponse();
    Varien_Profiler::stop('mage::app::dispatch::send_response');
    Mage::dispatchEvent('controller_front_send_response_after', array('front'=>$this));
    return $this;
}

Firstly, we check the base URL using the _checkBaseUrl() method. This method will redirect a visitor to the configured base URL if the current request does not match it.

This is used for, if for example a customer arrived on your website using http://www.yoursite.com and your configured base URL was http://yoursite.com. This check prevent duplicate content being available on both www and non-www versions of your website.

Here are the interesting lines of code. We enter a while loop that loops around if the isDispatched() flag of the $request variable remains false. In addition, we foreach around our _routers array property to see if any of the routers can process the request.

$i = 0;
while (!$request->isDispatched() && $i++ < 100) {
    foreach ($this->_routers as $router) {
        /** @var $router Mage_Core_Controller_Varien_Router_Abstract */
        if ($router->match($request)) {
            break;
        }
    }
}
Varien_Profiler::stop('mage::dispatch::routers_match');
if ($i>100) {
    Mage::throwException('Front controller reached 100 router match iterations');
}

The standard, cms and default routers all have a match() method, and the admin router just extends the standard router, so it uses the standard routers match() method.

Let’s take a look at the standard router’s match() method first. Unfortunately this method is rather long so for the purpose of this article it will be broken up into chunks.

So firstly, we check to see if the path info (that’s the part of the URL after the “/”) contains anything and if it does, we then split out the URL into a $p variable. If it doesn’t, we simply set the $p variable to the default Magento path, which in the admin, is usually set to cms.

<?php
class Mage_Core_Controller_Varien_Router_Standard extends Mage_Core_Controller_Varien_Router_Abstract {
    ....
    public function match(Zend_Controller_Request_Http $request)
    {
        ....

        $front = $this->getFront();
        $path = trim($request->getPathInfo(), '/');

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

So a URL of www.yoursite.com/customer/account/login, would be split into the following.

Array
(
    [0] => customer
    [1] => account
    [2] => login
)

Whereas the URL of www.yoursite.com, would be split into:

Array
(
    [0] -> cms
)

Then we proceed to get the module name (or front name as it’s sometimes called), which is the first part of the URL.

// get module name
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, '');
    }
}

Here we’re checking if $request->getModuleName() returns something. If it does, then we use this as the module name. This is usually the case when a previous router has already set the module name and delegated to the standard router.

If it doesn’t return something, we use the first part of the $p array and set the module name to this. It’s rare that $p[0] isn’t set to anything. Within the admin under System -> Configuration -> General -> Web and expand the Default Pages section and view the Default URL option. If you change the cms value and replaced it with an empty string, the core front name would be set as the module name.

if (!$module) {
    if (Mage::app()->getStore()->isAdmin()) {
        $module = 'admin';
    } else {
        return false;
    }
}

We then grab a list of modules using the getModuleByFrontName() method.

$modules = $this->getModuleByFrontName($module);
if ($modules === false) {
    return false;
}

// checks after we found out that this router should be used for current module
if (!$this->_afterModuleMatch()) {
    return false;
}

A list of more than one modules may arise when, say for example, you had a custom module called Namespace_Customermodule that had a front name of customer. This customer front name is also used in the Mage_Customer module, so these two modules would be included in the $modules array.

We then loop around the $modules array, setting the route name of the request as the value configured in the pair of frontName nodes found in the module’s config.xml file.

We also try and find a controller and action name for the module. If none are specified, we set the $controller to index and the $action to index.

$found = false;
foreach ($modules as $realModule) {
    $request->setRouteName($this->getRouteByFrontName($module));

    // 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(), '/')
            );
        }
    }

    // get action name
    if (empty($action)) {
        if ($request->getActionName()) {
            $action = $request->getActionName();
        } else {
            $action = !empty($p[2]) ? $p[2] : $front->getDefault('action');
        }
    }

    //checking if this place should be secure
    $this->_checkShouldBeSecure($request, '/'.$module.'/'.$controller.'/'.$action);

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

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

    if (!$this->_validateControllerInstance($controllerInstance)) {
        continue;
    }

    if (!$controllerInstance->hasAction($action)) {
        continue;
    }

    $found = true;
    break;
}

If we find a relevant controller name and action name, we set the $found flag to true and break out of the loop.

However if we do not found a suitable module to use, then Magento will continue looking in the next router.

/**
 * if we did not found any suitable
 */
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;
    }
}

Finally, we set the module name, controller name, action name and controller module properties to the given values. The setDispatched flag is also set to true and thus we break out of the while loop.

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

// 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]) : '');
}

// dispatch action
$request->setDispatched(true);
$controllerInstance->dispatch($action);

return true;

The CMS router uses the identifier (that’s the part after the “/”) and the cms/page model to check to see if a $pageId exists for the identifier.

If it does, we delegate to the standard router by setting the following.

  • Module/Front Name – cms
  • Controller Name – page
  • Action Name – view
  • Parameters – page_id = $pageId
public function match(Zend_Controller_Request_Http $request)
{
    if (!Mage::isInstalled()) {
        Mage::app()->getFrontController()->getResponse()
            ->setRedirect(Mage::getUrl('install'))
            ->sendResponse();
        exit;
    }

    $identifier = trim($request->getPathInfo(), '/');

    $condition = new Varien_Object(array(
        'identifier' => $identifier,
        'continue'   => true
    ));
    Mage::dispatchEvent('cms_controller_router_match_before', array(
        'router'    => $this,
        'condition' => $condition
    ));
    $identifier = $condition->getIdentifier();

    if ($condition->getRedirectUrl()) {
        Mage::app()->getFrontController()->getResponse()
            ->setRedirect($condition->getRedirectUrl())
            ->sendResponse();
        $request->setDispatched(true);
        return true;
    }

    if (!$condition->getContinue()) {
        return false;
    }

    $page   = Mage::getModel('cms/page');
    $pageId = $page->checkIdentifier($identifier, Mage::app()->getStore()->getId());
    if (!$pageId) {
        return false;
    }

    $request->setModuleName('cms')
        ->setControllerName('page')
        ->setActionName('view')
        ->setParam('page_id', $pageId);
    $request->setAlias(
        Mage_Core_Model_Url_Rewrite::REWRITE_REQUEST_PATH_ALIAS,
        $identifier
    );

    return true;
}

The Default router’s match() method is set when Magento does not know how to handle the request. The module/front name, controller name, and action name are usually set to cms/index/noroute. Therefore this router also delegates to the standard router.

public function match(Zend_Controller_Request_Http $request)
{
    $noRoute        = explode('/', $this->_getNoRouteConfig());
    $moduleName     = isset($noRoute[0]) && $noRoute[0] ? $noRoute[0] : 'core';
    $controllerName = isset($noRoute[1]) && $noRoute[1] ? $noRoute[1] : 'index';
    $actionName     = isset($noRoute[2]) && $noRoute[2] ? $noRoute[2] : 'index';

    if ($this->_isAdmin()) {
        $adminFrontName = (string)Mage::getConfig()->getNode('admin/routers/adminhtml/args/frontName');
        if ($adminFrontName != $moduleName) {
            $moduleName     = 'core';
            $controllerName = 'index';
            $actionName     = 'noRoute';
            Mage::app()->setCurrentStore(Mage::app()->getDefaultStoreView());
        }
    }

    $request->setModuleName($moduleName)
        ->setControllerName($controllerName)
        ->setActionName($actionName);

    return true;
}

Arriving back at the dispatch method, we find the following line.

$this->getResponse()->sendResponse();

When the response has been generated, it gets flushed to the browser.

public function getResponse()
{
    if (empty($this->_response)) {
        $this->_response = new Mage_Core_Controller_Response_Http();
        $this->_response->headersSentThrowsException = Mage::$headersSentThrowsException;
        $this->_response->setHeader("Content-Type", "text/html; charset=UTF-8");
    }
    return $this->_response;
}
public function sendResponse()
{
    $this->sendHeaders();

    if ($this->isException() && $this->renderExceptions()) {
        $exceptions = '';
        foreach ($this->getException() as $e) {
            $exceptions .= $e->__toString() . "\n";
        }
        echo $exceptions;
        return;
    }

    $this->outputBody();
}

Note: This article is based on Magento Community/Open Source version 1.9.