diff --git a/src/Symfony/Validator/EventListener/ValidationExceptionListener.php b/src/Symfony/Validator/EventListener/ValidationExceptionListener.php index de307cfb1ba..52e8be507e5 100644 --- a/src/Symfony/Validator/EventListener/ValidationExceptionListener.php +++ b/src/Symfony/Validator/EventListener/ValidationExceptionListener.php @@ -16,6 +16,7 @@ use ApiPlatform\Exception\FilterValidationException; use ApiPlatform\Symfony\Validator\Exception\ConstraintViolationListAwareExceptionInterface; use ApiPlatform\Util\ErrorFormatGuesser; +use ApiPlatform\Validator\Exception\ValidationException; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\Serializer\SerializerInterface; @@ -53,8 +54,13 @@ public function onKernelException(ExceptionEvent $event): void $format = ErrorFormatGuesser::guessErrorFormat($event->getRequest(), $this->errorFormats); + $context = []; + if ($exception instanceof ValidationException && ($errorTitle = $exception->getErrorTitle())) { + $context['title'] = $errorTitle; + } + $event->setResponse(new Response( - $this->serializer->serialize($exception instanceof ConstraintViolationListAwareExceptionInterface ? $exception->getConstraintViolationList() : $exception, $format['key']), + $this->serializer->serialize($exception instanceof ConstraintViolationListAwareExceptionInterface ? $exception->getConstraintViolationList() : $exception, $format['key'], $context), $statusCode, [ 'Content-Type' => sprintf('%s; charset=utf-8', $format['value'][0]), diff --git a/src/Symfony/Validator/Exception/ValidationException.php b/src/Symfony/Validator/Exception/ValidationException.php index 3c6f95d1c46..bfaa03cbb22 100644 --- a/src/Symfony/Validator/Exception/ValidationException.php +++ b/src/Symfony/Validator/Exception/ValidationException.php @@ -23,9 +23,9 @@ */ final class ValidationException extends BaseValidationException implements ConstraintViolationListAwareExceptionInterface, \Stringable { - public function __construct(private readonly ConstraintViolationListInterface $constraintViolationList, string $message = '', int $code = 0, \Exception $previous = null) + public function __construct(private readonly ConstraintViolationListInterface $constraintViolationList, string $message = '', int $code = 0, \Throwable $previous = null, ?string $errorTitle = null) { - parent::__construct($message ?: $this->__toString(), $code, $previous); + parent::__construct($message ?: $this->__toString(), $code, $previous, $errorTitle); } public function getConstraintViolationList(): ConstraintViolationListInterface diff --git a/src/Validator/Exception/ValidationException.php b/src/Validator/Exception/ValidationException.php index 361eb200485..f781c00b335 100644 --- a/src/Validator/Exception/ValidationException.php +++ b/src/Validator/Exception/ValidationException.php @@ -22,4 +22,13 @@ */ class ValidationException extends RuntimeException { + public function __construct(string $message = '', int $code = 0, \Throwable $previous = null, protected readonly ?string $errorTitle = null) + { + parent::__construct($message, $code, $previous); + } + + public function getErrorTitle(): ?string + { + return $this->errorTitle; + } } diff --git a/tests/Symfony/Validator/Metadata/ValidationExceptionListenerTest.php b/tests/Symfony/Validator/Metadata/ValidationExceptionListenerTest.php index ff43f0ea31f..ffba24aac7b 100644 --- a/tests/Symfony/Validator/Metadata/ValidationExceptionListenerTest.php +++ b/tests/Symfony/Validator/Metadata/ValidationExceptionListenerTest.php @@ -57,7 +57,7 @@ public function testValidationException(): void $list = new ConstraintViolationList([]); $serializerProphecy = $this->prophesize(SerializerInterface::class); - $serializerProphecy->serialize($list, 'hydra')->willReturn($exceptionJson)->shouldBeCalled(); + $serializerProphecy->serialize($list, 'hydra', [])->willReturn($exceptionJson)->shouldBeCalled(); $listener = new ValidationExceptionListener($serializerProphecy->reveal(), ['hydra' => ['application/ld+json']]); $event = new ExceptionEvent($this->prophesize(HttpKernelInterface::class)->reveal(), new Request(), \defined(HttpKernelInterface::class.'::MAIN_REQUEST') ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::MASTER_REQUEST, new ValidationException($list)); @@ -89,7 +89,7 @@ public function getConstraintViolationList(): ConstraintViolationListInterface }; $serializerProphecy = $this->prophesize(SerializerInterface::class); - $serializerProphecy->serialize($constraintViolationList, 'hydra')->willReturn($serializedConstraintViolationList)->shouldBeCalledOnce(); + $serializerProphecy->serialize($constraintViolationList, 'hydra', [])->willReturn($serializedConstraintViolationList)->shouldBeCalledOnce(); $exceptionEvent = new ExceptionEvent( $this->prophesize(HttpKernelInterface::class)->reveal(), @@ -120,7 +120,7 @@ public function testValidationFilterException(): void $exception = new FilterValidationException([], 'my message'); $serializerProphecy = $this->prophesize(SerializerInterface::class); - $serializerProphecy->serialize($exception, 'hydra')->willReturn($exceptionJson)->shouldBeCalled(); + $serializerProphecy->serialize($exception, 'hydra', [])->willReturn($exceptionJson)->shouldBeCalled(); $listener = new ValidationExceptionListener($serializerProphecy->reveal(), ['hydra' => ['application/ld+json']]); $event = new ExceptionEvent($this->prophesize(HttpKernelInterface::class)->reveal(), new Request(), \defined(HttpKernelInterface::class.'::MAIN_REQUEST') ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::MASTER_REQUEST, $exception); @@ -134,4 +134,25 @@ public function testValidationFilterException(): void $this->assertSame('nosniff', $response->headers->get('X-Content-Type-Options')); $this->assertSame('deny', $response->headers->get('X-Frame-Options')); } + + public function testValidationExceptionWithHydraTitle(): void + { + $exceptionJson = '{"foo": "bar"}'; + $list = new ConstraintViolationList([]); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->serialize($list, 'hydra', ['title' => 'foo'])->willReturn($exceptionJson)->shouldBeCalled(); + + $listener = new ValidationExceptionListener($serializerProphecy->reveal(), ['hydra' => ['application/ld+json']]); + $event = new ExceptionEvent($this->prophesize(HttpKernelInterface::class)->reveal(), new Request(), \defined(HttpKernelInterface::class.'::MAIN_REQUEST') ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::MASTER_REQUEST, new ValidationException($list, errorTitle: 'foo')); + $listener->onKernelException($event); + + $response = $event->getResponse(); + $this->assertInstanceOf(Response::class, $response); + $this->assertSame($exceptionJson, $response->getContent()); + $this->assertSame(Response::HTTP_UNPROCESSABLE_ENTITY, $response->getStatusCode()); + $this->assertSame('application/ld+json; charset=utf-8', $response->headers->get('Content-Type')); + $this->assertSame('nosniff', $response->headers->get('X-Content-Type-Options')); + $this->assertSame('deny', $response->headers->get('X-Frame-Options')); + } }