Home > PHP > A simple error handler for PHP

A simple error handler for PHP

December 25th, 2009 Leave a comment Go to comments

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);
Categories: PHP Tags: , , ,
  1. No comments yet.
  1. No trackbacks yet.