Exception Handling

Global Exception Handler

At some point, your application is going to throw an unhandled exception or shut down unexpectedly. When this happens, it would be nice to log details about the error and present a nicely-formatted response for the user. Aphiria provides GlobalExceptionHandler to do just this. It can be used to render exceptions for both HTTP and console applications, and is framework-agnostic.

Let's look at an example:

use Aphiria\Exceptions\GlobalExceptionHandler;
use Aphiria\Framework\Api\Exceptions\ApiExceptionRenderer;

$exceptionRenderer = new ApiExceptionRenderer();
$globalExceptionHandler = new GlobalExceptionHandler($exceptionRenderer);
// This is important - it's what registers the handler as the default handler in PHP
$globalExceptionHandler->registerWithPhp();

That's it. Now, whenever an unhandled error or exception is thrown, the global exception handler will catch it, log it, and render it. We'll go into more details on how to customize it below.

API Exception Renderer

ApiExceptionRenderer is provided out of the box to simplify rendering API responses for Aphiria applications. This renderer tries to create a response using the following steps:

  1. If an exception response factory exists for the thrown exception, it's used to create a response
  2. Otherwise, if the renderer is configured to use problem details, it will create a 500 response with a problem details body
  3. Otherwise, if the renderer does not use problem details, an empty 500 response is returned

By default, problem details are enabled. To disable them, pass in false in the constructor:

use Aphiria\Framework\Api\Exceptions\ApiExceptionRenderer;

$exceptionRenderer = new ApiExceptionRenderer(false);

Exception Responses

You might not want all exceptions to result in a 500. For example, if you have a UserNotFoundException, you might want to map that to a 404. Here's how:

use Aphiria\Exceptions\GlobalExceptionHandler;
use Aphiria\Framework\Api\Exceptions\ApiExceptionRenderer;
use Aphiria\Net\Http\HttpStatusCodes;
use Aphiria\Net\Http\IRequest;
use Aphiria\Net\Http\IResponseFactory;

$exceptionRenderer = new ApiExceptionRenderer();
$exceptionRenderer->registerResponseFactory(
    UserNotFoundException::class,
    function (UserNotFoundException $ex, IRequest $request, IResponseFactory $responseFactory) {
        return $responseFactory->createResponse($request, HttpStatusCodes::HTTP_NOT_FOUND);
    }
);

// You can also register many exceptions-to-response factories:
$exceptionRenderer->registerManyResponseFactories([
    UserNotFoundException::class => 
        function (UserNotFoundException $ex, IRequest $request, IResponseFactory $responseFactory) {
            return $responseFactory->createResponse($request, HttpStatusCodes::HTTP_NOT_FOUND);
        },
    // ...
]);

$globalExceptionHandler = new GlobalExceptionHandler($exceptionRenderer);
$globalExceptionHandler->registerWithPhp();

Console Exception Renderer

ConsoleExceptionRenderer renders exceptions for Aphiria console applications. To render the exception, it goes through the following steps:

  1. If an output writer is registered for the thrown exception, it's used
  2. Otherwise, the exception message and stack trace is output to the console

Output Writers

Output writers allow you to write errors to the output and return a status code.

use Aphiria\Console\Output\IOutput;
use Aphiria\Console\StatusCodes;
use Aphiria\Exceptions\GlobalExceptionHandler;
use Aphiria\Framework\Console\Exceptions\ConsoleExceptionRenderer;

$exceptionRenderer = new ConsoleExceptionRenderer();
$exceptionRenderer->registerOutputWriter(
    DatabaseNotFound::class,
    function (DatabaseNotFound $ex, IOutput $output) {
        $output->writeln('<fatal>Contact a sysadmin</fatal>');

        return StatusCodes::FATAL;
    }
);

// You can also register many exceptions-to-output writers
$exceptionRenderer->registerManyOutputWriters([
    DatabaseNotFound::class => function (DatabaseNotFound $ex, IOutput $output) {
        $output->writeln('<fatal>Contact a sysadmin</fatal>');

        return StatusCodes::FATAL;
    },
    // ...
]);

$globalExceptionHandler = new GlobalExceptionHandler($exceptionRenderer);
$globalExceptionHandler->registerWithPhp();

Logging

By default, the global exception handler is compatible with any PSR-3 logger, such as Monolog. To use a specific logger, just pass it into the handler:

use Aphiria\Exceptions\GlobalExceptionHandler;
use Aphiria\Framework\Api\Exceptions\ApiExceptionRenderer;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use Psr\Log\LogLevel;

$exceptionRenderer = new ApiExceptionRenderer();
$logger = new Logger('app');
$logger->pushHandler(new StreamHandler('/etc/logs/errors.txt', LogLevel::DEBUG));
$globalExceptionHandler = new GlobalExceptionHandler($exceptionRenderer, $logger);
$globalExceptionHandler->registerWithPhp();

Exception Log Levels

It's possible to map certain exceptions to a PSR-3 log level. For example, if you have an exception that means your infrastructure might be down, you can cause it to log as an emergency.

use Aphiria\Exceptions\GlobalExceptionHandler;
use Aphiria\Framework\Api\Exceptions\ApiExceptionRenderer;
use Psr\Log\LogLevel;

$globalExceptionHandler = new GlobalExceptionHandler(new ApiExceptionRenderer());
$globalExceptionHandler->registerLogLevelFactory(
    DatabaseNotFoundException::class,
    fn (DatabaseNotFoundException $ex) => LogLevel::EMERGENCY
);

// You can also register multiple exceptions-to-log-level factories
$globalExceptionHandler->registerManyLogLevelFactories([
    DatabaseNotFoundException::class => fn (DatabaseNotFoundException $ex) => LogLevel::EMERGENCY,
    // ...
]);