A simple error handler for PHP
What is the best way to handle your PHP application errors? There is no single good answer to that question. It all depends on how your application works, what environment the application runs in and also how maintenance is performed.
But one thing is the same for every application. You might already know that showing errors in your application to clients is not a very good practice. It’s a security risk because users can learn about the internals of your server and your application. Besides that, PHP error messages do not mean anything to a user of your application so it is pointless to show them what is wrong internally.
Alternatively you should use try and catch constructs to show friendly error messages to the users on the client side of your application if errors should be displayed to the user when they occur.
1 2 3 | try { } catch() { } |
You can use trigger_error() with the predefined error types E_USER_* to trigger your own application errors. For example to trigger PHP errors when your application encounters a MySQL error:
1 2 | $query = 'SELECT YEAR(NOW())'; $result = mysql_query($query) or trigger_error(mysql_error().' - '.$query, E_USER_ERROR)); |
This way MySQL errors will be visible for you, but you do not give away anything about your database model to clients.
To see which predefined error constants there are available, see: http://www.php.net/manual/en/errorfunc.constants.php. A useful thing to note is that triggering notes or warnings does not stop the script from executing further. If you need to break the script and stop it from continuing, trigger an error.
So what can you do to get notified of PHP errors yourself, without having to keep up with the apache error log while not displaying any errors in the browser on the client side on your production application?
I’ve created a simple error handler that can replace or accompany the default PHP error handler.
This is a useful way of getting notified of errors, because you will get the errors in your mailbox directly when the error occurs. So you will know an error has occurred before a user can even send a bug report. This is not a solution for all situations, but it can sure be useful in some cases to get notified of errors you would otherwise miss, or would not be reported by users. Another useful feature is that you will receive the state of variables at the moment the error occurs. This can be immensely useful for debugging.
Here is the full error handler file (error_handler.php). You can put it into a PHP include directory so you can include it anywhere, or you can put it in one of the directories of your application.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 | <?php /* * Just a simple error handler. * It sends mails now, but can easily be modified to store the errors in a database instead of sending mails. * * It optionally requires a database table for error checksums, this way you only get each error only once in your mailbox. * * It's not very prety yet, but it is functional. * * @author Oxidiser * @version 2.0 * CREATE TABLE `error_checksums` ( `checksum` varchar(32) collate utf8_unicode_ci NOT NULL, PRIMARY KEY (`checksum`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; */ // Mail where error e-mails should end up. define('ERROR_REPORT_EMAIL', "someemailaddress@somedomain.com"); // Define if the original PHP error handler should also handle the error. define('PHP_ERROR_HANDLER_ACTIVE', false); // Should the error handler use checksums to prevent sending dulicate error messages? define('USE_DATABASE', false); // MySQL db options define('MYSQL_HOST', "localhost"); define('MYSQL_USER', ""); define('MYSQL_PASSWORD', ""); define('MYSQL_DATABASE', ""); // Never display any errors to clients. ini_set('display_errors', 0); // Handle every error type. Good for test enviroment. error_reporting(E_ALL); // Handle every error type except notices and strict warnings. Good for the production enviroment. //error_reporting(E_ALL & ~ E_NOTICE | E_STRICT); // If error handler is not yet registered. if(!is_callable('lookup_ErrorHandler')) { /** * Fetches an instance of the ErrorHandler object and calls the handleError() method; * * @param integer $errNo Contains the level of the error raised, as an integer. * @param string $errMsg Contains the error message, as a string. * @param string $fileName Contains the filename that the error was raised in, as a string. * @param integer $lineNum Contains the line number the error was raised at, as an integer. * @param array $vars An array that points to the active symbol table at the point the error occurred. In other words, errcontext will contain an array of every variable that existed in the scope the error was triggered in. User error handler must not modify error context. * @return void * */ function lookup_ErrorHandler ($errNo, $errMsg, $fileName, $lineNum, $vars) { // Run trough all error types. // To disable one of the error types, just comment out that case. // It might be useful to disable the notice error levels for sloppy programmers for instance. switch($errNo) { case E_ERROR: case E_WARNING: case E_PARSE: case E_USER_ERROR: case E_USER_WARNING: case E_STRICT: case E_COMPILE_ERROR: case E_COMPILE_WARNING: case E_CORE_ERROR: case E_CORE_WARNING: case E_RECOVERABLE_ERROR: case E_NOTICE: case E_USER_NOTICE: ErrorHandler::getInstance(ERROR_HANDLER_SUBJ_APPEND)->handleError($errNo, $errMsg, $fileName, $lineNum, $vars); break; default: // Error type should not be handled. Do nothing. break; } // Do or don't execute PHP internal error handler, return false if it should execute the PHP error handler, true if it should not. return !PHP_ERROR_HANDLER_ACTIVE; } } /** * This class replaces the default PHP error handler. * * @param string $subjAppend This should contain the prefix for the e-mail subject. Useful for sorting e-mails by website when using the same error handler, when you for instance, place the error handler in an include directory. * @version 1.0 * @author Oxidiser * */ class ErrorHandler { /* * @var mixed (array with strings containing every error encountered during script execution) */ public $errorsARR; /* * @var string A variable trace. */ public $vars; /* * @var string (containing a string to prepend the error subject e-mail) */ public $subjectAppend; /* * @var ErrorHandler */ private static $instance; /* * @var MySQL link identifier */ protected $_errorDb; /** * Create a new instance of the ErrorHandler object. * * @param string $subjAppend The string that should be appended to the subject. * @return void */ private function __construct($subjAppend) { $this->errormail = ERROR_REPORT_EMAIL; $this->subjectAppend = $subjAppend; $this->errortype = array(E_ERROR => 'Error', E_WARNING => 'Warning', E_PARSE => 'Parsing Error', E_NOTICE => 'Notice', E_CORE_ERROR => 'Core Error', E_CORE_WARNING => 'Core Warning', E_COMPILE_ERROR => 'Compile Error', E_COMPILE_WARNING => 'Compile Warning', E_USER_ERROR => 'User Error', E_USER_WARNING => 'User Warning', E_USER_NOTICE => 'User Notice', E_STRICT => 'Runtime Notice', E_RECOVERABLE_ERROR => 'Catchable Fatal Error' ); $this->errorsARR = array(); if(USE_DATABASE) { // Database should be used, connect to MySQL instance. $this->_errorDb = mysql_connect(MYSQL_HOST, MYSQL_USER, MYSQL_PASSWORD); // Select the apropriate database. mysql_select_db(MYSQL_DATABASE, $this->_errorDb); } } /** * Clone the object (required for the singleton) * * @return void */ private function __clone () {} /** * Returns the instance of the ErrorHandler object. * * @param string $subjAppend This should contain the prefix for the e-mail subject. Usefull for sorting e-mails by website. * @return ErrorHandler */ public static function getInstance ($subjAppend) { if(!ErrorHandler::$instance instanceof self) { ErrorHandler::$instance = new self($subjAppend); } return ErrorHandler::$instance; } /** * This mehtod actually handles the errors recieved from the PHP core. * * @param integer $errNo Contains the level of the error raised, as an integer. * @param string $errMsg Contains the error message, as a string. * @param string $fileName Contains the filename that the error was raised in, as a string. * @param integer $lineNum Contains the line number the error was raised at, as an integer. * @param array $vars An array that points to the active symbol table at the point the error occurred. In other words, errcontext will contain an array of every variable that existed in the scope the error was triggered in. User error handler must not modify error context. * @return void * */ public function handleError($errNo, $errMsg, $fileName, $lineNum, $vars) { if(USE_DATABASE) { // Use database for checksums. // Check checksum in the hope of reducing server load by only mailing errors once. $errorChecksum = md5($errNo . $errMsg . $fileName . $lineNum); // Check if checksum exists in database. // @todo What is faster? Check with a select statement like it does now, or only do an insert query and check for an exception (mysql error, primairy key already exists)? $q = mysql_query("SELECT checksum FROM error_checksums WHERE checksum = '{$errorChecksum}'", $this->_errorDb) or trigger_error('Something went wrong with the error handler checksum check query: '.mysql_error(), E_USER_ERROR); if(mysql_num_rows($q) > 0) { // Error already mailed. Return without adding error to array in object and adding checksum to the database. return 0; } // Add checksum to database. mysql_query("INSERT INTO error_checksums (checksum) VALUES ('{$errorChecksum}')", $this->_errorDb) or trigger_error('Something went wrong with the error handler checksum insert query: '.mysql_error(), E_USER_ERROR); } // Get vars from thrown error. $this->vars = $vars; // Get the current date. $dt = date("Y-m-d H:i:s (T)"); // Get the next empty key (auto increment). $nextKey = count($this->errorsARR); // get defined vars. $this->errorsARR[$nextKey]['date'] = $dt; $this->errorsARR[$nextKey]['errno'] = $errNo; $this->errorsARR[$nextKey]['errtype'] = $this->errortype[$errNo]; $this->errorsARR[$nextKey]['errmessage'] = $errMsg; $this->errorsARR[$nextKey]['script'] = $fileName; $this->errorsARR[$nextKey]['url'] = 'http://'.$_SERVER['SERVER_NAME'].($_SERVER['PHP_SELF'] ? $_SERVER['PHP_SELF'] : ($_REQUEST['SCRIPT_NAME'] ? $_REQUEST['SCRIPT_NAME'] : ($_ENV['SCRIPT_NAME'] ? $_ENV['SCRIPT_NAME'] : ($PHP_SELF ? $PHP_SELF : 'oopsnoscript')))) . ($_SERVER['QUERY_STRING'] ? '?' . $_SERVER['QUERY_STRING'] : ''); $this->errorsARR[$nextKey]['lineno'] = $lineNum; // Get the state of the superglobals at the point of the error. $this->errorsARR[$nextKey]['session'] = $_SESSION; $this->errorsARR[$nextKey]['request'] = $_REQUEST; if(USE_DATABASE) { $this->errorsARR[$nextKey]['checksum'] = $errorChecksum; } if(isset($_SERVER['REMOTE_ADDR'])) { // Remote address is set, so store the client IP. $this->errorsARR[$nextKey]['user'] = $_SERVER['REMOTE_ADDR']; } else { // Unknown remote address. $this->errorsARR[$nextKey]['user'] = "Unknown"; } // Extract the filename. $tmpArr = explode('/', $fileName); $errorFileName = array_pop($tmpArr); // Set the subject for this errors e-mail. $this->errorsARR[$nextKey]['subject'] = $this->errortype[$errNo] . ' in ' . $errorFileName. ' line #' . $lineNum; } /** * Destructor for the object. Sends (unique if checksums are used) errors once for each script execution at the end of execution. * * @return void */ function __destruct () { // Check if any errors were triggered during execution. if (count($this->errorsARR) > 0) { // Set headers for error mails. $headers = "From: \"somesite.nl Error reporter\" <errorhandler@somesite.nl>\n"; $headers .= "Return-Path: <errorhandler@somesite.nl>\n"; $headers .= "MIME-Version: 1.0\n"; $headers .= "Content-Type: text/HTML; charset=ISO-8859-1\n"; // Go through each error that was encountered. foreach($this->errorsARR as $err) { // Create content for error mail. $content = "<table width=\"100%\" style=\"border:1px solid;\">"; $content .= "<tr><td style=\"border:1px solid; background-color:#9FC2FF;\" colspan=\"2\"><strong>Message & Vitals</strong></td></tr>"; $content .= "<tr><td style=\"border:1px solid;\"><strong>Error #</strong></td><td style=\"border:1px solid;\">{$err['errno']}</td></tr>"; $content .= "<tr><td style=\"border:1px solid;\"><strong>Error type</strong></td><td style=\"border:1px solid;\">{$err['errtype']}</td></tr>"; $content .= "<tr><td style=\"border:1px solid;\"><strong>Error message</strong></td><td style=\"border:1px solid;\">{$err['errmessage']}</td></tr>"; $content .= "<tr><td style=\"border:1px solid;\"><strong>Date & time</strong></td><td style=\"border:1px solid;\">{$err['date']}</td></tr>"; $content .= "<tr><td style=\"border:1px solid;\"><strong>URL</strong></td><td style=\"border:1px solid;\">{$err['url']}</td></tr>"; $content .= "<tr><td style=\"border:1px solid;\"><strong>File</strong></td><td style=\"border:1px solid;\">{$err['script']}</td></tr>"; $content .= "<tr><td style=\"border:1px solid;\"><strong>Line</strong></td><td style=\"border:1px solid;\">{$err['lineno']}</td></tr>"; if(USE_DATABASE) { $content .= "<tr><td style=\"border:1px solid;\"><strong>Error checksum</strong></td><td style=\"border:1px solid;\">{$err['checksum']}</td></tr>"; } $content .= "<tr><td style=\"border:1px solid; background-color:#9FC2FF;\" colspan=\"2\"><strong>\$_SERVER</strong></td></tr>"; // Format the server superglobal. foreach($_SERVER as $key=>$val) { $content .= "<tr><td style=\"border:1px solid;\"><strong>{$key}</strong></td><td style=\"border:1px solid;\">{$val}</td></tr>"; } $content .= "<tr><td style=\"border:1px solid; background-color:#9FC2FF;\" colspan=\"2\"><strong>\$_REQUEST</strong></td></tr>";; // Format the request superglobal. foreach($err['request'] as $key=>$val) { $content .= "<tr><td style=\"border:1px solid;\"><strong>".print_r($key, true)."</strong></td><td style=\"border:1px solid;\"><pre>".print_r($val, true)."</pre></td></tr>"; } $content .= "<tr><td style=\"border:1px solid; background-color:#9FC2FF;\" colspan=\"2\"><strong>\$_SESSION</strong></td></tr>";; // Format the session superglobal. foreach($err['session'] as $key=>$val) { $content .= "<tr><td style=\"border:1px solid;\"><strong>".print_r($key, true)."</strong></td><td style=\"border:1px solid;\"><pre>".print_r($val, true)."</pre></td></tr>"; } $content .= "<tr><td style=\"border:1px solid; background-color:#9FC2FF;\" colspan=\"2\"><strong>Stack trace</strong></td></tr>"; $content .= "<tr><td style=\"border:1px solid;\" colspan=\"2\"><pre>".print_r($this->vars, true)."</pre></td></tr>"; $content .= "</table>"; // Dispatch the error mail. mail($this->errormail, $this->subjectAppend.' '.$err["subject"], $content, $headers); } } // Restore the deafult PHP error handler. restore_error_handler(); // Restore the default PHP config value. ini_restore('display_errors'); } } // Set the custom error handler to handle the errors. set_error_handler('lookup_ErrorHandler'); |
Here is a small file with which you can test the error handler:
1 2 3 4 5 6 7 8 9 10 11 12 | <?php define("ERROR_HANDLER_SUBJ_APPEND", 'WEBSITE IDENTIFIER'); // You can use this error handler by simply including it in your code. Preferably as high in the execution chain as possible. // Include error handler from include path dir. //include_once("error_handler.php"); // Include with absolute path. include_once(dirname(__FILE__)."/error_handler.php"); session_start(); $_SESSION['justatest'] = array('anarraykey' => 'withanarrayvalue'); trigger_error('This is just a test.', E_USER_WARNING); |