Correct error handling in PHP

    What I mean by proper processing:


    • A universal solution that can be inserted into any existing code;
    • Easily extensible solution;
    • There are already three “error mechanisms” in PHP: the error itself, the exception, and the assertion. To reduce three mechanisms to one - exception. In the comments on the previous article on this topic, the opinion was expressed that exception is a bad and / or complicated error handling method. I don’t think so and am ready to discuss this in the comments;
    • Optional logging;
    • A common exception handler that will support different output formats and debug / production modes;
    • In debug mode, trace should be displayed. Requirements for trace: compact, clear and, if possible, links to open files in the IDE.


    One stop solution


    To do this, let's make an exceptionHandlerClass class. The exceptionHandlerClass will store settings and static methods - error, exception, and assertion handlers. We also need the setupHandlers and restoreHandlers methods. The first method will configure error trapping. Error and assertion handlers will throw an ErrorException. The Exception handler will handle unhandled Exception and, depending on the settings, output an appropriate response. restoreHandlers will return all handlers to their original state - this will help when embedding a class in code with an existing error handling mechanism. The connection looks like this:
    1. require 'exceptionHandler / exceptionHandlerClass.php';
    2. exceptionHandlerClass :: setupHandlers ();

    enable debug mode (disabled by default):
    1. exceptionHandlerClass :: $ debug = true;

    Output formats


    It’s easier to explain with an example: to display trace on a web page, I wrap it in pre tags and use htmlspecialchars (), on the other hand, it’s not convenient to read the same trace when outputting to the console, it would be easier if it were plainText. If you need to output an error as a response of SapServer, then it must be a properly formed XML document (SoapFault). If the script displays binary data, such as an image, it is more convenient to display errors through WildFire. In all of these situations, you just need to apply different output formats.
    For different formats we will create different classes. To begin with, I implement two output formats exceptionHandlerOutputWeb (for the web) and exceptionHandlerOutputCli (for the command line). We also need the factory class (exceptionHandlerOutputFactory), in which the logic will be encapsulated, when to use which output format.
    1. public function getExceptionHandlerOutput () {
    2.     if (php_sapi_name () == 'cli') {
    3.         return new exceptionHandlerOutputCli ();
    4.     }
    5.     return new exceptionHandlerOutputWeb ();
    6. }

    When calling setupHandlers, you can set the output format by passing an instance of the exceptionHandlerOutput * or exceptionHandlerOutputFactory * class.
    1. exceptionHandlerClass :: setupHandlers (new exceptionHandlerOutputAjax ());

    Thanks to this architecture, formats can be easily expanded. To create a new format, it is enough to create a class that will inherit from the abstract class exceptionHandlerOutput and implement one method (output).
    1. class exceptionHandlerOutputAjax extends exceptionHandlerOutput {
    2.     public function output ($ exception, $ debug) {
    3.         header ('HTTP / 1.0 500 Internal Server Error', true, 500);
    4.         header ('Status: 500 Internal Server Error', true, 500);
    5.         $ response = array (
    6.             'error' => true,
    7.             'message' => '',
    8.         );
    9.         if ($ debug) {
    10.             $ response ['message'] = $ exception-> getMessage ();
    11.         } else {
    12.             $ response ['message'] = self :: $ productionMessage;
    13.         }
    14.         exit (json_encode ($ response));
    15.     }
    16. }

    If you need more complex logic to automatically select the output format, you need to create a class that inherits from exceptionHandlerOutputFactory and implement the getExceptionHandlerOutput method.
    1. class exceptionHandlerOutputAjaxFactory extends exceptionHandlerOutputDefaultFactory {
    2.     public function getExceptionHandlerOutput () {
    3.         if (self :: detect ()) {
    4.             return new exceptionHandlerOutputAjax ();
    5.         }
    6.         parent :: getExceptionHandlerOutput ();
    7.     }
    8.     public static function detect () {
    9.         return (! empty ($ _ SERVER ['HTTP_X_REQUESTED_WITH']) 
    10.             && strtolower ($ _ SERVER ['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest');
    11.     }
    12. }
    13. exceptionHandlerClass :: setupHandlers (new exceptionHandlerOutputAjaxFactory ());

    Logging


    As I said above, logging can be enabled as desired. To do this, an exceptionLog method was created in exceptionHandlerClass
    1. public static function exceptionLog ($ exception, $ logPriority = null) {
    2.     if (! is_null (self :: $ exceptionHandlerLog)) {
    3.         self :: $ exceptionHandlerLog-> log ($ exception, $ logPriority);
    4.     }
    5. }

    if you need to enable logging, then just do the following:
    1. exceptionHandlerClass :: $ exceptionHandlerLog = new exceptionHandlerSimpleLog ();

    The class for logging should inherit from the abstract exceptionHandlerLog and implement the log method
    1. class exceptionHandlerSimpleLog extends exceptionHandlerLog {
    2.     public function log ($ exception, $ logType) {
    3.         switch ($ logType) {
    4.             case self :: uncaughtException:
    5.                 error_log ($ exception-> getMessage ());
    6.                 break;
    7.         }
    8.     }
    9. }

    logType is one of the constants declared exceptionHandlerLog
    1. const uncaughtException = 0; // unhandled exceptions
    2. const caughtException = 1; // call the logging method outside error handlers
    3. const ignoredError = 2; // errors masked by @ are logged if the scream option is disabled
    4. const lowPriorityError = 3; // errors that do not turn exception
    5. const assertion = 4; // assertion 

    Having logType and exception, the developer can decide for himself what exceptions and how to log. For example, uncaughtException can be sent by mail, ignoredError with severity E_ERROR log to a file, etc.

    Trace


    In the output of trace, I want to see the type of exception, the message, and actually the trace itself. In trace, for each call, it should display which function was called, a list of “short” parameters, the file and the line where the call occurred. I will explain what “short" parameters are as examples: if a function was called with a string 1000 characters long, then the presence of this string in trace will not help in solving the problem, it will only complicate the reading of trace, the same applies to arrays with great nesting. The trace output to the screen simply should tell where to look. To figure out what exactly is happening, you need to debug using xdebug or the primitive var_dump () and die (), as you like.
    Trace example:
    	[ErrorException]: E_WARNING - mysql_connect (): Can't connect to MySQL server on 'localhost' (10061)
    	# 0: mysql_connect ()
    	    D: \ projects1 \ d \ index.php: 19
    	# 1: testClass :: test1 ("long string ... eeeeeery long string" (56))
    	    D: \ projects1 \ d \ index.php: 22
    	# 2: testClass-> test2 (testClass (), -∞, i: iTest, c: testClass, fa: testClass :: test2)
    	    D: \ projects1 \ d \ index.php: 27
    	# 3: testAll (r: stream, fs: testClass :: test1)
    	    D: \ projects1 \ d \ index.php: 30
    
    Legend
    • r: - resource
    • fs: - function (callable string)
    • fa: - function (callable array)
    • i: - interface (string)
    • c: - class (string)
    • ∞ / INF - infinity
    • testClass () - object of type testClass
    • "" (n) - string, the length is indicated in parentheses, ... - the place where part of the string is cut
    • array (n) - array, the length is indicated in parentheses

    And the most useful ... links to open files in the IDE directly from trace.

    Clicking on the link in the IDE will open the corresponding file on the corresponding line.
    For console mode (NetBeans console):
    NetBeans Console
    1. exceptionHandlerOutputCli :: setFileLinkFormat (': in% f on line% l');
    For web mode (TextMate):
    1. exceptionHandlerOutputWeb :: setFileLinkFormat ('txmt: // open /? file: //% f & line =% l');

    Can be implemented for NetbBeans (or another IDE). For this you need: register a protocol; make a handler for this protocol (the simplest is a bat file). In the handler, through the command line, call NetBeans with the corresponding file and line. But this is the topic for the next article.
    The code was written in two days so that minor flaws are possible. Download (there was no time to put in the repository).

    UPD: moved to the PHP blog
    UPD2: continuing with the topic of working with exceptions in PHP

    Also popular now: