<?php
declare(strict_types=1);
namespace App\Api\EventListener;
use App\Api\DTO\Http\Response\ApiError;
use App\Api\DTO\Http\Response\ApiProblem;
use App\Api\Enum\ApiErrorCode;
use App\Api\Enum\ApiProblemType;
use App\Api\Enum\RouteData;
use App\Api\Exception\ApiProblemExceptionInterface;
use App\Api\Exception\ConstraintViolationListException;
use App\Api\Exception\WrongRequestArgumentException;
use App\Api\Service\LoggerService;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Serializer\SerializerInterface;
class ApiExceptionListener
{
use LoggerAwareTrait;
/**
* @var SerializerInterface
*/
private $serializer;
/**
* @var bool
*/
private $formatHandledException;
/**
* @var LoggerService
*/
private $loggerService;
public function __construct(
SerializerInterface $serializer,
LoggerService $loggerService,
bool $formatHandledException = false
) {
$this->serializer = $serializer;
$this->formatHandledException = $formatHandledException;
$this->logger = new NullLogger();
$this->loggerService = $loggerService;
}
public function onKernelException(ExceptionEvent $event): void
{
if (!str_contains($event->getRequest()->getPathInfo(),'/api')) {
return;
}
$e = $event->getThrowable();
if ($e instanceof ConstraintViolationListException) {
$apiProblem = $e->getConstraintViolationList();
} elseif ($e instanceof ApiProblemExceptionInterface) {
$apiProblem = $e->getApiProblem();
} else {
if ($e instanceof WrongRequestArgumentException) {
$title = $e->getMessage();
$apiError = new ApiError(ApiErrorCode::BAD_REQUEST_DATA, $title, $e->getProperty());
} elseif ($e instanceof HttpExceptionInterface) {
$title = $e->getMessage();
$apiError = new ApiError(ApiErrorCode::BAD_REQUEST_DATA, $title);
} elseif ($e instanceof AccessDeniedException) {
$e = new AccessDeniedHttpException();
$title = 'Access Denied';
$apiError = new ApiError(ApiErrorCode::FORBIDDEN, $title);
} else {
$title = 'Internal error';
$apiError = new ApiError(ApiErrorCode::INTERNAL_ERROR, $title);
}
$apiProblem = new ApiProblem(ApiProblemType::COMMON, $title);
$apiProblem->addError($apiError);
}
$isHandledException = $e instanceof HttpExceptionInterface;
if ($isHandledException) {
$statusCode = $e->getStatusCode();
$headers = $e->getHeaders();
} else {
$statusCode = 500;
$headers = [];
}
$this->resolveLogger($event->getRequest());
if (!$isHandledException) {
$this->logger->error('Not handled exception', ['exception' => $e]);
if ($this->formatHandledException) {
throw $e;
}
} else {
$this->logger->info('Handled exception', ['exception' => $e]);
}
$data = $this->serializer->serialize($apiProblem, 'json');
$response = new JsonResponse($data, $statusCode, $headers, true);
$response->headers->set('Content-Type', 'application/problem+json');
$event->setResponse($response);
}
private function resolveLogger(Request $request): void
{
if (in_array($request->get('_route'), RouteData::$routeListForLogging)) {
$this->loggerService->log($request);
}
}
}