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:
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) ... }
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 display_errors is set to On for development purposes, the error is displayed in the content area:
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:
If you do not 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:
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); ...