Categories


Archives


Recent Posts


Categories


Magento’s Mini Error Framework

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!

This entry is part 2 of 3 in the series PHP Error Handling. Earlier posts include A Survey of PHP Error Handling. Later posts include Laravel's Custom Error Handler.

If you’re a long time reader, you’re probably aware of my article on Magento’s many 404 pages. It’s been one of the more useful articles I’ve written here — I own the Google results for the phrase “Which 404 page” in Magento land.

One thing I didn’t cover was how Magento displays the store exception 404 page. That’s because the how is an article series unto itself, and I’m starting that series today. Magento’s store exception 404 page, as well as its 503 page and exception/error reporting page are all controlled by a mini framework that lives in the errors/ folder.

Over the next few weeks we’ll explore this mini framework — both from a “how can I customize these misunderstood Magento error pages” point of view, as well as a framework design point of view. Today we’re going to walk through the code path of Magento handling a 404 store exception.

Magento Application Main Exception Block

The best place to start is here

#File: app/Mage.php
public static function run($code = '', $type = 'store', $options = array())
{
    try {
        Varien_Profiler::start('mage');
        self::setRoot();
        if (isset($options['edition'])) {
            self::$_currentEdition = $options['edition'];
        }
        self::$_app    = new Mage_Core_Model_App();
        if (isset($options['request'])) {
            self::$_app->setRequest($options['request']);
        }
        if (isset($options['response'])) {
            self::$_app->setResponse($options['response']);
        }
        self::$_events = new Varien_Event_Collection();
        self::_setIsInstalled($options);
        self::_setConfigModel($options);
        // throw new Mage_Core_Model_Store_Exception('wtf?'); 
        // throw new Exception("Wtf");
        self::$_app->run(array(
            'scope_code' => $code,
            'scope_type' => $type,
            'options'    => $options,
        ));

        Varien_Profiler::stop('mage');
    } catch (Mage_Core_Model_Session_Exception $e) {
        header('Location: ' . self::getBaseUrl());
        die();
    } catch (Mage_Core_Model_Store_Exception $e) {
        require_once(self::getBaseDir() . DS . 'errors' . DS . '404.php');
        die();
    } catch (Exception $e) {
        if (self::isInstalled() || self::$_isDownloader) {
            self::printException($e);
            exit();
        }
        try {
            self::dispatchEvent('mage_run_exception', array('exception' => $e));
            if (!headers_sent() && self::isInstalled()) {
                header('Location:' . self::getUrl('install'));
            } else {
                self::printException($e);
            }
        } catch (Exception $ne) {
            self::printException($ne, $e->getMessage());
        }
    }
}

This is the main Magento run method. This is the method that Magento’s main index.php calls to start Magento’s page/URL processing. There’s a lot of important-but-extraneous-to-us code in there, so let’s consider this simplified version

#File: app/Mage.php
public static function run($code = '', $type = 'store', $options = array())
{
    try {
        //...
        self::$_app    = new Mage_Core_Model_App();
        //...
        self::$_app->run(array(
            'scope_code' => $code,
            'scope_type' => $type,
            'options'    => $options,
        ));
    } catch (Mage_Core_Model_Session_Exception $e) {
        //...
        die();
    } catch (Mage_Core_Model_Store_Exception $e) {
        //...
        die();
    } catch (Exception $e) {
        //...
        die();
    }
}

The Mage::run method is, at its heart, a simple try/catch block. Magento instantiates a Mage_Core_Model_App object, and then calls its run method. If no exceptions occur during processing, everything continues as normal. However, if an exception occurs, three different error/catch branches are considered. One catches a Mage_Core_Model_Session_Exception exception, another looks for a Mage_Core_Model_Store_Exception exception, and the final one looks for a plain old PHP Exception exception (which will also include any exception type not caught above, including the Mage_Core_Exception exception). Magento handles each exception type differently.

The exception type we’ll look at today is the Mage_Core_Model_Store_Exception exception.

Store Exception

All Magento requests have a global “store” object. This object is responsible for keeping track of the currently set store id. Anytime the core code needs to know the store id (to lookup a store specific configuration, price, behavior, etc.) it uses the Magento core/store singleton (a Mage_Core_Model_Store object)

If Magento can’t instantiate this store object, or find a store_id/code/scope-type for the store object, this means there’s something seriously wrong. Most of the application functionality will break under these condition. The Mage_Core_Model_Store_Exception exists to signal that this is the case, and that Magento should shut down the request.

If we take a look at the store exception catch block, we see the following

#File: app/Mage.php
catch (Mage_Core_Model_Store_Exception $e) {
    require_once(self::getBaseDir() . DS . 'errors' . DS . '404.php');
    die();
catch (Exception $e) {

This means Magento will require in the errors/404.php file. This brings us to today’s real topic — Magento’s mini error framework.

Error Processor

If you take a look inside 404.php, you’ll see the following

#File: errors/404.php
require_once 'processor.php';

$processor = new Error_Processor();
$processor->process404();

When considering the case of the Magento store exception above, this looks like a file that requires another file, instantiates an object, and then calls a method. However, consider that this 404.php file is also accessible directly via the browser

http://store.magento.com/errors/404.php

In this context, the Error_Processor object starts to look like a simple controller, and the process404 method starts to look like a corresponding controller action. This idea is strengthened if you look at the 503.php page.

#File: errors/503.php
require_once 'processor.php';

$processor = new Error_Processor();
$processor->process503();

Here we have the same pattern. Magento instantiates the same Error_Processor object (a controller), and calls the process503 method (a controller action).

The rest of this article will walk though the execution of the process404 method, and we’ll start to see other ways these error handling pages/scripts resemble a simple, rudimentary PHP framework.

Processing an Error

If we take a look at the process404 method, we see a pretty simple definition

#File: errors/processor.php    
public function process404()
{
    $this->pageTitle = 'Error 404: Not Found';
    $this->_sendHeaders(404);
    $this->_renderPage('404.phtml');
}

Magento sets a page title property on the object, calls the _sendHeaders method, and then calls the _renderPage method. We’ll return to the pageTitle property and renderPage method later, but first let’s look at the _sendHeaders method.

protected function _sendHeaders($statusCode)
{
    $serverProtocol = !empty($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0';
    switch ($statusCode) {
        case 404:
            $description = 'Not Found';
            break;
        case 503:
            $description = 'Service Unavailable';
            break;
        default:
            $description = '';
            break;
    }

    header(sprintf('%s %s %s', $serverProtocol, $statusCode, $description), true, $statusCode);
    header(sprintf('Status: %s %s', $statusCode, $description), true, $statusCode);
}

Here we see a simple method that takes a numeric $statusCode, translates it into a description, and then issues two HTTP headers. The first header call

header(sprintf('%s %s %s', $serverProtocol, $statusCode, $description), true, $statusCode);

also uses the $serverProtocol variable. Consider the first line of this method

$serverProtocol = !empty($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0';

The above code will populate $serverProtocol with HTTP/1.0 or whatever is set in PHP’s $_SERVER['SERVER_PROTOCOL'] variable (mostly likely HTTP/1.1). If we expand the sprintf call, this would look something like

header('HTTP/1.1 404 Not Found', true, 404);

The second header call

header(sprintf('Status: %s %s', $statusCode, $description), true, $statusCode);

is similar, in that it outputs a status header that looks something like this

header('Status: 404 Not Found');

If we use curl to look at the headers of a 404.php request, we’ll see both these headers

$ curl -I 'http://store.example.com/errors/404.php'
HTTP/1.1 404 Not Found
//...
Status: 404 Not Found
//...    
Content-Type: text/html

There’s a few interesting things here worth talking about. First, the initial header call uses the header function’s seldom seen second and third parameters. If we take a look at the function prototype from the docs

void header ( string $string [, bool $replace = true [, int $http_response_code ]] )

We see the second $replace parameter

indicates whether the header should replace a previous similar header, or add a second header of the same type. By default it will replace, but if you pass in FALSE as the second argument you can force multiple headers of the same type.

and the third $http_response_code

Forces the HTTP response code to the specified value. Note that this parameter only has an effect if the string is not empty.

The use of this syntax is puzzling. The $replace variable defaults to true, and the $http_status_code parameter is meant for use with the Location: header and setting 301/302/etc. status codes. Whether these explicit parameters point to the original core team’s inexperience with PHP/HTTP, or their encounters with strange PHP systems in the wild that behaved erratically (or both!) is hard to say.

The second interesting thing is the protocol header being explicitly set to match PHP’s. This seems like a needless bit of extra programming busy work since both 404 and 503 are HTTP 1.0 status codes, and near all web browsers understand them. However, if you scroll down to the comments on the PHP docs, you’ll see this

I had big troubles with an Apache/2.0.59 (Unix) answering in HTTP/1.0 while I (accidentally) added a “HTTP/1.1 200 Ok” – Header.
Most of the pages were displayed correct, but on some of them apache added weird content to it:

Here we have a case where, to an outsider who works on a single projects, Magento’s code looks extra explicit, busy and verbose, but to the Magento core team it looks like code that will help their users deal with an obscure bit of weird apache behavior.

Finally, I don’t know why Magento adds a second, explicit Status header, but given the range of systems Magento needed to work with, I wouldn’t be surprised if it’s another obscure “good enough” edge case bug in some web server or browser.

All these problems are a nice illustration of why, as an application developer, you want your framework of choice handling these sorts of low level details so that all you need to say is “Send a not found header”, and be back to concentrating on your application instead of 15 years of HTTP specifications and edge-casey browser/server behavior.

Rendering the Page

If we jump back to our main “action”

#File: errors/processor.php    
public function process404()
{
    $this->pageTitle = 'Error 404: Not Found';
    $this->_sendHeaders(404);
    $this->_renderPage('404.phtml');
}

After our _sendHeaders call, we have a _renderPage method call. If we’re thinking about this in a framework context, this is our request to render a view layer. If we pop down to the renderPage method

#File: errors/processor.php
protected function _renderPage($template)
{
    $baseTemplate = $this->_getTemplatePath('page.phtml');
    $contentTemplate = $this->_getTemplatePath($template);

    if ($baseTemplate && $contentTemplate) {
        require_once $baseTemplate;
    }
}

we can see rendering a page means

  1. Fetching the path to a base template

  2. Fetching the path to a content template

  3. If we can fetch both paths, then require in the base template, effectively rendering the page.

We’re going to save the actual template path fetching for another time, and tell you the paths fetched are

$baseTemplate    = '/path/to/magento/errors/default/page.phtml
$contentTemplate = '/path/to/magento/errors/default/404.phtml';

If you look at page.phtml (the $baseTemplate), you’ll see a mostly static HTML file. There’s a few lines of PHP that are worth calling out

#File: errors/default/page.phtml
//...
1. <title><?php echo $this->pageTitle?></title>
//...
2. <base href="<?php echo $this->getSkinUrl()?>" />
//...
3. <a href="<?php echo $this->getBaseUrl()?>" 
//
4. <?php require_once $contentTemplate; ?>

The first line prints our page title. You’ll remember back up in process404 we set this page title

#File: errors/processor.php    
public function process404()
{
    $this->pageTitle = 'Error 404: Not Found';
    $this->_sendHeaders(404);
    $this->_renderPage('404.phtml');
}

The second line outputs a URL for the <base/> tag. This lets the browser know where it can find image and css files for the 404 page. The third line outputs a base URL inside of a link. This links end-user-customers back to the main Magento site from the 404 page. Although getSkinUrl and getBaseUrl share names with methods in the main Magento framework, they’re different methods, defined on the Error_Processor class in errors/processor.php. We’ll return to these methods later.

Finally, we reach the most important line

#File: errors/default/page.phtml
<?php require_once $contentTemplate; ?>

The page.phtml file is a base template for the HTML page. The $contentTemplate path is the actual page we’re rendering. You’ll recall we set $contentTemplate up in the _renderPage method. When you include/require a file in PHP inside a method/function, the included file has the same variable context as the method. Also, this is what allows our template to reference the special PHP variable $this, which will point back to the Error_Processor object.

As we mentioned earlier, the $contentTemplate variable points to

$contentTemplate = '/path/to/magento/errors/default/404.phtml';

If we pop this file open, we’ll see a simple HTML fragment that contains our 404 text

<!-- File: errors/default/page.phtml -->
<div id="main" class="col-main">
<!-- [start] content -->
    <div class="page-title">
        <h1>404 error: Page not found.</h1>
    </div>
<!-- [end] content -->
</div>

With this final phtml file included, that completes our page rendering. When invoked via a URL

http://store.magento.com/errors/404.php

the require is the last bit of PHP code invoked. When invoked via the store exception catch block, Magento adds an explicit die to make sure the code exits.

catch (Mage_Core_Model_Store_Exception $e) {
    require_once(self::getBaseDir() . DS . 'errors' . DS . '404.php');
    die();
}

With that, our page rendering is complete.

Wrap Up

Although I’ve described this as a framework, I’m sure some of your are skeptical. Compared to robust systems like Symfony, Laravel, Zend, and Magento proper itself, the Error_Processing class hardly seems like a framework.

With the information presented so far, that’s a fair assessment. However, in our next few articles we’ll discuss some of the framework like features of this class. How it handles different actions, how it has its own global configuration parsing similar to the main Magento framework, and how it’s used for a variety of different error cases.

Beyond that though, it’s sometimes useful to think of all programming projects as having a framework. Even when developers are banging out code in an unfamiliar system under an unrealistic deadline, unwritten rules develop about what code goes where, and how to handle certain common tasks. A framework always develops out of projects longer than a weekend hackathon. Being able to put yourself in the mind of the programmers who worked on a particular sub-system is the first step to working effectively with that sub-system.

Originally published July 24, 2014
Series Navigation<< A Survey of PHP Error HandlingLaravel’s Custom Error Handler >>

Copyright © Alan Storm 1975 – 2017 All Rights Reserved

Originally Posted: 24th July 2014