Okay, so in case anyone else needs such a functionality, I’ve gotten around to implementing it.
First of all, you have to register it in bootstrap.php
use App\Error\AppError;
$errorHandler = new AppError();
$errorHandler->register();
Then add this code to src\Error\AppError.php:
<?php
namespace App\Error;
use Cake\Error\BaseErrorHandler;
use Cake\Filesystem\File;
use Cake\I18n\Time;
use Cake\Mailer\Email;
use Cake\Error\Debugger;
use Cake\Routing\Router;
class AppError extends BaseErrorHandler
{
public function _displayError($error, $debug)
{
return 'There has been an error!';
}
public function _displayException($exception)
{
return 'There has been an exception!';
}
public function handleFatalError($code, $description, $file, $line)
{
$this->emailErrors([
'code' => $code,
'description' => $description,
'file' => $file,
'line' => $line
], ADMIN_EMAIL);
return 'A fatal error has happened';
}
/**
* Email fatal errors to the specified eMail address, but only up to one within a certain time period.
* A temp file is created to determine whether to send an email. Only errors that haven't been sent via email are
* stored there; The sent ones are deleted, so there's no need to periodically empty this file.
*
* @param array $errorData The error details.
* @param string $recipient To which address the error email should be sent.
* @param string $wait How much time has to pass for an identical message to be sent again.
* @return boolean
*/
private function emailErrors($errorData = [], $recipient = '', $wait = '1 hour')
{
//Open json file with the latest errors and read it
$latestErrorsFile = new File(TMP . 'latest_errors.json');
//Create array from the latest errors file
$errors = json_decode( $latestErrorsFile->read(), true );
if (!$errors) $errors = [];
//Append date and name to error data
$now = new Time(null, TIMEZONE);
$errorData['date'] = $now->i18nFormat('yyyy-MM-dd HH:mm:ss');
$errorData['error'] = $this->mapErrorCode($errorData['code'])[0];
$cloneCount = 0; //Count how often the same error exists in latest errors file
$send = true; //Whether to email this error
foreach ($errors as $entry) //Loop through all entries and count how often they occured
{
$date = new Time($entry['date'], TIMEZONE);
if ($this->isIdenticalError($entry, $errorData)) //If this error is identical to the newest error...
{
if ($date->wasWithinLast($wait)) //...and happened within the waiting period...
{
$send = false; break; //...don't send the email and stop the loop.
}
else //Otherwise add it to the count
{
$cloneCount++;
}
}
}
if ($send) //Should the eMail be sent?
{
foreach ($errors as $k => $entry) //Loop through all entries and remove clones
{
if ($this->isIdenticalError($entry, $errorData)) unset($errors[$k]);
}
//Prepare the email content
$message = "\nYou have received this error $cloneCount time(s) within the last $wait.\n\n Debug information:\n\n";
$message .= sprintf(
'%s (%s): %s in [%s, line %s]',
$errorData['error'],
$errorData['code'],
$errorData['description'],
$errorData['file'],
$errorData['line']
);
$trace = Debugger::trace([
'start' => 1,
'format' => 'log'
]);
$request = Router::getRequest();
if ($request) {
$message .= $this->_requestContext($request);
}
$message .= "\n\nTrace:\n" . $trace . "\n";
//Email the error message
$email = new Email('default');
$email->to($recipient)
->subject('A fatal error occurred :: ' . $errorData['description'])
->emailFormat('text')
->send($message);
}
//Append it to the file
$errors[] = (object)$errorData;
//Write the new errors array to the json file
$latestErrorsFile->write( json_encode( array_values($errors), JSON_PRETTY_PRINT) );
return $send;
}
private function isIdenticalError($a, $b = array())
{
return
$a['code'] === $b['code'] &&
$a['description'] === $b['description'] &&
$a['file'] === $b['file'] &&
$a['line'] === $b['line'];
}
}
Note that the constants ADMIN_EMAIL and TIMEZONE have to be set somewhere or replaced with actual content.
This functionality behaves as you’d expect: You’ll receive an email to the predefined address when an error occurs. However, the same error is only sent once within a predefined time period (1 hour by default). Different errors will be sent whenever they occur, though, so there’s still a chance of your inbox being flooded if you for instance have an error that causes many different errors.