Trouble Shooting Wiki

PHP, Database and Application Error Handling

From TroubleshootingWiki

Jump to: navigation, search
PHP 5 CMS
Official Page
Project Documentation
Download
Source Book
200px-1847193579.jpg
ISBN 978-1-847193-57-5
Publisher Packt Publishing
Author(s) Martin Brampton

In an ideal world software would never experience errors but we don't live in an ideal world! So we need to consider what to do when errors arise. One option is to simply leave PHP5 to do its best, but when the issues are considered, that doesn't look a good choice.

What are our concerns over errors? Perhaps the overriding issue here has to be that in case of an error we need the software to degrade gracefully and not damage the system. Another consideration for web software is that errors should not provide information or opportunities that will aid crackers any more than can be helped.

Errors create problems for developers. One is that in the nature of the web, errors are often not reported. People simply give up and do something else. Web software is often written quickly, and it is surprising how many errors exist in released software. Other factors for developers are that error handling can be a big overhead; also it is often unclear what counts as a good way to deal with errors.

Given this range of issues, it is clear that it will be helpful if the CMS framework can contribute useful functionality for error handling. Also included here for convenience is the special processing that takes place when a URI does not correspond to any page in our site, thus demanding a "404 error".

Contents

[edit] The Problem

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

  • rapping different kinds of errors
  • aking a record of errors with sufficient detail to aid analysis
  • upporting a structure that mitigates the effect of errors

[edit] 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 sometime 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?

[edit] 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 a 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 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 simple message that it has been noted for rectification.

[edit] 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 has 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.

[edit] 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 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. For example, 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.

[edit] 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 to 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:

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)

where 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.

[edit] Handling Database Errors

As discussed earlier, 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, aliroRequest::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 aliroRequest, 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 above. 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.

[edit] Page 404 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, that 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.

The error can be detected at various possible points, but once detected, Aliro deals with it by use of a dedicated aliroPage404 class. A much simplified version of the code of the class is shown here:

class aliroPage404
 {
 public function __construct ()
 {
 if (aliroComponentHandler::getInstance()->componentCount() AND aliroMenuHandler::getInstance()->getMenuCount('mainmenu'))
 {
 header ('HTTP/1.1 404 Not Found');
 $this->record404();
 $searchtext = $this->searchuri();
 aliroRequest::getInstance()->setPageTitle(T_ ('404 Error - page not found'));
 echo <<<PAGE_404

[edit] Sorry! Page not found

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.

This page is also presented for an item that exists but is not available to you. If you are not logged in, you might like to log in and try again.

If you have any query you would like us to deal with, please use the CONTACT US facility from the main menu.

The following items have some connection with the URI you used to come here, so maybe they are what you were looking for?

 PAGE_404;
 echo $searchtext;
 }
 else echo T_('This Aliro based website is not yet configured with user data, please call back later');
 }

 private function record404 ()
 {
 $uri = $_SERVER['REQUEST_URI'];
 $timestamp = date ('Y-m-d H:i:s');
 $referer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : ;
 $post = base64_encode(serialize($_POST));
 $trace = aliroRequest::trace();
 aliroCoreDatabase::getInstance()->doSQL("INSERT INTO #__error_404 (uri, timestamp, referer, post, trace)
 VALUES ('$uri', '$timestamp', '$referer', '$post', '$trace') ON DUPLICATE KEY UPDATE timestamp = 
'$timestamp', referer='$referer', post='$post', trace='$trace'");
 }

 private function searchuri ()
 {
 $uri = $_SERVER['REQUEST_URI'];
 $bits = explode ('/', $uri);
 for ($i=count($bits); $i>0; $i--)
 {
 $bit = $bits[$i-1];
 if ($bit) break;
 }
 $searchword = preg_replace('/[^A-Za-z]/', ' ', $bit);
 ...
 }

 }

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 recorded in a database table dedicated to 404 errors. 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 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.

An improvement that needs to be made to the Aliro 404 processing is to also record the supposed IP address from which the request was made. A significant number of 404 errors are the result of hacking attempts, and although the IP address may well be faked or belong to a compromised machine, if a large number are indicating the same source it may be desirable to block the IP address using a facility such as the web server's firewall.

[edit] Summary

This tutorial 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 also taken into consideration.

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.

[edit] Source

The source of this content is Chapter 13: Error Handling of PHP 5 CMS Framework Development by Martin Brampton (Packt Publishing, 2008).

Personal tools