Error Handling in PHP 5 CMS

Exclusive offer: get 50% off this eBook here
PHP 5 CMS Framework Development

PHP 5 CMS Framework Development — Save 50%

Expert insight and practical guidance to creating an efficient, flexible, and robust framework for a PHP 5-based content management system

$39.99    $20.00
by Martin Brampton | August 2010 | Content Management Open Source PHP Web Development

In this article by Martin Brampton author of PHP 5 CMS Framework Development, we will learn various aspects about error handling in PHP 5 Content Management System. Specifically we will cover:

  • PHP error handling
  • Database errors
  • Application errors
  • Exploring PHP—Error handling
  • Framework solution
  • Handling database errors
  • 404 and 403 errors

(For more resources on PHP, see here.)

The problem

Errors will happen whether we like it or not. Ideally the framework can help in their discovery, recording, and handling by:

  • Trapping different kinds of errors
  • Making a record of errors with sufficient detail to aid analysis
  • Supporting a structure that mitigates the effect of errors

Discussion

There are three main kinds of errors that can arise. Many possible situations can crop up within PHP code that count as errors, such as an attempt to use a method on a variable that turns out not to be an object, or is an object but does not implement the specified method. The database will sometimes report errors, such as an attempt to retrieve information from a non-existent table, or to ask for a field that has not been defined for a table. And the logic of applications can often lead to situations that can only be described as errors. What resources do we have to handle these error situations?

PHP error handling

If nothing else is done, PHP has its own error handler. But developers are free to build their own handlers. So that is the first item on our to do list. Consistently with our generally object oriented approach, the natural thing to do is to build an error recording class, and then to tell PHP that one of its methods is to be called whenever PHP detects an error. Once that is done, the error handler must deal with whatever PHP passes, as it has taken over full responsibility for error handling.

It has been a common practice to suppress the lowest levels of PHP error such as notices and warnings, but this is not really a good idea. Even these relatively unimportant messages can reveal more serious problems. It is not difficult to write code to avoid them, so that if a warning or notice does arise, it will indicate something unexpected and therefore worth investigation. For example, the PHP foreach statement expects to work on something iterable and will generate a warning if it is given, say, a null value. But this is easily avoided, either by making sure that methods which return arrays will always return an array, even if it is an array of zero items, rather than a null value. Failing that, the foreach can be protected by a preceding test. So it is safest to assume that a low level error may be a symptom of a bigger problem, and have our error handler record every error that is passed to it. The database is the obvious place to put the error, and the handler receives enough information to make it possible to save only the latest occurrence of the same error, thus avoiding a bloated table of many more or less identical errors.

The other important mechanism offered by PHP is new to version 5 and is the try, catch, and throw construct. A section of code can be put within a try and followed by one or more catch specifications that define what is to be done if a particular kind of problem arises. The problems are triggered by using throw. This is a valuable mechanism for errors that need to break the flow of program execution, and is particularly helpful for dealing with database errors. It also has the advantage that the try sections can be nested, so if a large area of code, such as an entire component, is covered by a try it is still possible to write a try of narrower scope within that code.

In general, it is better to be cautious about giving information about errors to users. For one thing, ordinary users are simply irritated by technically oriented error messages that mean nothing to them. Equally important is the issue of cracking, and the need to avoid displaying any weaknesses too clearly. It is bad enough that an error has occurred, without giving away details of what is going wrong. So a design assumption for error handling should be that the detail of errors is recorded for later analysis, but that only a very simple indication of the presence of an error is given to the user with a message that it has been noted for rectification.

Database errors

Errors in database operations are a particular problem for developers. Within the actual database handling code, it would be negligent to ignore the error indications that are available through the PHP interfaces to database systems. Yet within applications, it is hard to know what to do with such errors. SQL is very flexible, and a developer has no reason to expect any errors, so in the nature of things, any error that does arise is unexpected, and therefore difficult to handle. Furthermore, if there have to be several lines of error handling code every time the database is accessed, then the overhead in code size and loss of clarity is considerable.

The best solution therefore seems to be to utilize the PHP try, catch, and throw structure. A special database error exception can be created by writing a suitable class, and the database handling code will then deal with an error situation by "throwing" a new error with an exception of that class. The CMS framework can have a default try and catch in place around most of its operation, so that individual applications within the CMS are not obliged to take any action. But if an application developer wants to handle database errors, it is always possible to do so by coding a nested try and catch within the application.

One thing that must still be remembered by developers is that SQL easily allows some kinds of error situation to go unnoticed. For example, a DELETE or UPDATE SQL statement will not generate any error if nothing is deleted or updated. It is up to the developer to check how many rows, if any, were affected. This may not be worth doing, but issues of this kind need to be kept in mind when considering how software will work. A good error handling framework makes it easier for a developer to choose between different checking options.

Application errors

Even without there being a PHP or database error, an application may decide that an error situation has arisen. For some reason, normal processing is impossible, and the user cannot be expected to solve the problem. There are two main choices that will fit with the error handling framework we are considering.

One is to use the PHP trigger_error statement. It raises a user error, and allows an error message to be specified. The error that is created will be trapped and passed to the error handler, since we have decided to have our own handler. This mechanism is best used for wholly unexpected errors that nonetheless could arise out of the logic of the application.

The other choice is to use a complete try, catch, and throw structure within the application. This is most useful when there are a number of fatal errors that can arise, and are somewhat expected. The CMS extension installer uses this approach to deal with the various possible fatal errors that can occur during an attempt to install an extension. They are mostly related to errors in the XML packaging file, or in problems with accessing the file system. These are errors that need to be reported to help the user in resolving the problem, but they also involve abandoning the installation process. Whenever a situation of this kind arises, try, catch, and throw is a good way to deal with it.

Exploring PHP—Error handling

PHP provides quite a lot of control over error handling in its configuration. One question to be decided is whether to allow PHP to send any errors to the browser. This is determined by setting the value of display_errors in the php.ini configuration file. It is also possible to determine whether errors will be logged by setting log_errors and to decide where they should be logged by setting error_log. (Often there are several copies of this file, and it is important to find the one that is actually used by the system.) The case against sending errors is that it may give away information useful to crackers. Or it may look bad to users.

On the other hand, it makes development and bug fixing harder if errors have to be looked up in a log file rather than being visible on the screen. And if errors are not sent to the screen, then in the event of a fatal error, the user will simply see a blank screen. This is not a good outcome either.

Although the general advice is that errors should not be displayed on production systems, I am still rather inclined to show them. It seems to me that an error message, even if it is a technical one that is meaningless to the user, is rather better than a totally blank screen. The information given is only a bare description of the error, with the name and line number for the file having the error. It is unlikely to be a great deal of use to a cracker, especially since the PHP script just terminates on a fatal error, not leaving any clear opportunity for intrusion. You should make your own decision on which approach is preferable.

Without any special action in the PHP code, an error will be reported by PHP giving details of where it occurred. Providing our own error handler by using the PHP set_error_handler function gives us far more flexibility to decide what information will be recorded and what will be shown to the user. A limitation on this is that PHP will still immediately terminate on a fatal error, such as attempting a method on something that is not an object. Termination also occurs whenever a parsing error is found, that is to say when the PHP program code is badly formed. It is not possible to have control transferred to a user provided error handler, which is an unfortunate limitation on what can be achieved.

However, an error handler can take advantage of knowledge of the framework to capture relevant information. Quite apart from special information on the framework, the handler can make use of the useful PHP debug_backtrace function to find out the route that was followed before the error was reached. This will give information about what called the current code. It can then be used again to find what called that, and so on until no further trace information is available. A trace greatly increases the value of error reporting as it makes it much easier to find out the route that led to the error.

When an error is trapped using PHP's try and catch, then it is best to trace the route to the error at the point the exception is thrown. Otherwise, the error trace will only show the chain of events from the exception to the error handler.

There are a number of other PHP options that can further refine how errors are handled, but those just described form the primary tool box that we need for building a solid framework.

PHP 5 CMS Framework Development Expert insight and practical guidance to creating an efficient, flexible, and robust framework for a PHP 5-based content management system
Published: June 2008
eBook Price: $39.99
Book Price: $49.99
See more
Select your format and quantity:

(For more resources on PHP, see here.)

Framework solution

The first thing we need is the error handler class, which is invoked almost at the start of processing any request with the code:

$errorhandler = aliroErrorRecorder::getInstance($controller);
set_error_handler(array($errorhandler, 'PHPerror'));

What this does is create an error handler object from aliroErrorRecorder and to tell PHP to use the object's PHPerror method when an error is detected. The first part of the error handling class is as follows:

final class aliroErrorRecorder extends aliroDatabaseRow {
protected static $instance = null;
protected $DBclass = 'aliroCoreDatabase';
protected $tableName = '#__error_log';
protected $rowKey = 'id';

public static function getInstance ($request=null) {
return (null == self::$instance) ? (self::$instance = new self())
: self::$instance;
}

public function PHPerror ($errno, $errstr, $errfile, $errline,
$errcontext) {
if (!($errno & error_reporting())) return;
$rawmessage = function_exists('T_') ? T_('PHP Error %s: %s in %s
at line %s') : 'PHP Error %s: %s in %s at line %s';
$message = sprintf($rawmessage, $errno, $errstr, $errfile,
$errline);
$lmessage = $message;
if (is_array($errcontext)) {
foreach ($errcontext as $key=>$value) if (!is_object($value)
AND !(is_array($value))) $lmessage .= "; $key=$value";
}
$errorkey = "PHP/$errno/$errfile/$errline/$errstr";
$this->recordError($message, $errorkey, $lmessage);
aliroRequest::getInstance()->setErrorMessage(T_('A PHP error has
been recorded in the log'), _ALIRO_ERROR_WARN);
if ($errno & (E_USER_ERROR|E_COMPILE_ERROR|E_CORE_ERROR|E_ERROR))
die (T_('Serious PHP error - processing halted - see error log for
details'));
}

The class follows standard singleton logic, and it is convenient to make it a subclass of aliroDatabaseRow so that, among other things, it can represent a row of the error log table. The properties (apart from $instance) define the relationship with the database table.

When a PHP error occurs, the method PHPerror is called. The first thing it does is to check the level of the error that has been reported against the error level set in PHP. If the error falls outside those errors that are to be reported, it is ignored and an immediate return is made. In practice, Aliro normally runs at the maximum reporting level, but this has to be relaxed when older (or less developed) software is being accommodated. Software written to the full Aliro standard is assumed to have adopted the practice of eliminating all levels of error.

An error message is constructed, with translation if possible. Occasionally, this is not possible because the error occurs before the language system is active. The message is similar to the standard PHP error message. Values of variables that are in the context of the error are added to the long version of the error message.

Using the critical parameters of the error, an error key is constructed such that if the same error keeps occurring, it will have the same key. The error is then passed to the class's recordError method for writing to the database. A very simple message is set into the aliroRequest error reporting mechanism to inform the user that there has been a problem. Where the error is serious, processing terminates.

The recordError method is defined as:


public function recordError ($smessage, $errorkey, $lmessage='',
$exception=null)

Here, the parameters are a short message, the error key that stops the repetition of duplicate errors, the long message, and an optional database exception object. The processing is primarily about organizing all the data and writing it to the database, either as a new record or as an update of the time stamp if the error is a repeat of one already stored. After the record is written, the table is pruned so that a maximum of seven days' information is retained. Where a database exception object is supplied, additional fields are completed. This is discussed in more detail shortly.

Handling database errors

Errors in database operations are handled by throwing an exception. To do that, a suitable exception class is needed:

class databaseException extends Exception
{
public $dbname = '';
public $sql = '';
public $number = 0;

public function __construct ($dbname, $message, $sql,
$number, $dbtrace)
{
parent::__construct($message, $number);
$this->dbname = $dbname;
$this->sql = $sql;
$this->dbtrace = $dbtrace;
}
}

In principle, it is usually better to access data using methods rather than public properties, but the operations here are very simple. The class is used within the aliroDatabase class when an error is detected:

throw new databaseException ($this->DBname, $this->_errorMsg,
$this->_sql, $this->_errorNum, aliroBase::trace());

The result is that a new exception object is created, containing information about the database, the error message, the SQL, the error number and a trace of method or function calls to the point where the error occurs. The trace method is a static class method of aliroBase, provided for convenience of debugging. The general trapping of database errors is achieved by placing a try round the code that calls extensions to generate output:

try
{
...
}
catch (databaseException $exception)
{
$target = $this->core_item ? $this->core_item :
$this->option;
$message = sprintf(T_('A database error occurred on %s at %s
while processing %s'), date('Y-M-d'),
date('H:i:s'), $target);
$errorkey = "SQL/{$exception->getCode()}/$target/
$exception->dbname/{$exception->
getMessage()}/$exception->sql";
aliroErrorRecorder::getInstance()->recordError($message,
$errorkey, $message, $exception);
$this->redirect('', $message, _ALIRO_ERROR_FATAL);
}

The detailed code within the try is omitted for clarity. Any database error occurring within the try clause will be processed by the catch. This forms up a message and an error key, on similar principles to the PHP error processing described. Then the recordError method is called, just as it was for a PHP error, except this time we are outside the aliroErrorRecorder class instead of being already within it. The redirect causes processing to be abandoned, and the basic error message to be shown to the user.

404 and 403 errors

Although not an error in quite the same sense as we have been using up to now, there is a condition that we must handle, which is the problem of being supplied with a URI that does not work to define a page in our site. The name "404 error" comes from the fact that a web server that cannot return a page for a given URI is required to return an HTTP header containing an error with the number 404.

In some cases, this processing can still be done by the web server (such as Apache) but in other cases the URI will be of a form that appears legitimate until some processing has been done by our CMS. In this case, we can still come to the conclusion that the URI is illegal. It is important to give a suitable message to the user and it is also useful to record these errors as they sometimes indicate incorrect links within our own site or links that have been stored by search engines but have become invalid because of changes in our site.

Another kind of error is where a request is made for a page that is not permitted for the user. If the request is made by a visitor, and it might have been legal if made by a user who has logged in, then it is better for the application to return a message suggesting that the visitor may need to log in. But if the error is more likely attributable to an attempt to access information that is not intended to be available in the way requested, then the best response is to send an HTTP header indicating a 403 (not permitted) error and some simple text. To make application development easier, a class is provided for each of 404 and 403 errors.

They are subclassed from a common parent which contains this code:

abstract class aliroPageFail {
protected function formatMessage ($message) {
if ($message) return <<<FORMAT_MSG
<h4>
$message
</h4>
FORMAT_MSG;
}
protected function T_ ($string) {
return function_exists('T_') ? T_($string) : $string;
}
protected function recordPageFail ($errorcode) {
$database = aliroCoreDatabase::getInstance();
$uri = $database->getEscaped(@$_SERVER['REQUEST_URI']);
$timestamp = date ('Y-m-d H:i:s');
$referer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER']
: '';
$referer = $database->getEscaped($referer);
$ip = aliroSession::getSession()->getIP();
$post = base64_encode(serialize($_POST));
$trace = aliroRequest::trace();
$database->doSQL("INSERT INTO #__error_404 (uri, timestamp, referer,
ip, errortype, post, trace) VALUES ('$uri', '$timestamp', '$referer',
'$ip', '$errorcode', '$post', '$trace') ON DUPLICATE KEY UPDATE timestamp
= '$timestamp', referer='$referer', post='$post', trace='$trace'");
}
protected function searchuri () {
// Details omitted for clarity
}
}

The base class provides a simple HTML method to help create a message, followed by the recordPageFail method which records the problem to a database table. The latter method records all the details of the failed URI, including the IP address from which the request came. A significant number of 404 errors will result from hacking attempts, and the IP address may be of some use in resisting them. The searchuri method looks at the URI that was submitted and tries to find relevant content. Also a translation method is provided to safeguard against the translation function not being available and to permit use within heredoc.

An invalid URI error can be detected at various possible points, but once detected, Aliro deals with it by use of a dedicated aliroPage404 class. The code is quite straightforward and consists entirely of a constructor:


class aliroPage404 extends aliroPageFail {
public function __construct ($message='') {
if (aliroCore::getInstance()->getCfg('debug')) echo aliroRequest::
trace();
if (aliroComponentHandler::getInstance()->componentCount() AND
aliroMenuHandler::getInstance()->getMenuCount()) {
header ($_SERVER['SERVER_PROTOCOL'].' 404 Not Found');
$this->recordPageFail('404');
$searchtext = $this->searchuri();
$request = aliroRequest::getInstance();
$request->noRedirectHere();

$request->setPageTitle($this->T_('404 Error - page not found'));
echo <<<PAGE_404
<h3>{$this->T_('Sorry! Page not found')}</h3>
{$this->formatMessage($message)}
<p>
{$this->T_('This may be a problem with our system, and the issue has
been logged for investigation. Or it could be that you have an outdated
link.')}
</p>
<p>
{$this->T_('If you have any query you would like us to deal with,
please contact us')}
</p>
<p>

{$this->T_('The following items have some connection with the URI
you used to come here, so maybe they are what you were looking for?')}
</p>
PAGE_404;
echo $searchtext;
}
else echo $this->T_('This Aliro based web site is not yet configured
with user data, please call back later');
}
}

The test around most of the constructor code deals with the case where the URI is illegal only because the site has not been configured with any extensions yet. In all other cases, a message to the user is constructed explaining the problem that has occurred. Obviously, this needs to be specific to the circumstances of the site and its users.

An HTTP header showing the 404 error is sent, and the information about the problem is recorded in a database table dedicated to 404 errors, using the recordPageFail method in the parent class. The Aliro request object is used to set the header in the browser to show the 404 error. In order to be helpful to the user, the URI is passed to a search method, searchuri. This will succeed only in the case where the URI has been processed with a SEF (Search Engine Friendly) mechanism such that it contains text rather than numbers and terse symbols.

The searchuri method is not shown, but it pulls out the last part of the URI, separated by slashes, and converts everything that is not an alphabetic character into a space. The text resulting from this process is then used as if it had been submitted to a site search. This may result in a list of possible links into the site that fit with the URI given.

The class for handling 403 errors is very similar, although the text presented to the user is altered to reflect the different kind of error.

Summary

This article has reviewed the handling of the inevitable errors that go with software systems. Errors arising out of PHP code make up one important area, and database errors another. The special case of an invalid URI causing a "404 error" was taken into consideration. Situations where a "403 error" is appropriate were reviewed, arising when a request is not permitted.

We've devised mechanisms for dealing with all of them, usually recording a good deal of information, including an execution trace, to the database for diagnosis by a developer. By contrast, the user is given only a modest amount of information, so that they know that an error has happened. This is a choice based on avoiding confusion and also on securing the system from hostile interventions.


Further resources on this subject:

PHP 5 CMS Framework Development Expert insight and practical guidance to creating an efficient, flexible, and robust framework for a PHP 5-based content management system
Published: June 2008
eBook Price: $39.99
Book Price: $49.99
See more
Select your format and quantity:

About the Author :


Martin Brampton

Now primarily a software developer and writer, Martin Brampton started out studying mathematics at Cambridge University. He then spent a number of years helping to create the so-called legacy, which remained in use far longer than he ever expected. He worked on a variety of major systems in areas like banking and insurance, spiced with occasional forays into technical areas such as cargo ship hull design and natural gas pipeline telemetry.

After a decade of heading IT for an accountancy firm, a few years as a director of a leading analyst firm, and an MA degree in Modern European Philosophy, Martin finally returned to his interest in software, but this time transformed into web applications. He found PHP5, which fits well with his prejudice in favor of programming languages that are interpreted and strongly object oriented.

Utilizing PHP, Martin took on development of useful extensions for the Mambo (and now also Joomla!) systems, then became leader of the team developing Mambo itself. More recently, he has written a complete new generation CMS named Aliro, many aspects of which are described in this book. He has also created a common API to enable add-on applications to be written with a single code base for Aliro, Joomla (1.0 and 1.5) and Mambo.

All in all, Martin is now interested in many aspects of web development and hosting; he consequently has little spare time. But his focus remains on object oriented software with a web slant, much of which is open source. He runs Black Sheep Research, which provides software, speaking and writing services, and also manages web servers for himself and his clients.

Books From Packt


AJAX and PHP: Building Modern Web Applications 2nd Edition
AJAX and PHP: Building Modern Web Applications 2nd Edition

Object-Oriented Programming with PHP5
Object-Oriented Programming with PHP5

Magento 1.3: PHP Developer's Guide
Magento 1.3: PHP Developer's Guide

CakePHP Application Development
CakePHP Application Development

Agile Web Application Development with Yii1.1 and PHP5
Agile Web Application Development with Yii1.1 and PHP5

PHP 5 E-commerce Development
PHP 5 E-commerce Development

Expert PHP 5 Tools
Expert PHP 5 Tools

Mastering phpMyAdmin 3.1 for Effective MySQL Management
Mastering phpMyAdmin 3.1 for Effective MySQL Management


Code Download and Errata
Packt Anytime, Anywhere
Register Books
Print Upgrades
eBook Downloads
Video Support
Contact Us
Awards Voting Nominations Previous Winners
Judges Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software
Resources
Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software