How to log PHP errors and warnings into a file
Tags: php, programming, webdev
Published: 2021-02-03 08:43:19

When writing server-side code in PHP, it's always a good practice to consider security first. When dealing with PHP errors that could occur at runtime, it's advisable to hide these error messages from the users and instead log them into a file for developers or system admins to review them. This is a good security practice as the error messages can expose an existing vulnerability or give an attacker an entry point into your system.

Enough of talking, how can we do that effectively and efficiently?

We are going to create a class that's going to be invoked every time an error occurs in the system.

+ If you are using an MVC place this code at the top of your main index file.
+ If you are not using an MVC, place this code at the top of every PHP file that you want the errors handled.

```php
include('<REPLACE WITH THE LOCATION OF YOUR FILE>.php');
if (ERR_HANDLER_ENABLED) { 
    register_shutdown_function('sysFatalErr'); 
    set_error_handler('sysErrorhandler'); 
}
```
This class collects all the errors reported from a PHP script, classifies them as either general errors or fatal errors. After classification, it passes the error to the respective method which then calls the log function that writes them out.

Let's define some constant variables here.
```php
/**
 * if your error class file resides in a folder called classes and the index file is in the base folder use:
 *
 * define('ERR_HANDLER_PATH', substr(dirname(__file__), 0, strpos(dirname(__file__), 'classes') - 1) . '/'); // DO NOT change!!
 * 
 * after using the above line comment or remove the fisrt line down here
**/
define('ERR_HANDLER_PATH', dirname(__file__).'/'); // DO NOT change!!
define('ERR_HANDLER_LOG_FOLDER', 'logs'); // Name of logs folder.. Create if it does not exist.
define('ERR_HANDLER_ENABLED', 1); // Enable custom error handler?
define('ERR_HANDLER_DISPLAY', 1); // Display a message on screen?
define('ERR_APPEND_RAND_STRING', 0); // Adds random string to file name for security. Prevents someone attempting browser access.
define('MASK_FILE_PATH', 0); // Hide file path if error occurs..
define('FILE_ERR_LOG_FILE', 'errors.log'); // File name of error log
define('FILE_FATAL_ERR_LOG_FILE', 'fatal_errors.log'); // File name of fatal error log
```
We create our class which has four functions and each function calls the log function when invoked:
+ generalErr - This function is invoked when a general error occurs.
+ mailErr - This function is invoked when an email error occurs.(if you have an email sending script)
+ fataErr - This function is invoked when a fatal error occurs and no further execution can happen.
+ log - calls the write function which then writes the error into a file.
```php
class Errs {

  public function generalErr($error) {
    Errs::log($error, FILE_ERR_LOG_FILE);
  }

  public function mailErr($error) {
    Errs::log($error, FILE_ERR_LOG_FILE);
  }

  public function fatalErr($error) {
    Errs::log($error, FILE_FATAL_ERR_LOG_FILE);
  }

  public function log($error, $file) {
    if (is_dir(ERR_HANDLER_PATH . ERR_HANDLER_LOG_FOLDER)) {
      write(ERR_HANDLER_PATH . ERR_HANDLER_LOG_FOLDER . '/' . Errs::raStr() . $file, trim($error) . linending() . '***** - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  - - - - - - - - - - - - *****' . linending());
    }
  }

  public function raStr() {
    return (ERR_APPEND_RAND_STRING ? substr(md5(uniqid(rand(),1)), 3, 30) . '-' : '');
  }


}

// Initiate the class..
$DDEH = new Errs();
```
Now we check to see if the error handler is enabled. If it is enabled, we turn of displaying all the errors to the user.
```php
if (ERR_HANDLER_ENABLED) {
  // Switch off display errors
  @ini_set('display_errors', 0);
  // Set error reporting level..
  error_reporting(E_ALL);
}
```
On the Error reporting levels, we're going to be using E_ALL. PHP provides lots of them which you can look at here [PHP Error Levels](https://www.php.net/manual/en/errorfunc.constants.php).
Here's just a few that I think are useful together with their error codes.

<table>
<tr>
<th>Error</th>
<th>Error Code</th>
<th>Description</th>
</tr>
<tr>
<td>E_ERROR</td>
<td>1</td>
<td>Fatal runtime error that can be recovered, The execution of the script is stopped immediately</td>
</tr>
<tr>
<td>E_WARNING</td>
<td>2</td>
<td>A runtime warning, not fatal and most errors fall here, execution is not stopped</td>
</tr>
<tr>
<td>E_NOTICE</td>
<td>8</td>
<td>Runtime notice indicating something that could possibly be an error was encountered when running a script normally</td>
</tr>
<tr>
<td>E_COMPILE_ERROR</td>
<td>64</td>
<td>Fatal error that occurs while the script was being compiled generated by the Zend Scripting engine</td>
</tr>
<tr>
<td>E_USER_ERROR</td>
<td>256</td>
<td>A fatal user-generated error, generated by the PHP code using the function trigger_error() rather than the PHP engine</td>
</tr>
<tr>
<td>E_USER_WARNING </td>
<td>512</td>
<td>NON FATAL USER GENERATED warning message, generated by the PHP code using the function trigger_error() rather than the PHP engine</td>
</tr>
<tr>
<tr>
<td>E_STRICT</td>
<td>2048</td>
<td>Not strictly an error but it's triggered whenever PHP encounters code that could lead to problems or forward incompatibilities</td>
</tr>
<tr>
<td>E_ALL</td>
<td>32767</td>
<td>All errors and warnings except from level E_STRICT</td>
</tr>


</table>
**TIP:** Passing a value of (-1) to the error_reporting() function will show every possible error when new levels and constants are added in future PHP versions.

Now, we create a function that inserts a newline depending on the OS architecture.
```php
function linending() {
  $newline = "\r\n";
  if (isset($_SERVER["HTTP_USER_AGENT"]) && strstr(strtolower($_SERVER["HTTP_USER_AGENT"]), 'win')) {
    $newline = "\r\n";
  } else if (isset($_SERVER["HTTP_USER_AGENT"]) && strstr(strtolower($_SERVER["HTTP_USER_AGENT"]), 'mac')) {
    $newline = "\r";
  } else {
    $newline = "\n";
  }
  return (defined('PHP_EOL') ? PHP_EOL : $newline);
}
```
This function will be called whenever a fatal error occurs.
```php
function sysFatalErr() {
  global $DDEH;
  $error = error_get_last();
  if (isset($error['type'])) {
    if ((defined('E_ERROR') && $error['type'] == E_ERROR) || $error['type'] == 4) {
      $string = '[Error Code: ' . $error['type'] . '] ' . $error['message'] . linending();
      $string .= '[Date/Time: ' . date('j F Y @ H:iA') . ']' . linending();
      $string .= '[Fatal error on line ' . $error['line'] . ' in file ' . $error['file'] . ']';
      if (ERR_HANDLER_DISPLAY) {
        echo '<div style="background:#ff9999"><p style="padding:10px;color:#fff">A fatal error has occurred. For more details please view "' . ERR_HANDLER_LOG_FOLDER . '/' . FILE_FATAL_ERR_LOG_FILE . '".</div>';
      }
      $DDEH->fatalErr($string);
    }
  }
}
```
This other function is called when general errors occur, and can further classify them accordingly. *You can add more in the switch block.*
```php
function sysErrorhandler($errno, $errstr, $errfile, $errline) {
  global $DDEH;
  if (!(error_reporting() & $errno)) {
    return;
  }
  if (!method_exists($DDEH,'generalErr') || !method_exists($DDEH,'fatalErr')) {
    return;
  }
  switch ($errno) {
    case E_USER_ERROR:
      $string = '[Error Code: ' . $errno . '] ' . $errstr . linending();
      $string .= '[Date/Time: ' . date('j F Y @ H:iA') . ']' . linending();
      $string .= '[Error on line ' . $errline . ' in file ' . $errfile . ']';
      if (ERR_HANDLER_DISPLAY) {
        echo '<div style="background:#ff9999"><p style="padding:10px;color:#fff">A fatal error has occurred. For more details please view "' . ERR_HANDLER_LOG_FOLDER . '/' . FILE_FATAL_ERR_LOG_FILE . '".</div>';
      }
      $DDEH->fatalErr($string);
      exit;
      break;

    case E_USER_WARNING:
      $string = '[Error Code: ' . $errno . '] ' . $errstr;
      $string .= '[Date/Time: ' . date('j F Y @ H:iA') . ']' . linending();
      $string .= '[Error on line ' . $errline . ' in file ' . $errfile . ']';
      if (ERR_HANDLER_DISPLAY) {
        echo '<div style="background:#ff9999"><p style="padding:10px;color:#fff">An error has occurred. For more details please view "' . ERR_HANDLER_LOG_FOLDER . '/' . FILE_ERR_LOG_FILE . '".</div>';
      }
      $DDEH->generalErr($string);
      break;

    case E_USER_NOTICE:
      $string = '[Error Code: ' . $errno . '] ' . $errstr . linending();
      $string .= '[Date/Time: ' . date('j F Y @ H:iA') . ']' . linending();
      $string .= '[Error on line ' . $errline . ' in file ' . $errfile . ']';
      if (ERR_HANDLER_DISPLAY) {
        echo '<div style="background:#ff9999"><p style="padding:10px;color:#fff">An error has occurred. For more details please view "' . ERR_HANDLER_LOG_FOLDER . '/' . FILE_ERR_LOG_FILE . '".</div>';
      }
      $DDEH->generalErr($string);
      break;

    default:
      $string = '[Error Code: ' . $errno . '] ' . $errstr . linending();
      $string .= '[Date/Time: ' . date('j F Y @ H:iA') . ']' . linending();
      $string .= '[Error on line ' . $errline . ' in file ' . $errfile . ']';
      if (ERR_HANDLER_DISPLAY) {
        echo '<div style="background:#ff9999"><p style="padding:10px;color:#fff">An error has occurred. For more details please view "' . ERR_HANDLER_LOG_FOLDER . '/' . FILE_ERR_LOG_FILE . '".</div>';
      }
      $DDEH->generalErr($string);
      break;
  }
  return true;
}
```
Finally, we write out our errors into a file. Remember to have the logs folder created.
```php
function write($file, $data) {
  file_put_contents($file, $data, FILE_APPEND);
}
```
You can get the whole code on the GitHub repo [Here](https://github.com/mainadennis/php-class-for-php-and-sql-error-logging-into-a-file)

In need of a developer for a project? Hit me Up [DentriceDev Solutions](https://dentricedev.com)

Happy Coding.