Things don't always go the way they were intended in your application. Sometimes this is the client's fault, and sometimes it is the server's (i.e. your) fault. When this happens, it is important that your application:
UserFrosting provides a framework for this process using custom PHP exceptions and exception handlers.
Any time an uncaught exception is thrown in your code, the underlying Slim application catches it and invokes a custom error handler. This may be familiar to you if you've used Slim before.
The difference with UserFrosting is that it replaces Slim's default error handler with a custom error handler, UserFrosting\Sprinkle\Core\Handler\CoreErrorHandler
. CoreErrorHandler
receives the exception, along with the current Request
and Response
objects. Rather than handling the exception directly, though, CoreErrorHandler
checks to see if the exception type has been registered with a custom exception handler. If so, it invokes the corresponding exception handler; otherwise, it invokes the default UserFrosting\Sprinkle\Core\Handler\ExceptionHandler
.
Every custom exception handler needs to implement two methods (as defined by the ExceptionHandlerInterface
):
ajaxHandler
- Invoked when the exception was generated during an xhr (AJAX) request;standardHandler
- Invoked when the exception was generated during a non-AJAX requestAs a basic example, lets take a look at the PhpMailerExceptionHandler
class, which handles exceptions generated when trying to send mail:
<?php
namespace UserFrosting\Sprinkle\Core\Handler;
use UserFrosting\Support\Message\UserMessage;
/**
* Handler for phpMailer exceptions.
*
* @author Alex Weissman (https://alexanderweissman.com)
*/
class PhpMailerExceptionHandler extends ExceptionHandler
{
public function ajaxHandler($request, $response, $exception)
{
$message = new UserMessage("MAIL_ERROR");
$this->logFlag = true;
$this->ci->alerts->addMessageTranslated("danger", $message->message, $message->parameters);
return $response->withStatus(500);
}
public function standardHandler($request, $response, $exception)
{
$messages = [
new UserMessage("MAIL_ERROR")
];
$httpCode = 500;
$this->logFlag = true;
$view = $this->ci->view;
$response = $view->render($response, 'pages/error/default.html.twig', [
"messages" => $messages
])
->withStatus($httpCode)
->withHeader('Content-Type', 'text/html');
return $response;
}
}
As you can see, the ajaxHandler
generates an error message and appends it to the alert stream, allowing the client to fetch and render the message as necessary. The response is simply an HTTP 500 status code, with no response body.
The standardHandler
, on the other hand, renders a complete error page and appends it to the response. You'll notice that the error/default.html.twig
template simply lists any UserMessage
s that were passed to it in the handler; of course, you can create a custom error page as you see fit and use that instead.
You'll also notice that in both cases we set the logFlag
to true, which will tell the CoreErrorHandler
that it should log the exception so that the developer or system administrator can review the problem later. This makes sense for an exception that was raised while trying to send an email - perhaps there is a problem with the mail server, and we want the sysadmin to be able to go back and figure out what went wrong.
You should always have your custom exception handlers extend the base
ExceptionHandler
class so that you do not need to re-implement thegetLogFlag
method.
Once you have defined your custom exception handler, you'll need to map the corresponding exception to it in your service provider.
To do this, simply extend the errorHandler
service and call the register
method on the $handler
object:
$container->extend('errorHandler', function ($handler, $c) {
// Register the MissingOwlExceptionHandler
$handler->registerHandler('\UserFrosting\Sprinkle\Site\Model\Exception\MissingOwlException', '\UserFrosting\Sprinkle\Site\Handler\MissingOwlExceptionHandler');
return $handler;
});
The first argument is the fully qualified name of the exception class, and the second argument is the fully qualified name of the handler class. Notice that we need to use the fully qualified names, including the entire namespace!
Generally speaking, when an exception is thrown and a matching handler is found UserFrosting simply invokes the ajaxHandler
or standardHandler
, depending on the request type. During development however, you may find it more convenient to simply output exception information directly to the browser. This behavior can be controlled by two configuration parameters: settings.displayErrorDetails
, and site.debug.ajax
.
This setting, when enabled, controls two behaviors:
ajaxHandler
will be invoked as normal. However, a detailed error report will be appended to the response as well, including a trace of the exception and a list of the request headers and parameters.standardHandler
will not be invoked. Instead, the detailed error report will be generated and appended to the response. The exception will not be logged, even if the handler has set its logFlag
to true.If this setting is enabled AND settings.displayErrorDetails
is enabled, then logging will be disabled and the message stream will be cleared when handling an exception thrown during an AJAX request.
This setting is also used by client-side code. When site.debug.ajax
is enabled and an error code is received by an AJAX call, your Javascript can use this information to decide whether or not to completely replace the current page with the error report page that was returned in the response.
For example, in the ufAlerts
widget:
base._newMessagesPromise = $.getJSON( base.options.url )
.done(function ( data ) {
if (data) {
base.messages = $.merge(base.messages, data);
}
base.$T.trigger("fetch.ufAlerts");
}).fail(function ( data ) {
base.$T.trigger('error.ufAlerts');
if ((typeof site !== "undefined") && site.debug.ajax && data.responseText) {
document.write(data.responseText);
document.close();
} else {
if (base.options.DEBUG) {
console.log("Error (" + data.status + "): " + data.responseText );
}
}
});
You'll notice the block:
if ((typeof site !== "undefined") && site.debug.ajax && data.responseText) {
document.write(data.responseText);
document.close();
}
This lets you display an error report when an exception is thrown during the AJAX request to the /alerts
route.
By default, both
settings.displayErrorDetails
andsite.debug.ajax
are disabled in the production configuration environment. Do not change this! Displaying detailed exception traces in production is an extreme security risk and could leak sensitive passwords to your users and/or the public.
UserFrosting comes with the following exceptions already built-in:
RuntimeException (built-in to PHP)
├── UserFrosting\Support\FileNotFoundException
├── UserFrosting\Support\JsonException
└── UserFrosting\Sprinkle\Core\Throttle\ThrottlerException
UserFrosting\Support\HttpException
├── UserFrosting\Support\BadRequestException
├── UserFrosting\Support\ForbiddenException
├── UserFrosting\Sprinkle\Core\Model\DatabaseInvalidException
└── UserFrosting\Sprinkle\Account\Authenticate\Exception\AuthCompromisedException
├── UserFrosting\Sprinkle\Account\Authenticate\Exception\AccountDisabledException
├── UserFrosting\Sprinkle\Account\Authenticate\Exception\AccountInvalidException
├── UserFrosting\Sprinkle\Account\Authenticate\Exception\AccountNotVerifiedException
├── UserFrosting\Sprinkle\Account\Authenticate\Exception\AuthExpiredException
├── UserFrosting\Sprinkle\Account\Authenticate\Exception\InvalidCredentialsException
├── UserFrosting\Sprinkle\Account\Authorize\AuthorizationException
├── UserFrosting\Sprinkle\Account\Controller\Exception\SpammyRequestException
└── UserFrosting\Sprinkle\Account\Util\HashFailedException
You can define your own exceptions, of course, optionally inheriting from any of these existing exception types. Every exception you define must eventually inherit back to PHP's base Exception
class.
You'll notice that a large portion of UserFrosting's exception types inherit from the HttpException
class. This is an interesting exception (no pun intended) - it acknowledges that the exception message (which you would want your developers and sysadmins to see), and the client messages (which are displayed to the user to let them know that something went wrong), are generally different things.
The HttpException
class acts like a typical exception, but it maintains two additional parameters internally: a list of messages that the exception handler may display to the client, and a status code that should be returned with the response. As a simple example, consider the AccountInvalidException
:
<?php
namespace UserFrosting\Sprinkle\Account\Authenticate\Exception;
use UserFrosting\Support\Exception\HttpException;
class AccountInvalidException extends HttpException
{
protected $default_message = 'ACCOUNT.INVALID';
protected $http_error_code = 403;
}
It defines a default message, ACCOUNT.INVALID
, that the registered exception handler can display on an error page or push to the alert stream. It also sets a default HTTP status code to return with the error response.
The default message can be overridden when the exception is thrown in your code:
$e = new AccountInvalidException("This is the exception message that will be logged for the dev/sysadmin.");
$e->addUserMessage("This is a custom error message that will be sent back to the client. Hello, client!");
throw $e;
Internally, CoreErrorHandler
uses the instanceof
method to map a given exception to a given handler. This means that if your exception is an instanceof
multiple different classes, for example if you inherited from another exception class, then CoreErrorHandler
will use the handler mapped to the last matching exception type. For example, if you have an exception:
<?php
class MissingOwlException extends MissingBirdException
{
}
And you have registered a handler for MissingBirdException
:
$container->extend('errorHandler', function ($handler, $c) {
$handler->registerHandler('\UserFrosting\MissingBirdException', '\UserFrosting\MissingBirdExceptionHandler');
return $handler;
});
Since MissingOwlException
inherits from MissingBirdException
, UserFrosting will use the MissingBirdExceptionHandler
to handle your MissingOwlException
, unless later on you register another handler specifically on the MissingOwlException
type.