Skip to content

Commit

Permalink
Refactoring errors and extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
Florian Krämer committed Jan 6, 2025
1 parent a60d38d commit 9d111cd
Show file tree
Hide file tree
Showing 10 changed files with 129 additions and 91 deletions.
14 changes: 14 additions & 0 deletions .idea/php-test-framework.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions .idea/php.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@
"type": "library",
"description": "Symfony bundle for the Problem Details for HTTP APIs RFC",
"require": {
"php": "^8.2"
"php": "^8.2",
"symfony/http-kernel": "~7.0",
"symfony/serializer": "~7.0",
"symfony/uid": "~7.0",
"symfony/validator": "~7.0"
},
"require-dev": {
"infection/infection": "^0.29.10",
"phpmd/phpmd": "^2.5",
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^11.5.0",
"squizlabs/php_codesniffer": "^3.7.2",
"symfony/http-kernel": "~7.0",
"symfony/serializer": "~7.0",
"symfony/uid": "~7.0",
"symfony/validator": "~7.0"
"squizlabs/php_codesniffer": "^3.7.2"
},
"license": "MIT",
"autoload": {
Expand Down
2 changes: 1 addition & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,6 @@ Content-Language: en

## License

This bundle is under the MIT license.
This bundle is under the [MIT license](LICENSE).

Copyright Florian Krämer
24 changes: 15 additions & 9 deletions src/ProblemDetailsFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent as KernelExceptionEvent;

/**
*
*/
readonly class ProblemDetailsFactory implements ProblemDetailsFactoryInterface, FromExceptionEventFactoryInterface
{
public function __construct(
private string $type = 'about:blank',
private string $title = 'Validation Failed',
private int $status = Response::HTTP_UNPROCESSABLE_ENTITY,
private string $defaultType = 'about:blank',
private string $defaultTitle = 'Validation Failed',
private int $defaultStatus = Response::HTTP_UNPROCESSABLE_ENTITY,
private string $errorField = 'errors'
) {
}

Expand All @@ -25,14 +29,14 @@ public function createResponse(
string $type = 'about:blank',
?string $title = null,
?string $instance = null,
array $errors = []
array $extensions = []
): Response {
return ProblemDetailsResponse::create(
status: $status,
type: $type,
title: $title,
instance: $instance,
errors: $errors
extensions: $extensions
);
}

Expand All @@ -45,11 +49,13 @@ public function createResponse(
public function createResponseFromKernelExceptionEvent(KernelExceptionEvent $event, array $errors): Response
{
return ProblemDetailsResponse::create(
status: $this->status,
type: $this->type,
title: $this->title,
status: $this->defaultStatus,
type: $this->defaultType,
title: $this->defaultTitle,
instance: $event->getRequest()->getRequestUri(),
errors: $errors
extensions: [
$this->errorField => $errors,
]
);
}
}
4 changes: 2 additions & 2 deletions src/ProblemDetailsFactoryInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ interface ProblemDetailsFactoryInterface
* @param string $type
* @param string|null $title
* @param string|null $instance
* @param array<int, array<string, mixed>> $errors
* @param array<string, mixed> $extensions
* @return Response
* @link https://www.rfc-editor.org/rfc/rfc9457.html#name-members-of-a-problem-detail
*/
Expand All @@ -22,6 +22,6 @@ public function createResponse(
string $type = 'about:blank',
?string $title = null,
?string $instance = null,
array $errors = []
array $extensions = []
): Response;
}
39 changes: 31 additions & 8 deletions src/ProblemDetailsResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Phauthentic\Symfony\ProblemDetails;

use InvalidArgumentException;
use LogicException;
use Symfony\Component\HttpFoundation\JsonResponse;

Expand All @@ -13,15 +14,25 @@
class ProblemDetailsResponse extends JsonResponse
{
protected static string $contentType = 'application/problem+json';
protected static string $errorsField = 'errors';

/**
* @var array<string>
*/
protected static array $problemDetailsProtectedFields = [
'status',
'type',
'title',
'detail',
'instance',
];

/**
* @param int $status
* @param string $type
* @param string|null $title
* @param string|null $detail
* @param string|null $instance
* @param array<int, array<string, mixed>> $errors
* @param array<string, mixed> $extensions
* @return self
*/
public static function create(
Expand All @@ -30,9 +41,10 @@ public static function create(
?string $title = null,
?string $detail = null,
?string $instance = null,
array $errors = []
array $extensions = []
): self {
self::assertValidstatusCode($status);
self::assertReservedResponseFields($extensions);

$data = [
'status' => $status,
Expand All @@ -48,19 +60,30 @@ public static function create(
$data['instance'] = $instance;
}

if (!empty($errors)) {
$data[self::$errorsField] = $errors;
}

return new self(
$data,
array_merge($data, $extensions),
$status,
[
'Content-Type' => self::$contentType
]
);
}

/**
* @param array<string, mixed> $extensions
*/
public static function assertReservedResponseFields(array $extensions): void
{
foreach (array_keys($extensions) as $key) {
if (in_array($key, self::$problemDetailsProtectedFields, true)) {
throw new InvalidArgumentException(sprintf(
'The key "%s" is a reserved key and cannot be used as an extension.',
$key
));
}
}
}

/**
* Validates if the given status code is a valid client-side (4xx) or server-side (5xx) error.
*/
Expand Down
30 changes: 17 additions & 13 deletions src/ThrowableToProblemDetailsKernelListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,23 @@
class ThrowableToProblemDetailsKernelListener
{
/**
* @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 onKernelException(ExceptionEvent $event): void
{
if ($this->isNotAJsonRequest($event)) {
return;
}

$throwable = $event->getThrowable();

$class = get_class($throwable);
Expand All @@ -53,24 +59,22 @@ public function onKernelException(ExceptionEvent $event): void
$event->setResponse($this->buildResponse($throwable));
}

private function buildResponse(Throwable $throwable): JsonResponse
private function isNotAJsonRequest(ExceptionEvent $event): bool
{
$data = [
'type' => 'about:blank',
'title' => $throwable->getMessage(),
'status' => Response::HTTP_INTERNAL_SERVER_ERROR,
];
return $event->getRequest()->getPreferredFormat() !== 'json';
}

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

return new JsonResponse(
data: $data,
return $this->problemDetailsFactory->createResponse(
status: Response::HTTP_INTERNAL_SERVER_ERROR,
headers: [
'Content-Type' => 'application/problem+json',
]
title: $throwable->getMessage(),
extensions: $extensions
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ public function testOnKernelExceptionWithHttpException(): void
// Arrange
$exception = new HttpException(404, 'Not Found');
$kernel = $this->createMock(HttpKernelInterface::class);
$request = new Request();
$request = new Request(
server: ['HTTP_ACCEPT' => 'application/json']
);
$event = new ExceptionEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $exception);
$listener = new HttpExceptionToProblemDetailsKernelListener();

Expand All @@ -49,7 +51,9 @@ public function testOnKernelExceptionWithNonHttpException(): void
// Arrange
$exception = new Exception('Some other exception');
$kernel = $this->createMock(HttpKernelInterface::class);
$request = new Request();
$request = new Request(
server: ['HTTP_ACCEPT' => 'application/json']
);
$event = new ExceptionEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $exception);
$listener = new HttpExceptionToProblemDetailsKernelListener();

Expand Down
Loading

0 comments on commit 9d111cd

Please sign in to comment.