Error Handling in CakePHP 3
In CakePHP, you can attach custom error and exception handlers. The default one displays a stack trace in debug mode, and a set of http errors when debug is off. That’s nice and all, but sometimes you need to know when your users are encountering errors, and since you aren’t psychic, we need to store those somewhere. Thankfully, there are quite a few services that allow us to track bugs:
One annoying thing about error/exception handling in CakePHP is needing to attach two handlers:
// in your config/bootstrap.php
$isCli = php_sapi_name() === 'cli';
if ($isCli) {
(new ConsoleErrorHandler(Configure::consume('Error')))->register();
} else {
(new ErrorHandler(Configure::consume('Error')))->register();
}
ConsoleErrorHandler
: For cli-based exceptionsErrorHandler
: For web-based exceptions
This is necessary as the exception handler needs to render the exception information differently; the web error handler might want to use json for responses, or show an html message with an interactive stacktrace.
You also have to have two methods in your handlers:
handleError
: For PHP Errors. PHP 7 does some magic to make these catchable, so perhaps this one will go away someday.handleException
: For thrown Exception instances.
Rather than implement all error handling logic twice, we’ll go the trait-based route.
Traits
Traits are composable units of behavior. They are similar to CakePHP Behaviors, though built into the PHP Core. They come in handy for exception handling in CakePHP as it becomes easy to implement the core logic of capturing an exception and just using that in multiple classes.
Below is a trait for the Bugsnag
service:
<?php
namespace App\Error\Bugsnag;
use Bugsnag_Client;
use Cake\Core\Configure;
use Exception;
trait BugsnagTrait
{
public function handleError($code, $description, $file = null, $line = null, $context = null)
{
$client = $this->client();
if ($client) {
$client->errorHandler($code, $description, $file, $line);
}
return parent::handleError($code, $description, $file, $line, $context);
}
public function handleException(Exception $exception)
{
$client = $this->client();
if ($client) {
$client->notifyException($exception);
}
return parent::handleException($exception);
}
protected function client()
{
$apiKey = Configure::read('Bugsnag.apiKey');
if (!$apiKey && defined('BUGSNAG_API_KEY')) {
$apiKey = BUGSNAG_API_KEY;
}
if (!$apiKey) {
return null;
}
$client = null;
if ($apiKey) {
$client = new Bugsnag_Client($apiKey);
$config = Configure::read('Bugsnag.config');
foreach ($config as $key => $value) {
if (method_exists($client, $key)) {
$client->$key($value);
}
}
}
return $client;
}
}
?>
And here is what my ErrorHandler
looks like:
<?php
namespace App\Error\Bugsnag;
use App\Error\Bugsnag\BugsnagTrait;
use Cake\Error\ErrorHandler as CoreErrorHandler;
class ErrorHandler extends CoreErrorHandler
{
use BugsnagTrait;
}
?>
Notice how the ErrorHandler
class itself is devoid of any “real” logic? I’ve pushed all the heavy-lifting into the trait and then just ensured my ErrorHandler
extends the CakePHP core ErrorHandler
. Similarly, my ConsoleErrorHandler
is quite empty as well:
<?php
namespace App\Error\Bugsnag;
use App\Error\Bugsnag\BugsnagTrait;
use Cake\Console\ConsoleErrorHandler as CoreConsoleErrorHandler;
class ConsoleErrorHandler extends CoreConsoleErrorHandler
{
use BugsnagTrait;
}
?>
Homework time
While the logic I implemented is fairly easy to understand, it may also not give the full picture around the exception. For instance, many error collection services provide the ability to add extra metadata to an error, such as the user that was signed in, or client information such as operating system version. ExceptionRenderer
instances can have access to this information using their _getController
methods, and it wouldn’t be too much work to copy that logic into your ErrorHandler
to add extra metadata to the request.
I recommend customizing the error handler to fit your needs - adding metadata, or perhaps using a different service - and seeing what helps you find, replicate, and fix bugs your automated testing didn’t catch.