<?php
declare(strict_types=1);
namespace Ui\Http\Rest\EventSubscriber;
use App\Shared\Domain\DomainException;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Validator\ConstraintViolationInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Throwable;
use Ui\Http\Web\ConstraintCollection\Exception\ValidationException;
final class ExceptionSubscriber implements EventSubscriberInterface
{
public function __construct(
private TranslatorInterface $translator,
private string $environment,
private array $exceptionToStatus = []
) {
}
public static function getSubscribedEvents() : array
{
return [
KernelEvents::EXCEPTION => 'onKernelException',
];
}
public function onKernelException(ExceptionEvent $event) : void
{
$request = $event->getRequest();
if (
'json' !== $request->getContentType()
&& 'json' !== $request->getPreferredFormat()
) {
return;
}
$exception = $event->getThrowable();
$response = new JsonResponse();
$response->headers->set('Content-Type', 'application/vnd.api+json');
$response->setStatusCode($this->determineStatusCode($exception));
$response->setData($this->getErrorMessage($exception, $response));
$event->setResponse($response);
}
private function getErrorMessage(Throwable $exception, Response $response) : array
{
$error = [
'error' => [
'message' => $this->getExceptionMessage($exception, $response),
'code' => $this->determineStatusCode($exception),
],
];
if ($exception instanceof ValidationException) {
$constraints = [];
/** @var ConstraintViolationInterface $result */
foreach ($exception->getViolationList() as $result) {
$constraints[$result->getPropertyPath()][] = $this->translator->trans($result->getMessage());
}
$error['error']['code'] = Response::HTTP_BAD_REQUEST;
$error['error']['errors'] = $constraints;
}
if ('dev' === $this->environment) {
$error = \array_merge(
$error,
[
'meta' => [
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'message' => $exception->getMessage(),
'trace' => $exception->getTrace(),
'traceString' => $exception->getTraceAsString(),
],
]
);
}
return $error;
}
private function getExceptionMessage(Throwable $exception, Response $response) : string
{
if ($exception instanceof DomainException) {
$response->setStatusCode($this->determineStatusCode($exception));
return $this->translator->trans(
$exception->getMessage(),
$exception->getParameters(),
DomainException::TRANSLATION_DOMAIN
);
}
return $exception->getMessage();
}
private function determineStatusCode(Throwable $exception) : int
{
$exceptionClass = \get_class($exception);
foreach ($this->exceptionToStatus as $class => $status) {
if (\is_a($exceptionClass, $class, true)) {
return $status;
}
}
// Process HttpExceptionInterface after `exceptionToStatus` to allow overrides from config
if ($exception instanceof HttpExceptionInterface) {
return $exception->getStatusCode();
}
if ($exception instanceof DomainException) {
return $exception->getCode();
}
// Default status code is always 500
return Response::HTTP_INTERNAL_SERVER_ERROR;
}
}