Skip to content

Commit

Permalink
Refactoring the conversion of exceptions
Browse files Browse the repository at this point in the history
  • Loading branch information
Florian Krämer committed Jan 6, 2025
1 parent aee6944 commit aa044cd
Show file tree
Hide file tree
Showing 13 changed files with 362 additions and 315 deletions.
10 changes: 5 additions & 5 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,9 @@ composer require phauthentic/problem-details-symfony-bundle
```php
class ExampleController
{
private ProblemDetailsFactoryInterface $problemDetailsFactory;

public function __construct(ProblemDetailsFactoryInterface $problemDetailsFactory)
{
$this->problemDetailsFactory = $problemDetailsFactory;
public function __construct(
private ProblemDetailsFactoryInterface $problemDetailsFactory
) {
}

/**
Expand All @@ -33,8 +31,10 @@ class ExampleController
type: 'https://example.net/validation-error',
detail: 'Your request is not valid.',
status: 422,
title: 'Validation Error',
);
}
}
```

## Problem Details Example
Expand Down
22 changes: 22 additions & 0 deletions src/ExceptionConversion/ExceptionConverterInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace Phauthentic\Symfony\ProblemDetails\ExceptionConversion;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Throwable;

/**
* Handles Thowable and converts them into a Problem Details HTTP response.
*
* Notice that you might need to adjust the priority of the listener in your services.yaml file to make sure it is
* executed in the right order if you have other listeners.
*/
interface ExceptionConverterInterface
{
public function canHandle(Throwable $throwable): bool;

public function convertExceptionToErrorDetails(Throwable $throwable, ExceptionEvent $event): Response;
}
59 changes: 59 additions & 0 deletions src/ExceptionConversion/GenericThrowableConverter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace Phauthentic\Symfony\ProblemDetails\ExceptionConversion;

use Phauthentic\Symfony\ProblemDetails\ProblemDetailsFactoryInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Throwable;

/**
* Handles Thowable and converts it into a Problem Details HTTP response.
*
* Notice that you might need to adjust the priority of the listener in your services.yaml file to make sure it is
* executed in the right order if you have other listeners.
*
* <code>
* Phauthentic\Symfony\ProblemDetails\ThrowableToProblemDetailsKernelListener:
* arguments: ['%kernel.environment%', { }]
* tags:
* - { name: kernel.event_listener, event: kernel.exception, priority: -20 }
* </code>
*
* @link https://www.rfc-editor.org/rfc/rfc9457.html
*/
class GenericThrowableConverter implements ExceptionConverterInterface
{
/**
* @param ProblemDetailsFactoryInterface $problemDetailsFactory
* @param string $environment
* @param array<callable> $mappers
*/
public function __construct(
protected ProblemDetailsFactoryInterface $problemDetailsFactory,
protected string $environment = 'prod',
protected array $mappers = []
) {
}

public function canHandle(Throwable $throwable): bool
{
return true;
}

public function convertExceptionToErrorDetails(Throwable $throwable, ExceptionEvent $event): Response
{
$extensions = [];
if ($this->environment === 'dev' || $this->environment === 'test') {
$extensions['trace'] = $throwable->getTrace();
}

return $this->problemDetailsFactory->createResponse(
status: Response::HTTP_INTERNAL_SERVER_ERROR,
title: $throwable->getMessage(),
extensions: $extensions,
);
}
}
43 changes: 43 additions & 0 deletions src/ExceptionConversion/HttpExceptionConverter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace Phauthentic\Symfony\ProblemDetails\ExceptionConversion;

use Phauthentic\Symfony\ProblemDetails\ProblemDetailsFactoryInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Throwable;

/**
* Handles Symfony\Component\HttpKernel\Exception\HttpException exceptions and converts them into Problem Details HTTP
* responses.
*
* Notice that you might need to adjust the priority of the order in your services.yaml file to make sure it is
* executed in the right order if you have other converters.
*
* @link https://www.rfc-editor.org/rfc/rfc9457.html
*/
class HttpExceptionConverter implements ExceptionConverterInterface
{
public function __construct(
protected ProblemDetailsFactoryInterface $problemDetailsFactory
) {
}

public function canHandle(Throwable $throwable): bool
{
return $throwable instanceof HttpException;
}

public function convertExceptionToErrorDetails(Throwable $throwable, ExceptionEvent $event): Response
{
/** @var HttpException $throwable */
return $this->problemDetailsFactory->createResponse(
status: $throwable->getStatusCode(),
type: 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/' . $throwable->getStatusCode(),
title: $throwable->getMessage()
);
}
}
59 changes: 59 additions & 0 deletions src/ExceptionConversion/ValidationFailedExceptionConverter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace Phauthentic\Symfony\ProblemDetails\ExceptionConversion;

use Phauthentic\Symfony\ProblemDetails\FromExceptionEventFactoryInterface;
use Phauthentic\Symfony\ProblemDetails\Validation\ValidationErrorsBuilder;
use RuntimeException;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Symfony\Component\Validator\Exception\ValidationFailedException;
use Throwable;

/**
* Notice that you might need to adjust the priority of the order in your services.yaml file to make sure it is
* executed in the right order if you have other converters.
*/
class ValidationFailedExceptionConverter implements ExceptionConverterInterface
{
public function __construct(
protected ValidationErrorsBuilder $validationErrorsBuilder,
protected FromExceptionEventFactoryInterface $problemDetailsResponseFactory
) {
}

public function canHandle(Throwable $throwable): bool
{
if ($throwable instanceof UnprocessableEntityHttpException) {
$throwable = $throwable->getPrevious();
}

return $throwable instanceof ValidationFailedException;
}

private function extractValidationFailedException(Throwable $throwable): ValidationFailedException
{
if ($throwable instanceof UnprocessableEntityHttpException) {
$throwable = $throwable->getPrevious();
}

if ($throwable instanceof ValidationFailedException) {
return $throwable;
}

throw new RuntimeException('ValidationFailedException not found');
}

public function convertExceptionToErrorDetails(Throwable $throwable, ExceptionEvent $event): Response
{
$throwable = $this->extractValidationFailedException($throwable);

$errors = $this->validationErrorsBuilder->buildErrors($throwable);

return $this->problemDetailsResponseFactory->createResponseFromKernelExceptionEvent($event, $errors);
}
}
57 changes: 0 additions & 57 deletions src/HttpExceptionToProblemDetailsKernelListener.php

This file was deleted.

49 changes: 18 additions & 31 deletions src/ThrowableToProblemDetailsKernelListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@

namespace Phauthentic\Symfony\ProblemDetails;

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use InvalidArgumentException;
use Phauthentic\Symfony\ProblemDetails\ExceptionConversion\ExceptionConverterInterface;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Throwable;

/**
* Handles Thowable and converts it into a Problem Details HTTP response.
*
* Notice that you might need to adjust the priority of the listener in your services.yaml file to make sure it is
* Notice that you might need to adjust the priority of the converters in your services.yaml file to make sure it is
* executed in the right order if you have other listeners.
*
* <code>
Expand All @@ -27,15 +26,14 @@
class ThrowableToProblemDetailsKernelListener
{
/**
* @param ProblemDetailsFactoryInterface $problemDetailsFactory
* @param string $environment
* @param array<callable> $mappers
* @param array<ExceptionConverterInterface> $exceptionConverters
*/
public function __construct(
protected ProblemDetailsFactoryInterface $problemDetailsFactory,
protected string $environment = 'prod',
protected array $mappers = []
protected array $exceptionConverters = []
) {
if (empty($this->exceptionConverters)) {
throw new InvalidArgumentException('No exception converter passed!');
}
}

public function onKernelException(ExceptionEvent $event): void
Expand All @@ -44,37 +42,26 @@ public function onKernelException(ExceptionEvent $event): void
return;
}

$throwable = $event->getThrowable();
$this->processConverters($event);
}

$class = get_class($throwable);
if (isset($this->mappers[$class])) {
$mapper = $this->mappers[$class];
$response = $mapper($throwable);
private function processConverters(ExceptionEvent $event): void
{
$throwable = $event->getThrowable();
foreach ($this->exceptionConverters as $exceptionConverter) {
if (!$exceptionConverter->canHandle($throwable)) {
continue;
}

$response = $exceptionConverter->convertExceptionToErrorDetails($throwable, $event);
$event->setResponse($response);

return;
}

$event->setResponse($this->buildResponse($throwable));
}

private function isNotAJsonRequest(ExceptionEvent $event): bool
{
return $event->getRequest()->getPreferredFormat() !== 'json';
}

private function buildResponse(Throwable $throwable): Response
{
$extensions = [];
if ($this->environment === 'dev' || $this->environment === 'test') {
$extensions['trace'] = $throwable->getTrace();
}

return $this->problemDetailsFactory->createResponse(
status: Response::HTTP_INTERNAL_SERVER_ERROR,
title: $throwable->getMessage(),
extensions: $extensions
);
}
}
Loading

0 comments on commit aa044cd

Please sign in to comment.