Laravel - Turn Exceptions Into API Responses
Quick / Basic Solution
One can simply edit the exception handler by going to:
app/Exceptions/Handler.php
Within there, there is a register()
method which registers your renderable and reportable exceptions .
One can put in a switch with a case for each Exception that you wish to turn into a legitimate API response like so:
/**
* Register the exception handling callbacks for the application.
*
* @return void
*/
public function register()
{
$this->reportable(function (Throwable $e) {
// perhaps things are here...
});
// Fill this with your switch of API responses to exceptions you wish to handle.
$this->renderable(function (Throwable $e, $request) {
$response = null;
if ($request->is('api/*'))
{
switch (get_class($e))
{
case ExceptionValidationFailed::class:
{
/* @var $e \App\Exceptions\ExceptionValidationFailed */
$response = ResponseFactory::createValidationFailedApiError($e->getValidator());
}
break;
case ExceptionPermissionDenied::class:
{
/* @var $e \App\Exceptions\ExceptionPermissionDenied */
$response = ResponseFactory::createPermissionDenied($e->getRequiredPermission());
}
break;
}
// register more here...
}
return $response;
});
}
A Better Solution
It won't take long with the previous method, before your Handler gets absolutely filled with lots of exceptions and looks quite ugly and unmanageable. It would be a lot nicer if we could just "register" the exceptions automatically, so we never have to touch this code again and have all of the logic that handles the rendering in the Exceptions themselves.
Luckily, this is as easy as adding a render()
method to our exceptions as explained here.
For this, I create an abstract exception called AbstractRenderableException
that my other exceptions extend like so:
<?php
namespace App\Exceptions;
use Illuminate\Http\Request;
use Programster\Http\HttpCode;
use Symfony\Component\HttpFoundation\Response;
abstract class AbstractRenderableException extends \Exception
{
/**
* Force the child implementation to set the HTTP status code.
* @return HttpCode
*/
public abstract function getHttpStatusCode() : HttpCode;
public function render(Request $request) : Response | null
{
if ($request->expectsJson() || $request->is("api/*")) // may wish to use instead: if ($request->is('api/*'))
{
$errorArrayBase = [
"code" => $this->getCode(),
"message" => $this->getMessage()
];
$errorArray = array_merge($errorArrayBase, $this->getAdditionalJsonErrorItems());
// API JSON response
$response = \response()->json([
'error' => $errorArray,
]);
$response->setStatusCode($this->getHttpStatusCode()->value);
}
else
{
// web view response
$response = null;
}
return $response;
}
/**
* Get the array of any extra items that should form part of the error array that gets returned.
* Most of the time you just want to return [];
*/
abstract protected function getAdditionalJsonErrorItems() : array;
}
This allows me to create self-explanatory exceptions like so:
<?php
namespace App\Exceptions;
use App\Models\ErrorCode;
use \Illuminate\Validation\Validator;
use Programster\Http\HttpCode;
class ExceptionValidationFailed extends AbstractRenderableException
{
public function __construct(
private readonly Validator $validator,
ErrorCode $code = ErrorCode::VALIDATION_FAILED
)
{
parent::__construct("Validation failed.", $code->value);
}
public function getHttpStatusCode(): HttpCode
{
return HttpCode::BAD_REQUEST;
}
protected function getAdditionalJsonErrorItems(): array
{
return ["validation_errors" => $this->getValidator()->getMessageBag()];
}
public function getValidator() : Validator
{
return $this->validator;
}
}
Now in my request handlers, I can simply do:
$validationRules = [
'id' => 'uuid',
// other things here
];
$data = request()->all();
$validator = \Validator::make($data, $validationRules);
if ($validator->fails())
{
throw new App\Exceptions\ExceptionValidationFailed($validator);
}
First published: 11th August 2022