7 Error handling

Error handling in web applications should occur at many levels, protecting against everything from invalid user input right through to database errors. To make the user experience smooth, PHP errors should never be displayed to the web user. They should be captured in mid-tier log files and the user should instead be given the chance to retry or do another task. In a production system the php.ini display_errors setting should be Off.

This chapter contains the following topics:

Database Errors

At the database level, it is recommended to check all PHP OCI8 errors.

In ac_db.inc.php, currently the only error checking occurs at connection time in __construct():

        ...
        if (!$this->conn) {
            $m = oci_error();
            throw new \Exception('Cannot connect to database: ' . $m['message']);
        }
        ...

The oci_error() function returns an associative array, one element of which includes the text of the Oracle error message.

Left as an extra exercise for the reader is to improve the error handling in the Db class. The rest of this tutorial is not dependent on any changes in this regard. Evaluate each PHP OCI8 call and decide where to check return values. Call oci_error() to get the text of the message. For a connection error, do not pass an argument to oci_error(), as shown above. Unlike connection errors where oci_error() takes no argument, to check errors from oci_parse() pass the connection resource to oci_error():

$stid = oci_parse($conn, $sql);
if (!$stid) {
    $m = oci_error($conn)
    ...
}

For oci_execute() errors pass the statement handle:

$r = oci_execute($stid);
if (!$r) {
    $m = oci_error($stid)
    ...
}

Displaying a Custom Error Message

Simulate an error in ac_show_equip.php by editing getempname() and throwing an exception in printcontent(). PHP will give a run time error when it reaches that call:

function printcontent($sess, $empid) {
    echo "<div id='content'>\n";
    $db = new \Oracle\Db("Equipment", $sess->username);
    $empname = htmlspecialchars(getempname($db, $empid), ENT_NOQUOTES, 'UTF-8');
    echo "$empname has: ";
 
    throw new Exception;
 
    $sql = "BEGIN get_equip(:id, :rc); END;";
 
    ...

Run the application in a browser and click the Show link for Steven King. Because we set display_errors to On for development purposes we see the error displayed in the content area:

A nice error message

The error is printed after the initial part of the page showing the user name is printed. In a production site with display_errors set to Off, the user would see just this partial section content being displayed, which is not ideal. To prevent this, PHP's output buffering can be used.

Edit ac_show_equip.php and modify where printcontent() is called. Wrap the call in a PHP try-catch block, changing it to:

...
$page->printMenu($sess->username, $sess->isPrivilegedUser());
ob_start();
try {
    printcontent($sess, $empid);
} catch (Exception $e) {
    ob_end_clean();
    echo "<div id='content'>\n";
    echo "Sorry, an error occurred";
    echo "</div>";
}
ob_end_flush();
$page->printFooter();
...

The ob_start() function captures all subsequently generated output in a buffer. Other PHP ob_* functions allow that buffer to be discarded or flushed to the browser. In the code above, the ob_end_clean() call in the exception handler will discard the "Steven King has:" message so a custom error message can be printed.

Run the application again to see the following error:

A nice error message

If you don't like using object-oriented code, an alternative to throwing and catching an exception would be to return a boolean from printcontent() and handle the error manually. If you want to stop execution you can use PHP's trigger_error().

Edit the printcontent() function in ac_show_equip.php and change the temporary line:

    throw new Exception;

to

    trigger_error('Whoops!', E_USER_ERROR);

To catch and handle PHP errors like E_USER_ERROR, you can use PHP's set_error_handler() function which allows an error handler function to be registered.

At the top of ac_show_equip.php add a call to set_error_handler():

...
session_start();
set_error_handler("ac_error_handler");
 
require('ac_db.inc.php');
require('ac_equip.inc.php');
...

Also add the called function:

/**
 * Error Handler
 *
 * @param integer $errno Error level raised
 * @param string $errstr Error text
 * @param string $errfile File name that the error occurred in
 * @param integer $errline File line where the error occurred
 */
function ac_error_handler($errno, $errstr, $errfile, $errline) {
    error_log(sprintf("PHP AnyCo Corp.: %d:  %s in %s on line %d",
            $errno, $errstr, $errfile, $errline));
    header('Location: ac_error.html');
    exit;
}

This records the message in the Apache log file and redirects to an error page. Create that error page in a new HTML file ac_error.html:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
       "http://www.w3.org/TR/html4/loose.dtd">
<html>
<!-- ac_error.html: a catch-all error page -->
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title></title>
</head>
 
<body bgcolor="#ffffff">
<h1>AnyCo Corp. Error Page</h1>
<p>We're sorry an error occurred.<p>
<p><a href="index.php">Login</a></p>
 
</body>
</html>

Run the application and login. Click Show to see an employee's equipment. The error page is shown:

A nice error message

Locate the Apache error log on your system, for example in /var/log/httpd/error_log on Oracle Linux. The log will contain message generated by PHP:

[Wed Apr 27 13:06:09 2011] [error] [client 127.0.0.1] PHP AnyCo Corp.: 256:
Whoops! in /home/chris/public_html/ACXE/ac_show_equip.php on line 71, referer:
http://localhost/~chris/ACXE/ac_emp_list.php

Remove or comment out the temporary trigger_error() call in printcontent() before continuing with the next chapter.

...
// trigger_error('Whoops!', E_USER_ERROR);
...