tarlepp /
symfony-flex-backend
| 1 | <?php |
||
| 2 | declare(strict_types = 1); |
||
| 3 | /** |
||
| 4 | * /src/EventSubscriber/ExceptionSubscriber.php |
||
| 5 | * |
||
| 6 | * @author TLe, Tarmo Leppänen <[email protected]> |
||
| 7 | */ |
||
| 8 | |||
| 9 | namespace App\EventSubscriber; |
||
| 10 | |||
| 11 | use App\Exception\interfaces\ClientErrorInterface; |
||
| 12 | use App\Security\UserTypeIdentification; |
||
| 13 | use App\Utils\JSON; |
||
| 14 | use Doctrine\DBAL\Exception; |
||
| 15 | use Doctrine\ORM\Exception\ORMException; |
||
| 16 | use JsonException; |
||
| 17 | use Psr\Log\LoggerInterface; |
||
| 18 | use Symfony\Component\DependencyInjection\Attribute\Autowire; |
||
| 19 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; |
||
| 20 | use Symfony\Component\HttpFoundation\Response; |
||
| 21 | use Symfony\Component\HttpKernel\Event\ExceptionEvent; |
||
| 22 | use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; |
||
| 23 | use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; |
||
| 24 | use Symfony\Component\Security\Core\Exception\AccessDeniedException; |
||
| 25 | use Symfony\Component\Security\Core\Exception\AuthenticationException; |
||
| 26 | use Throwable; |
||
| 27 | use function array_intersect; |
||
| 28 | use function array_key_exists; |
||
| 29 | use function class_implements; |
||
| 30 | use function in_array; |
||
| 31 | use function spl_object_hash; |
||
| 32 | |||
| 33 | /** |
||
| 34 | * Class ExceptionSubscriber |
||
| 35 | * |
||
| 36 | * @package App\EventSubscriber |
||
| 37 | * @author TLe, Tarmo Leppänen <[email protected]> |
||
| 38 | */ |
||
| 39 | class ExceptionSubscriber implements EventSubscriberInterface |
||
| 40 | { |
||
| 41 | /** |
||
| 42 | * @var array<string, bool> |
||
| 43 | */ |
||
| 44 | private static array $cache = []; |
||
| 45 | |||
| 46 | /** |
||
| 47 | * @var array<int, class-string> |
||
|
0 ignored issues
–
show
Documentation
Bug
introduced
by
Loading history...
|
|||
| 48 | */ |
||
| 49 | private static array $clientExceptions = [ |
||
| 50 | HttpExceptionInterface::class, |
||
| 51 | ClientErrorInterface::class, |
||
| 52 | ]; |
||
| 53 | |||
| 54 | 657 | public function __construct( |
|
| 55 | private readonly LoggerInterface $logger, |
||
| 56 | private readonly UserTypeIdentification $userService, |
||
| 57 | #[Autowire('%kernel.environment%')] |
||
| 58 | private readonly string $environment, |
||
| 59 | ) { |
||
| 60 | 657 | } |
|
| 61 | |||
| 62 | /** |
||
| 63 | * {@inheritdoc} |
||
| 64 | */ |
||
| 65 | 1 | public static function getSubscribedEvents(): array |
|
| 66 | { |
||
| 67 | 1 | return [ |
|
| 68 | 1 | ExceptionEvent::class => [ |
|
| 69 | 1 | 'onKernelException', |
|
| 70 | 1 | -100, |
|
| 71 | 1 | ], |
|
| 72 | 1 | ]; |
|
| 73 | } |
||
| 74 | |||
| 75 | /** |
||
| 76 | * Method to handle kernel exception. |
||
| 77 | * |
||
| 78 | * @throws JsonException |
||
| 79 | */ |
||
| 80 | 422 | public function onKernelException(ExceptionEvent $event): void |
|
| 81 | { |
||
| 82 | // Get exception from current event |
||
| 83 | 422 | $exception = $event->getThrowable(); |
|
| 84 | |||
| 85 | // Log error |
||
| 86 | 422 | $this->logger->error((string)$exception); |
|
| 87 | |||
| 88 | // Create new response |
||
| 89 | 422 | $response = new Response(); |
|
| 90 | 422 | $response->headers->set('Content-Type', 'application/json'); |
|
| 91 | 422 | $response->setStatusCode($this->getStatusCode($exception)); |
|
| 92 | 422 | $response->setContent(JSON::encode($this->getErrorMessage($exception, $response))); |
|
| 93 | |||
| 94 | // Send the modified response object to the event |
||
| 95 | 422 | $event->setResponse($response); |
|
| 96 | } |
||
| 97 | |||
| 98 | /** |
||
| 99 | * Method to get "proper" status code for exception response. |
||
| 100 | */ |
||
| 101 | 437 | private function getStatusCode(Throwable $exception): int |
|
| 102 | { |
||
| 103 | 437 | return $this->determineStatusCode($exception, $this->userService->getSecurityUser() !== null); |
|
| 104 | } |
||
| 105 | |||
| 106 | /** |
||
| 107 | * Method to get actual error message. |
||
| 108 | * |
||
| 109 | * @return array<string, mixed> |
||
| 110 | */ |
||
| 111 | 422 | private function getErrorMessage(Throwable $exception, Response $response): array |
|
| 112 | { |
||
| 113 | // Set base of error message |
||
| 114 | 422 | $error = [ |
|
| 115 | 422 | 'message' => $this->getExceptionMessage($exception), |
|
| 116 | 422 | 'code' => $exception->getCode(), |
|
| 117 | 422 | 'status' => $response->getStatusCode(), |
|
| 118 | 422 | ]; |
|
| 119 | |||
| 120 | // Attach more info to error response in dev environment |
||
| 121 | 422 | if ($this->environment === 'dev') { |
|
| 122 | 12 | $error += [ |
|
| 123 | 12 | 'debug' => [ |
|
| 124 | 12 | 'exception' => $exception::class, |
|
| 125 | 12 | 'file' => $exception->getFile(), |
|
| 126 | 12 | 'line' => $exception->getLine(), |
|
| 127 | 12 | 'message' => $exception->getMessage(), |
|
| 128 | 12 | 'trace' => $exception->getTrace(), |
|
| 129 | 12 | 'traceString' => $exception->getTraceAsString(), |
|
| 130 | 12 | ], |
|
| 131 | 12 | ]; |
|
| 132 | } |
||
| 133 | |||
| 134 | 422 | return $error; |
|
| 135 | } |
||
| 136 | |||
| 137 | /** |
||
| 138 | * Helper method to convert exception message for user. This method is |
||
| 139 | * used in 'production' environment so, that application won't reveal any |
||
| 140 | * sensitive error data to users. |
||
| 141 | */ |
||
| 142 | 440 | private function getExceptionMessage(Throwable $exception): string |
|
| 143 | { |
||
| 144 | 440 | return $this->environment === 'dev' |
|
| 145 | 20 | ? $exception->getMessage() |
|
| 146 | 440 | : $this->getMessageForProductionEnvironment($exception); |
|
| 147 | } |
||
| 148 | |||
| 149 | 420 | private function getMessageForProductionEnvironment(Throwable $exception): string |
|
| 150 | { |
||
| 151 | 420 | $message = $exception->getMessage(); |
|
| 152 | |||
| 153 | 420 | $accessDeniedClasses = [ |
|
| 154 | 420 | AccessDeniedHttpException::class, |
|
| 155 | 420 | AccessDeniedException::class, |
|
| 156 | 420 | AuthenticationException::class, |
|
| 157 | 420 | ]; |
|
| 158 | |||
| 159 | 420 | if (in_array($exception::class, $accessDeniedClasses, true)) { |
|
| 160 | 96 | $message = 'Access denied.'; |
|
| 161 | 324 | } elseif ($exception instanceof Exception || $exception instanceof ORMException) { |
|
| 162 | // Database errors |
||
| 163 | 3 | $message = 'Database error.'; |
|
| 164 | 321 | } elseif (!$this->isClientExceptions($exception)) { |
|
| 165 | 12 | $message = 'Internal server error.'; |
|
| 166 | } |
||
| 167 | |||
| 168 | 420 | return $message; |
|
| 169 | } |
||
| 170 | |||
| 171 | /** |
||
| 172 | * Method to determine status code for specified exception. |
||
| 173 | */ |
||
| 174 | 437 | private function determineStatusCode(Throwable $exception, bool $isUser): int |
|
| 175 | { |
||
| 176 | 437 | $accessDeniedException = static fn (bool $isUser): int => $isUser |
|
| 177 | 2 | ? Response::HTTP_FORBIDDEN |
|
| 178 | 6 | : Response::HTTP_UNAUTHORIZED; |
|
| 179 | |||
| 180 | 437 | $clientException = static fn (HttpExceptionInterface|ClientErrorInterface|Throwable $exception): int => |
|
| 181 | 408 | $exception instanceof HttpExceptionInterface || $exception instanceof ClientErrorInterface |
|
| 182 | 408 | ? $exception->getStatusCode() |
|
| 183 | 408 | : (int)$exception->getCode(); |
|
| 184 | |||
| 185 | 437 | $statusCode = match (true) { |
|
| 186 | 437 | $exception instanceof AuthenticationException => Response::HTTP_UNAUTHORIZED, |
|
| 187 | 437 | $exception instanceof AccessDeniedException => $accessDeniedException($isUser), |
|
| 188 | 437 | $this->isClientExceptions($exception) => $clientException($exception), |
|
| 189 | 437 | default => 0, |
|
| 190 | 437 | }; |
|
| 191 | |||
| 192 | 437 | return $statusCode > 0 ? $statusCode : Response::HTTP_INTERNAL_SERVER_ERROR; |
|
| 193 | } |
||
| 194 | |||
| 195 | /** |
||
| 196 | * Method to check if exception is ok to show to user (client) or not. Note |
||
| 197 | * that if this returns true exception message is shown as-is to user. |
||
| 198 | */ |
||
| 199 | 433 | private function isClientExceptions(Throwable $exception): bool |
|
| 200 | { |
||
| 201 | 433 | $cacheKey = spl_object_hash($exception); |
|
| 202 | |||
| 203 | 433 | if (!array_key_exists($cacheKey, self::$cache)) { |
|
| 204 | 430 | $intersect = array_intersect((array)class_implements($exception), self::$clientExceptions); |
|
| 205 | |||
| 206 | 430 | self::$cache[$cacheKey] = $intersect !== []; |
|
| 207 | } |
||
| 208 | |||
| 209 | 433 | return self::$cache[$cacheKey]; |
|
| 210 | } |
||
| 211 | } |
||
| 212 |