Skip to content

Commit aa044cd

Browse files
author
Florian Krämer
committed
Refactoring the conversion of exceptions
1 parent aee6944 commit aa044cd

13 files changed

+362
-315
lines changed

readme.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,9 @@ composer require phauthentic/problem-details-symfony-bundle
1717
```php
1818
class ExampleController
1919
{
20-
private ProblemDetailsFactoryInterface $problemDetailsFactory;
21-
22-
public function __construct(ProblemDetailsFactoryInterface $problemDetailsFactory)
23-
{
24-
$this->problemDetailsFactory = $problemDetailsFactory;
20+
public function __construct(
21+
private ProblemDetailsFactoryInterface $problemDetailsFactory
22+
) {
2523
}
2624

2725
/**
@@ -33,8 +31,10 @@ class ExampleController
3331
type: 'https://example.net/validation-error',
3432
detail: 'Your request is not valid.',
3533
status: 422,
34+
title: 'Validation Error',
3635
);
3736
}
37+
}
3838
```
3939

4040
## Problem Details Example
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Phauthentic\Symfony\ProblemDetails\ExceptionConversion;
6+
7+
use Symfony\Component\HttpFoundation\Response;
8+
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
9+
use Throwable;
10+
11+
/**
12+
* Handles Thowable and converts them into a Problem Details HTTP response.
13+
*
14+
* Notice that you might need to adjust the priority of the listener in your services.yaml file to make sure it is
15+
* executed in the right order if you have other listeners.
16+
*/
17+
interface ExceptionConverterInterface
18+
{
19+
public function canHandle(Throwable $throwable): bool;
20+
21+
public function convertExceptionToErrorDetails(Throwable $throwable, ExceptionEvent $event): Response;
22+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Phauthentic\Symfony\ProblemDetails\ExceptionConversion;
6+
7+
use Phauthentic\Symfony\ProblemDetails\ProblemDetailsFactoryInterface;
8+
use Symfony\Component\HttpFoundation\Response;
9+
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
10+
use Throwable;
11+
12+
/**
13+
* Handles Thowable and converts it into a Problem Details HTTP response.
14+
*
15+
* Notice that you might need to adjust the priority of the listener in your services.yaml file to make sure it is
16+
* executed in the right order if you have other listeners.
17+
*
18+
* <code>
19+
* Phauthentic\Symfony\ProblemDetails\ThrowableToProblemDetailsKernelListener:
20+
* arguments: ['%kernel.environment%', { }]
21+
* tags:
22+
* - { name: kernel.event_listener, event: kernel.exception, priority: -20 }
23+
* </code>
24+
*
25+
* @link https://www.rfc-editor.org/rfc/rfc9457.html
26+
*/
27+
class GenericThrowableConverter implements ExceptionConverterInterface
28+
{
29+
/**
30+
* @param ProblemDetailsFactoryInterface $problemDetailsFactory
31+
* @param string $environment
32+
* @param array<callable> $mappers
33+
*/
34+
public function __construct(
35+
protected ProblemDetailsFactoryInterface $problemDetailsFactory,
36+
protected string $environment = 'prod',
37+
protected array $mappers = []
38+
) {
39+
}
40+
41+
public function canHandle(Throwable $throwable): bool
42+
{
43+
return true;
44+
}
45+
46+
public function convertExceptionToErrorDetails(Throwable $throwable, ExceptionEvent $event): Response
47+
{
48+
$extensions = [];
49+
if ($this->environment === 'dev' || $this->environment === 'test') {
50+
$extensions['trace'] = $throwable->getTrace();
51+
}
52+
53+
return $this->problemDetailsFactory->createResponse(
54+
status: Response::HTTP_INTERNAL_SERVER_ERROR,
55+
title: $throwable->getMessage(),
56+
extensions: $extensions,
57+
);
58+
}
59+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Phauthentic\Symfony\ProblemDetails\ExceptionConversion;
6+
7+
use Phauthentic\Symfony\ProblemDetails\ProblemDetailsFactoryInterface;
8+
use Symfony\Component\HttpFoundation\Response;
9+
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
10+
use Symfony\Component\HttpKernel\Exception\HttpException;
11+
use Throwable;
12+
13+
/**
14+
* Handles Symfony\Component\HttpKernel\Exception\HttpException exceptions and converts them into Problem Details HTTP
15+
* responses.
16+
*
17+
* Notice that you might need to adjust the priority of the order in your services.yaml file to make sure it is
18+
* executed in the right order if you have other converters.
19+
*
20+
* @link https://www.rfc-editor.org/rfc/rfc9457.html
21+
*/
22+
class HttpExceptionConverter implements ExceptionConverterInterface
23+
{
24+
public function __construct(
25+
protected ProblemDetailsFactoryInterface $problemDetailsFactory
26+
) {
27+
}
28+
29+
public function canHandle(Throwable $throwable): bool
30+
{
31+
return $throwable instanceof HttpException;
32+
}
33+
34+
public function convertExceptionToErrorDetails(Throwable $throwable, ExceptionEvent $event): Response
35+
{
36+
/** @var HttpException $throwable */
37+
return $this->problemDetailsFactory->createResponse(
38+
status: $throwable->getStatusCode(),
39+
type: 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/' . $throwable->getStatusCode(),
40+
title: $throwable->getMessage()
41+
);
42+
}
43+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Phauthentic\Symfony\ProblemDetails\ExceptionConversion;
6+
7+
use Phauthentic\Symfony\ProblemDetails\FromExceptionEventFactoryInterface;
8+
use Phauthentic\Symfony\ProblemDetails\Validation\ValidationErrorsBuilder;
9+
use RuntimeException;
10+
use Symfony\Component\HttpFoundation\Response;
11+
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
12+
use Symfony\Component\HttpKernel\Exception\HttpException;
13+
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
14+
use Symfony\Component\Validator\Exception\ValidationFailedException;
15+
use Throwable;
16+
17+
/**
18+
* Notice that you might need to adjust the priority of the order in your services.yaml file to make sure it is
19+
* executed in the right order if you have other converters.
20+
*/
21+
class ValidationFailedExceptionConverter implements ExceptionConverterInterface
22+
{
23+
public function __construct(
24+
protected ValidationErrorsBuilder $validationErrorsBuilder,
25+
protected FromExceptionEventFactoryInterface $problemDetailsResponseFactory
26+
) {
27+
}
28+
29+
public function canHandle(Throwable $throwable): bool
30+
{
31+
if ($throwable instanceof UnprocessableEntityHttpException) {
32+
$throwable = $throwable->getPrevious();
33+
}
34+
35+
return $throwable instanceof ValidationFailedException;
36+
}
37+
38+
private function extractValidationFailedException(Throwable $throwable): ValidationFailedException
39+
{
40+
if ($throwable instanceof UnprocessableEntityHttpException) {
41+
$throwable = $throwable->getPrevious();
42+
}
43+
44+
if ($throwable instanceof ValidationFailedException) {
45+
return $throwable;
46+
}
47+
48+
throw new RuntimeException('ValidationFailedException not found');
49+
}
50+
51+
public function convertExceptionToErrorDetails(Throwable $throwable, ExceptionEvent $event): Response
52+
{
53+
$throwable = $this->extractValidationFailedException($throwable);
54+
55+
$errors = $this->validationErrorsBuilder->buildErrors($throwable);
56+
57+
return $this->problemDetailsResponseFactory->createResponseFromKernelExceptionEvent($event, $errors);
58+
}
59+
}

src/HttpExceptionToProblemDetailsKernelListener.php

Lines changed: 0 additions & 57 deletions
This file was deleted.

src/ThrowableToProblemDetailsKernelListener.php

Lines changed: 18 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,14 @@
44

55
namespace Phauthentic\Symfony\ProblemDetails;
66

7-
use Symfony\Component\HttpFoundation\JsonResponse;
8-
use Symfony\Component\HttpFoundation\Response;
7+
use InvalidArgumentException;
8+
use Phauthentic\Symfony\ProblemDetails\ExceptionConversion\ExceptionConverterInterface;
99
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
10-
use Throwable;
1110

1211
/**
1312
* Handles Thowable and converts it into a Problem Details HTTP response.
1413
*
15-
* Notice that you might need to adjust the priority of the listener in your services.yaml file to make sure it is
14+
* Notice that you might need to adjust the priority of the converters in your services.yaml file to make sure it is
1615
* executed in the right order if you have other listeners.
1716
*
1817
* <code>
@@ -27,15 +26,14 @@
2726
class ThrowableToProblemDetailsKernelListener
2827
{
2928
/**
30-
* @param ProblemDetailsFactoryInterface $problemDetailsFactory
31-
* @param string $environment
32-
* @param array<callable> $mappers
29+
* @param array<ExceptionConverterInterface> $exceptionConverters
3330
*/
3431
public function __construct(
35-
protected ProblemDetailsFactoryInterface $problemDetailsFactory,
36-
protected string $environment = 'prod',
37-
protected array $mappers = []
32+
protected array $exceptionConverters = []
3833
) {
34+
if (empty($this->exceptionConverters)) {
35+
throw new InvalidArgumentException('No exception converter passed!');
36+
}
3937
}
4038

4139
public function onKernelException(ExceptionEvent $event): void
@@ -44,37 +42,26 @@ public function onKernelException(ExceptionEvent $event): void
4442
return;
4543
}
4644

47-
$throwable = $event->getThrowable();
45+
$this->processConverters($event);
46+
}
4847

49-
$class = get_class($throwable);
50-
if (isset($this->mappers[$class])) {
51-
$mapper = $this->mappers[$class];
52-
$response = $mapper($throwable);
48+
private function processConverters(ExceptionEvent $event): void
49+
{
50+
$throwable = $event->getThrowable();
51+
foreach ($this->exceptionConverters as $exceptionConverter) {
52+
if (!$exceptionConverter->canHandle($throwable)) {
53+
continue;
54+
}
5355

56+
$response = $exceptionConverter->convertExceptionToErrorDetails($throwable, $event);
5457
$event->setResponse($response);
5558

5659
return;
5760
}
58-
59-
$event->setResponse($this->buildResponse($throwable));
6061
}
6162

6263
private function isNotAJsonRequest(ExceptionEvent $event): bool
6364
{
6465
return $event->getRequest()->getPreferredFormat() !== 'json';
6566
}
66-
67-
private function buildResponse(Throwable $throwable): Response
68-
{
69-
$extensions = [];
70-
if ($this->environment === 'dev' || $this->environment === 'test') {
71-
$extensions['trace'] = $throwable->getTrace();
72-
}
73-
74-
return $this->problemDetailsFactory->createResponse(
75-
status: Response::HTTP_INTERNAL_SERVER_ERROR,
76-
title: $throwable->getMessage(),
77-
extensions: $extensions
78-
);
79-
}
8067
}

0 commit comments

Comments
 (0)