- A Survey of PHP Error Handling
- Magento’s Mini Error Framework
- 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
- Fetching the path to a base template
- Fetching the path to a content template
-
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.