3
\$\begingroup\$

I set up an error handler class to handle all server errors, I know there are already quite a few out there, but I like creating my own for better learning and understanding of how things work. I am currently using this in a framework I am working on (again, for learning purposes), and its working as it should.

With this error handler you can:

  1. Choose if to log the error
  2. Choose the file to log it to
  3. Send the error to an email if enabled
  4. Add a custom error page
  5. Set a custom general error message if no error page is set

The way it works and is set up is like this:

  1. You set up your app settings:

/* App settings
===============================================*/

//App name
define("APP_NAME", "Test App");

//App url
define("APP_URL", "https://testapp.com");

//Support email
define("APP_SUPPORT_EMAIL", "support@testapp.com");



/* Error handling settings (used in ErrorHandler.php)
===============================================*/

//Set debug mode
define("DEBUG_MODE", true);

//Log errors
define("LOG_ERRORS", true);

//Send error reports to email
define("SEND_ERROR_EMAILS", true);

//Email to send the error reports to
define("ERROR_REPORTING_EMAIL", "security@testapp.com");

//Path to the error log file
define("ERROR_LOG_PATH", $_SERVER['DOCUMENT_ROOT']."/../logs/error_log.log");

//Path to the "500" error page
define("ERROR_PAGE_PATH", $_SERVER['DOCUMENT_ROOT']."/../framework/defaults/pages/error500.php");

//Default error message (if no error page is set or found)
define("PUBLIC_ERROR_MESSAGE", "Looks like there was an error. We are already looking in to it!");

Then you either load the error handler with an auto loader, or load it directly (how ever the developer chooses to do it), and then you set the error handler to be the default one with: set_error_handler(array(new ErrorHandler(), 'handleError'));

And for the actual error handler class:

<?

class ErrorHandler{

    //Set default class properties
    private $debugMode          = false;
    private $logErrors          = false;
    private $sendEmail          = false;
    private $securityEmail      = null;
    private $publicErrorMessage = "Looks like there was an error. We are already looking in to it!";
    private $appName            = null;
    private $appSupportEmail    = null;


    /* Constructor - Sets up the class settings
    ===========================================*/
    public function __construct(){

        //Update class properties from defined constants if they are set
        $this->debugMode            = (defined('DEBUG_MODE')?DEBUG_MODE:$this->debugMode);
        $this->logErrors            = (defined('LOG_ERRORS')?LOG_ERRORS:$this->logErrors);
        $this->sendEmail            = (defined('SEND_ERROR_EMAILS')?SEND_ERROR_EMAILS:$this->sendEmail);
        $this->securityEmail        = (defined('ERROR_REPORTING_EMAIL')&&filter_var(ERROR_REPORTING_EMAIL, FILTER_VALIDATE_EMAIL)?ERROR_REPORTING_EMAIL:$this->securityEmail);
        $this->publicErrorMessage   = (defined('PUBLIC_ERROR_MESSAGE')?PUBLIC_ERROR_MESSAGE:$this->publicErrorMessage);
        $this->appName              = (defined('APP_NAME')?APP_NAME:$this->appName);
        $this->appSupportEmail      = (defined('APP_SUPPORT_EMAIL')&&filter_var(APP_SUPPORT_EMAIL, FILTER_VALIDATE_EMAIL)?APP_SUPPORT_EMAIL:$this->appSupportEmail);


        //Create new class properties from defined constants if they are set
        $this->errorLogPath         = (defined('ERROR_LOG_PATH')?ERROR_LOG_PATH:$_SERVER['DOCUMENT_ROOT']."/../logs/error_log.log");
        $this->errorPagePath        = (defined('ERROR_PAGE_PATH')?ERROR_PAGE_PATH:$_SERVER['DOCUMENT_ROOT']."/../framework/defaults/pages/error500.php");
    }



    /*  Handle error
    ===========================================*/
    public function handleError($errno, $errstr, $errfile=false, $errline=false){

        //Save the error data
        $this->errno    = $errno;
        $this->errstr   = $errstr;
        $this->errfile  = $errfile;
        $this->errline  = $errline;


        //Get the acutal file this error occurred in
        $link_array = explode('/',$this->errfile);
        $this->failedOnFile = end($link_array);


        //If logging errors is enabled
        if($this->logErrors){
            $this->saveToLog();
        }


        //If email reporting is enabled
        if($this->sendEmail && $this->securityEmail){
            $this->sendToEmail();
        }


        //Load the error page
        $this->loadErrorPage();
    }



    /* Save the error to a log
    ===========================================*/
    private function saveToLog(){

        //Get the error string
        $errorString = $this->getErrorString();


        //Save the error to the log
        error_log($errorString,3,$this->errorLogPath);
    }



    /* Send the error to the set email
    ===========================================*/
    private function sendToEmail(){

        //Get the error string
        $errorString = $this->getErrorString(true);


        //Set the email headers
        $headers = "MIME-Version: 1.0" . "\r\n";
        $headers .= "Content-type:text/html;charset=UTF-8" . "\r\n";
        $headers .= "From: ".$this->appName." Security <".$this->securityEmail.">\r\n";
        $headers .= "Reply-To: ".$this->securityEmail."" . "\r\n";


        //Send the email
        @mail($this->securityEmail, "An error has occured", $errorString, $headers);
    }



    /* Load the error page
    ===========================================*/
    private function loadErrorPage(){

        //If there is no error page path or the file doesnt exist, output a message
        if(!$this->errorPagePath || !file_exists($this->errorPagePath)){

            //Output a general error message
            echo $this->publicErrorMessage;


            //If debug mode is enabled, output the error
            if($this->debugMode){
                echo "<BR><BR>";
                echo $this->getErrorString(true);
            }
            exit();


        /*  If there is an error page path and it exists, include it
        *   The file itself has access to the error string, it will
        *   output it if debug mode is enabled.
        *   Custom error files can be used by definining the constant ERROR_PAGE_PATH */
        }else{
            include($this->errorPagePath);
            exit();
        }
    }



    /*  Set up the error string to be logged or emailed
    *   Receives an argument for the type of output.
    *   Lines are separated by \n or <BR> (based on the $output argument).
    ===========================================*/
    private function getErrorString($output = false){

        //Switch between the error numbers and set up the error type variable
        switch ($this->errno) {
            case E_NOTICE:
            case E_USER_NOTICE:
            case E_DEPRECATED:
            case E_USER_DEPRECATED:
            case E_STRICT:
                $errorType = "NOTICE";
                break;

            case E_WARNING:
            case E_USER_WARNING:
                $errorType = "WARNING";
                break;

            case E_ERROR:
            case E_USER_ERROR:
            case E_RECOVERABLE_ERROR:
                $errorType = "FATAL";

            default:
                $errorType = "UNKNOWN";
        }


        //Set up the separator based on the $output argument
        if($output){
            $separator = "<BR>";
        }else{
            $separator = "\n";
        }


        //Set up the error string
        $errorString = $errorType.' ['.$this->errno.'] At: '.date("j M y - g:i:s A (T)", time()).":".$separator."";
        $errorString .= "File: ".$this->errfile." (Line: ".$this->errline.")".$separator."";
        $errorString .= "Message: ".$this->errstr."".$separator."";
        $errorString .= "Backtrace: ".$this->backTraceError()."".$separator."";


        //Add a dashed line break only for the log
        if(!$output){
            $errorString .= "------------------------------------------".$separator."";
        }


        //Return the error string
        return $errorString;
    }



    /* Function to back trace the error
    ===========================================*/
    private function backTraceError(){

        //Set up backtrace variables
        $rawBacktrace = debug_backtrace();
        $cleanBacktrace = $backtraceSeparator = '';
        $i = 0;


        //Loop through the backtrace
        foreach($rawBacktrace as $a_key => $a_value){

            //If a file or line is not set, skip this iteration
            if(!isset($a_value['file']) || !isset($a_value['line'])){
                continue;
            }


            //Start saving the backtrace from the file the error occurred in, skip the rest
            if(!isset($backtraceStarted) && basename($a_value['file']) != $this->failedOnFile){
                continue;
            }else{
                $backtraceStarted = true;
            }


            //Add this file to the backtrace
            $cleanBacktrace .= $backtraceSeparator.basename($a_value['file']).' ['.$a_value['line'].']';


            //Set the separator for the next iteration
            $backtraceSeparator = ' < ';


            //Increment the counter
            $i++;
        }


        //Return the backtrace
        return $cleanBacktrace;
    }
}

The output of the error is like this:

WARNING [2] At: 20 Jun 19 - 4:28:02 PM (UTC):
File: /home/framework/core/Application.php (Line: 9)
Message: Use of undefined constant s - assumed 's' (this will throw an Error in a future version of PHP)
Backtrace: Application.php [9] < index.php [17]
------------------------------------------

I tried to make it as customization as I could. Since this is part of the framework I am working on, it comes with a default error500.php file, but if there isn't one then a general message will be shown. Developers also have the option to use their own error pages. The error pages have access to the properties of the error handler class since they are included in it, so the error and other data can be shown for debugging.

Any feedback on this would be great!

\$\endgroup\$
5
  • \$\begingroup\$ It would be a good idea to type hint your arguments e.g. getErrorString(bool $output = false) \$\endgroup\$
    – Dharman
    Jun 20 '19 at 19:54
  • \$\begingroup\$ I am slowly working on something similar, it's on Git - how mine works is you can register multiple callbacks, then I have a set of callback classes like FileLog, Email, Json etc. \$\endgroup\$ Jun 21 '19 at 5:01
  • \$\begingroup\$ @Dharman Yes thats true, i will start doing that \$\endgroup\$
    – K.D
    Jun 21 '19 at 5:51
  • \$\begingroup\$ @ArtisticPhoenix At the moment i have a shutdown function separate from the error handler, but i am going to include the function in to the error handler class itself. I see that you handle exceptions and error in the same class, why is that? \$\endgroup\$
    – K.D
    Jun 21 '19 at 5:54
  • \$\begingroup\$ I normalize the errors by throwing exceptions for them. \$\endgroup\$ Jun 21 '19 at 5:59
1
\$\begingroup\$

Two things here,

  1. Remove your extra empty lines as its hard to read.

  2. Use PHPDoc comments for each function, it's like JavaDocs but for PHP and you can use it to generate documentation for your classes. So Like

/**
* Save the error to a log
*/
private function saveToLog(){

instead of

/* Save the error to a log
===========================================*/
private function saveToLog(){
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.