12.3 Custom Error Handlers

The errors produced by PHP are useful when developing scripts, but aren't sufficient for deployment in a web database application. Errors should inform users without confusing them, not expose secure internal information, report details to administrators, and have a look and feel consistent with the application. This section shows you how to add a professional error handler to your application, and also how to improve the internal PHP error handler to produce even more information during development.

If you're not keen to develop a custom handler (or don't want to use ours!), you'll find an excellent class that includes one at http://www.phpclasses.org/browse.html/package/345.

12.3.1 A Basic Custom Handler

To begin, we show you how to implement a simple custom handler. The set_error_handler( ) function allows you to define a custom error handler that replaces the internal PHP handler for non-critical errors:


string set_error_handler(string error_handler)

The function takes one parameter, a user-defined error_handler function that is called whenever an error occurs. On success, the function returns the previously defined error handler function name, which can be saved and restored later with another call to set_error_handler( ). The function returns false on failure.

The custom error handler is not called for the following errors: E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, and E_COMPILE WARNING. For these, the PHP internal error handler is always used.

For example, to set up a new error handler that's defined in the function customHandler( ), you can register it with:

set_error_handler("customHandler");

The function name is passed as a quoted string, and doesn't include the brackets. After the new handler is defined, the error_reporting level in php.ini or defined in the script with error_reporting( ) has no effect: all errors are either passed to the custom handler or, if they're critical, to the PHP internal default handler. We discuss this more later.

A custom error handler function must accept at least two parameters: an integer error number and a descriptive error string. Three additional optional parameters can be also be used: a string representing the filename of the script that caused the error; an integer line number indicating the line in that file where the error was noticed; and, an array of additional variable context information.

Our initial implementation of the customHandler( ) function is shown in Example 12-1. It supports all five parameters, and uses them to construct an error string that displays more information than the default PHP internal handler. It handles only E_NOTICE and E_WARNING errors, and ignores all others.

After running the example, the handler outputs the following:

<hr><font color="red">

<b>Custom Error Handler -- Warning/Notice<b>

<br>An error has occurred on 38 line in the

  /usr/local/apache2/htdocs/example.12-1.php file.

<br>The error is a "Missing argument 1 for double( )" (error #2).

 <br>Here's some context information:<br>

<pre>

Array

(

    [number] => 

)

</pre></font>

<hr>

The useful additional information is the output of a call to the print_r( ) that dumps the state of all variables in the current context. In this case, there's only one variable which doesn't have a value: that's not surprising, because the warning is generated because the parameter is missing!

The context information is extracted from the fifth, array parameter to the customHandler( ) function. It contains as elements all of the variables that are in the current scope when the error occurred. In our Example 12-1, only one variable was in scope within the function, $number. If the customHandler( ) function is called from outside of all functions (in the main body of the program), it shows the contents of all global variables including the superglobals $_GET, $_POST, and $_SESSION.

Example 12-1. A script with a custom error handler
<!DOCTYPE HTML PUBLIC

                 "-//W3C//DTD HTML 4.01 Transitional//EN"

                 "http://www.w3.org/TR/html401/loose.dtd">

<html>

<head>

  <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">

  <title>Error</title>

<body>

<h1>Two times!</h1>

<?php

function customHandler($number, $string, $file, $line, $context)

{

  switch ($number)

  {

     case E_WARNING:

     case E_NOTICE:

       print "<hr><font color=\"red\">\n";

       print "<b>Custom Error Handler -- Warning/Notice<b>\n";

       print "<br>An error has occurred on {$line} line in 

              the {$file} file.\n";

       print "<br>The error is a \"{$string}\" (error #{$number}).\n ";

       print "<br>Here's some context information:<br>\n<pre>\n";

       print_r($context);

       print "\n</pre></font>\n<hr>\n";

       break;

     default:

        // Do nothing

  }

}



function double($number)

{

  return $number*2;

}



set_error_handler("customHandler");



// Generates a warning for a missing parameter

print "Two times ten is: " . double( );

?>

</body>

</html>

As we stated earlier, the customHandler( ) function isn't called for the critical error types. For example, if we omit the semi-colon from the end of the first print statement:

print "Two times ten is: " . double( )

then the parse error that's output is the PHP default:

Parse error: parse error, unexpected T_PRINT in

  /usr/local/apache2/htdocs/example.12-1.php on line 46

You can't change this behavior.[1] Custom handlers work only for the E_WARNING and E_NOTICE errors, and for the entire USER class. The techniques to generate USER class errors are discussed in the next section.

[1] This isn't strictly true. It isn't possible to change the behavior within your scripts or in the php.ini file. However, it is possible to force all output produced by your script through a function, and to catch them after they've been output; this has a significant performance penalty. See http://www.webkreator.com/php/configuration/handling-fatal-and-parse-errors.html for detailed information.

The custom handler we've shown here deliberately doesn't support USER class errors. If, for example, an E_USER_ERROR is generated, the handler is called, but nothing is output and the script doesn't stop. It's the responsibility of the programmer to deal with all error types, and to stop or continue the execution as appropriate. We develop a handler for all errors in the next section.

12.3.2 A Production Error Handler

The simple custom error handler in the previous section has several disadvantages:

  • The handler offers only slightly more information than the PHP internal handler. Ideally, it should also include a backtrace, showing which function called the one containing the error, and so on back to the beginning of the script.

  • It shows technical information to the user, which is both confusing and a security risk. It should explain to the user that there's a problem with their request, and then log or send the technical information to someone who can fix it.

  • It can't handle programmer-generated errors. For example, in Chapter 6, we've used the showerror( ) function to handle database server errors. These errors should be integrated with our custom handler.

  • Our handler doesn't stop script execution, and doesn't leave the application in a known state. For example, if a session is open or the database is locked, the error handler doesn't clean these up.

In this section, we improve our custom handler to address these problems.

12.3.2.1 Including debugging information

Example 12-2 shows an improved error handler that reports more information about how and where the error occurred. For example, if an E_WARNING error is generated by the fragment:

// Generates a warning for a missing parameter

print "Two times ten is: " . double( );

then the handler outputs:

[PHP Error 20030616104153]E_WARNING on line 67 in bug.php. 

[PHP Error 20030616104153]Error: "Missing argument 1 for double( )" 

  (error #2). 

[PHP Error 20030616104153]Backtrace: 

[PHP Error 20030616104153] 0: double (line 67 in bug.php) 

[PHP Error 20030616104153] 1: double (line 75 in bug.php) 

[PHP Error 20030616104153]Variables in double ( ): 

[PHP Error 20030616104153] number is NULL 

[PHP Error 20030616104153]Client IP: 192.168.1.1

The backTrace( ) function uses the PHP library function debug_backtrace( ) to show a call graph, that is, the hierarchy of functions that were called to reach the function containing the bug. In this example, call #1 was from the main part of the script (though this is shown as a call from double( ), which is the function name that was called?this is a bug in debug_backtrace( )) and call #0 was the double( ) function that caused the error.

The debug_backtrace( ) function stores more details than the function name, but they are in a multidimensional array. If you're interested in using the function directly, try adding the following to your code:

var_dump(debug_backtrace( ));

Our custom handler also includes the following fragment:

$prepend = "\n[PHP Error " . date("YmdHis") . "]";

$error = ereg_replace("\n", $prepend, $error);

This replaces the carriage return at the beginning of each error line with a fragment that includes the date and time. Later in this section, we write this information to an error log file.

Example 12-2. A custom handler with a backtrace
<?php

function backTrace($context)

{

   // Get a backtrace of the function calls

   $trace = debug_backtrace( );



   $calls = "\nBacktrace:";



   // Start at 2 -- ignore this function (0) and the customHandler( ) (1)

   for($x=2; $x < count($trace); $x++)

   {

     $callNo = $x - 2;

     $calls .= "\n  {$callNo}: {$trace[$x]["function"]} ";

     $calls .= "(line {$trace[$x]["line"]} in {$trace[$x]["file"]})";

   }



   $calls .= "\nVariables in {$trace[2]["function"]} ( ):";



   // Use the $context to get variable information for the function

   // with the error

   foreach($context as $name => $value)

   {

     if (!empty($value))

       $calls .= "\n  {$name} is {$value}";

     else

       $calls .= "\n  {$name} is NULL";

   }

   return ($calls);

}



function customHandler($number, $string, $file, $line, $context)

{

  $error = "";



  switch ($number)

  {

     case E_WARNING:

       $error .= "\nE_WARNING on line {$line} in {$file}.\n";

       break;

     case E_NOTICE:

       $error .= "\nE_NOTICE on line {$line} in {$file}.\n";

       break;

     default:

       $error .= "UNHANDLED ERROR on line {$line} in {$file}.\n";

  }

  $error .= "Error: \"{$string}\" (error #{$number}).";

  $error .= backTrace($context);

  $error .= "\nClient IP: {$_SERVER["REMOTE_ADDR"]}";



  $prepend = "\n[PHP Error " . date("YmdHis") . "]";

  $error = ereg_replace("\n", $prepend, $error);



  // Output the error as pre-formatted text

  print "<pre>{$error}</pre>";

  // Log to a user-defined filename

  // error_log($error, 3, "/home/hugh/php_error_log");



}

12.3.2.2 Logging and notifying the user

Output of errors to the user agent (usually a web browser) is useful for debugging during development but shouldn't be used in a production application. Instead, you can use the PHP library error_log( ) function to log to an email address or a file. Also, you should alert the user of actions they can take, without providing them with unnecessary technical information.

The error_log( ) function has the following prototype:


int error_log (string message, int message_type [, string destination [, string extra_headers]])

The string message is the error message to be logged. The message_type can be 0, 1, or 3. A setting of 0 sends the message to the PHP system's error logger, which is configured using the error_log directive in the php.ini file. A setting of 1 sends an email to the destination email address with any additional email extra_headers that are provided. A setting of 3 appends the message to the file destination. A setting of 2 isn't available.

In practice, you should choose between logging to an email address or to a user-defined file; it's unlikely that the web server process will have permissions to write to the system error logger. To log to a file using our customHandler( ) in Example 12-2, uncomment the statement:

error_log($error, 3, "/home/hugh/php_error_log");

This will log to whatever is set as the logging destination by the third parameter; in this example, we're writing into a file in the administrator's home directory. You could use the directory C:\Windows\temp on a Microsoft Windows platform. If you'd prefer that errors arrive in email, replace the error_log( ) call with:

// Use a real email address!

error_log($error, 1, "hugh@asdfgh.com");

In practice, we recommend logging to a file and monitoring the file. Receiving emails might sound like a good idea, but in practice if the DBMS is unavailable or another serious problem occurs, you're likely to receive hundreds of emails in a short time.

When the application goes into production, we also recommend removing the print statement that outputs messages to the browser. Instead, you should add a generic message that alerts the user to a problem and asks them contact the system administrator. You might also follow these statements with a call to die( ) to stop the program execution; remember, it's up to you whether you stop the program when an error occurs.

A better approach than adding print statements to show the error to the user is to create a template with the same look and feel as your application, and include the error messages there; we use this approach in our online winestore in later chapters. This approach also has the additional advantage that it prevents the problem we describe next.

An additional problem with printing errors without a template is that they can still appear anywhere in a partial page. This can lead to user confusion, produce non-compliant HTML, and look unattractive. If you use a template, you can choose whether to output the page or not: nothing is output until you call the show( ) method. However, even without a template, it's possible to prevent this happening by using the PHP library output buffering library.

The output buffering approach works as shown in the simplified error handler in Example 12-3. The call to ob_start( ) at the beginning of the script forces all output to be held in a buffer. When an error occurs, the ob_end_clean( ) function in the customHandler( ) function throws away whatever is in the buffer, and then outputs only the error message and stops the script. If no errors occur, the script runs as normal and the ob_end_flush( ) function outputs the document by flushing the buffer. With this approach, partial pages can't occur.

Example 12-3. Using output buffering to prevent partial output pages
<?php

  // start buffering

  ob_start( );

?>

<!DOCTYPE HTML PUBLIC

                 "-//W3C//DTD HTML 4.01 Transitional//EN"

                 "http://www.w3.org/TR/html401/loose.dtd">

<html>

<head>

  <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">

  <title>Error</title>

<body>

<?php

function customHandler($number, $string, $file, $line, $context)

{

  // Throw away the current buffer

  ob_end_clean( );



  print "An error occurred!";

  die( );

}



set_error_handler("customHandler");



// Generates an E_NOTICE

print $a;



// Output the buffer

ob_end_flush( );



?>

</body>

</html>

12.3.2.3 Triggering your own errors

In Chapter 6, we triggered our own errors by calling the showerror( ) function, which outputs MySQL error messages. We added our own calls to die( ) to handle PEAR DB errors in Chapter 7. However, these approaches aren't consistent with using the custom error handler we've built in this chapter. Now that we have an error handler, it would be useful to be able to trigger its use through programmer-generated errors. This is where the USER class of errors and the PHP library function trigger_error( ) are useful:


void trigger_error (string error_message [, int error_type])

The function triggers a programmer-defined error using two parameters: an error_message and an optional error_type that's set to one of E_USER_ERROR, E_USER_WARNING, or E_USER_NOTICE. The function calls the current error handler, and provides the same five parameters as other PHP error types.

Example 12-4 is a modified handler that processes errors generated by trigger_error( ). In addition, it stops the script when WARNING or ERROR class errors occur.

Example 12-4. A custom error handler that supports programmer-generated errors
function customHandler($number, $string, $file, $line, $context)

{

  $error = "";



  switch ($number)

  {

     case E_USER_ERROR:

       $error .= "\nERROR on line {$line} in {$file}.\n";

       $stop = true;

       break;

     case E_WARNING:

     case E_USER_WARNING:

       $error .= "\nWARNING on line {$line} in {$file}.\n";

       $stop = true;

       break;

     case E_NOTICE:

     case E_USER_NOTICE:

       $error .= "\nNOTICE on line {$line} in {$file}.\n";

       $stop = false;

       break;

     default:

       $error .= "UNHANDLED ERROR on line {$line} in {$file}.\n";

       $stop = false;

  }

  $error .= "Error: \"{$string}\" (error #{$number}).";

  $error .= backTrace($context);

  $error .= "\nClient IP: {$_SERVER["REMOTE_ADDR"]}";



  $prepend = "\n[PHP Error " . date("YmdHis") . "]";

  $error = ereg_replace("\n", $prepend, $error);



  // Throw away the buffer

  ob_end_clean( );



  print "<pre>{$error}</pre>";

  // Log to a user-defined filename

  // error_log($error, 3, "/home/hugh/php_error_log");



  if ($stop == true)

    die( ); 

}

You can use this handler for several different purposes. For example, if a MySQL connection fails, you can report an error and halt the script:

// Connect to the MySQL server

if (!($connection = @ mysql_connect($hostname, $username, $password)))

   trigger_error("Could not connect to DBMS", E_USER_ERROR);

You can also send error codes and messages through to the handler that are reported as the error string:

if (!(mysql_select_db($databaseName, $connection)))

 trigger_error(mysql_errno( ) . " : " . mysql_error( ), E_USER_ERROR);

You could even use this to log security or other problems. For example, if the user fails to log in with the correct password, you could store a NOTICE:

if ($password != $storedPassword)

  trigger_error("Incorrect login attempt by {$username}", E_USER_NOTICE);

We use trigger_error( ) extensively for error reporting in the online winestore in Chapter 16 through Chapter 20.

12.3.2.4 Cleaning up the application

An advantage of a custom error handler is that you can add additional features to gracefully stop the application when an error occurs. For example, you might delete session variables, close database connections, unlock a database table, and log out the user. What actions are carried out is dependent on the application requirements, and we don't discuss this in detail here. However, our online winestore error handler in Chapter 16 carries out selected cleanup actions based on the state of session variables, and leaves the application in a known state.



     
    ASPTreeView.com
     
    Evaluation has јЩєГБexpired.
    Info...