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/browsel/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, anddoesn'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">The useful additional information is the output of a call to the
<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>
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 PUBLICAs we stated earlier, the customHandler( )
"-//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>
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 inYou can't change this behavior.[1] Custom handlers work
/usr/local/apache2/htdocs/example.12-1.php on line 46
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-errorsl
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 parameterthen the handler outputs:
print "Two times ten is: " . double( );
[PHP Error 20030616104153]E_WARNING on line 67 in bug.php.The backTrace( )
[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
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 calledthis
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 linewith 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!In practice, we recommend logging to a file and monitoring the file.
error_log($error, 1, "hugh@asdfgh.com");
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)You can use this handler for several different purposes. For example,
{
$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( );
}
if a MySQL connection fails, you can report an error and halt the
script:
// Connect to the MySQL serverYou can also send error codes and messages through to the handler
if (!($connection = @ mysql_connect($hostname, $username, $password)))
trigger_error("Could not connect to DBMS", E_USER_ERROR);
that are reported as the error string:
if (!(mysql_select_db($databaseName, $connection)))You could even use this to log security or other problems. For
trigger_error(mysql_errno( ) . " : " . mysql_error( ), E_USER_ERROR);
example, if the user fails to log in with the correct password, you
could store a NOTICE:
if ($password != $storedPassword)We use trigger_error( ) extensively for error
trigger_error("Incorrect login attempt by {$username}", E_USER_NOTICE);
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.