Skip to content

Commit 08d5b3f

Browse files
Mokhtar Tlilisfmok
authored andcommitted
add denormalization exception
1 parent 8859f8b commit 08d5b3f

File tree

9 files changed

+181
-10
lines changed

9 files changed

+181
-10
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,5 @@
5050
"fix-cs": "vendor/bin/php-cs-fixer fix src/",
5151
"phpstan": "vendor/bin/phpstan analyse src tests"
5252
},
53-
"prefer-stable": true
53+
"minimum-stability": "dev"
5454
}

src/EventListener/ExceptionListener.php

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44

55
namespace Sfmok\RequestInput\EventListener;
66

7+
use Sfmok\RequestInput\Exception\ExceptionInterface;
78
use Sfmok\RequestInput\Exception\ValidationException;
89
use Symfony\Component\HttpFoundation\Response;
910
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
11+
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
12+
use Symfony\Component\Serializer\Exception\PartialDenormalizationException;
1013
use Symfony\Component\Serializer\SerializerInterface;
1114

1215
class ExceptionListener
@@ -19,14 +22,43 @@ public function onKernelException(ExceptionEvent $event): void
1922
{
2023
$exception = $event->getThrowable();
2124

22-
if (!$exception instanceof ValidationException) {
25+
if (!$exception instanceof ExceptionInterface) {
2326
return;
2427
}
2528

26-
$event->setResponse(new Response(
27-
$this->serializer->serialize($exception->getViolationList(), 'json'),
28-
$exception->getStatusCode(),
29-
['Content-Type' => 'application/problem+json; charset=utf-8']
30-
));
29+
$headers = ['Content-Type' => 'application/problem+json; charset=utf-8'];
30+
31+
if ($exception instanceof ValidationException) {
32+
$event->setResponse(new Response(
33+
$this->serializer->serialize($exception->getViolationList(), 'json'),
34+
$exception->getStatusCode(),
35+
$headers
36+
));
37+
38+
return;
39+
}
40+
41+
$previous = $exception->getPrevious();
42+
$detail = $previous->getMessage();
43+
44+
$violations = [];
45+
$errors = \method_exists($previous, 'getErrors') ? $previous->getErrors() : [$previous];
46+
foreach ($errors as $error) {
47+
if ($error instanceof NotNormalizableValueException) {
48+
$violations[] = [
49+
'propertyPath' => $error->getPath(),
50+
'message' => sprintf('This value should be of type %s', $error->getExpectedTypes()[0]),
51+
'currentType' => $error->getCurrentType(),
52+
];
53+
}
54+
}
55+
56+
$data = json_encode([
57+
'title' => $exception->getMessage(),
58+
'detail' => empty($violations) ? $detail : 'Data error',
59+
'violations' => $violations
60+
]);
61+
62+
$event->setResponse(new Response($data, $exception->getStatusCode(), $headers));
3163
}
3264
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Sfmok\RequestInput\Exception;
4+
5+
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
6+
7+
class DeserializationException extends BadRequestHttpException implements ExceptionInterface
8+
{
9+
public function __construct(string $message, \Throwable $previous)
10+
{
11+
parent::__construct($message, $previous);
12+
}
13+
}

src/Exception/ExceptionInterface.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sfmok\RequestInput\Exception;
6+
7+
interface ExceptionInterface extends \Throwable
8+
{
9+
}

src/Exception/UnexpectedFormatException.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@
66

77
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
88

9-
class UnexpectedFormatException extends BadRequestHttpException
9+
class UnexpectedFormatException extends BadRequestHttpException implements ExceptionInterface
1010
{
1111
}

src/Exception/ValidationException.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
88
use Symfony\Component\Validator\ConstraintViolationListInterface;
99

10-
class ValidationException extends BadRequestHttpException
10+
class ValidationException extends BadRequestHttpException implements ExceptionInterface
1111
{
1212
private ConstraintViolationListInterface $violationList;
1313

src/Factory/InputFactory.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@
55
namespace Sfmok\RequestInput\Factory;
66

77
use Sfmok\RequestInput\Attribute\Input;
8+
use Sfmok\RequestInput\Exception\DeserializationException;
89
use Sfmok\RequestInput\Exception\UnexpectedFormatException;
910
use Sfmok\RequestInput\InputInterface;
1011
use Sfmok\RequestInput\Exception\ValidationException;
1112
use Symfony\Component\HttpFoundation\Request;
13+
use Symfony\Component\Serializer\Exception\NotEncodableValueException;
14+
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
15+
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
1216
use Symfony\Component\Serializer\SerializerInterface;
1317
use Symfony\Component\Validator\Validator\ValidatorInterface;
1418

@@ -42,7 +46,12 @@ public function createFromRequest(Request $request, string $type, string $format
4246

4347
$inputMetadata = $request->attributes->get('_input');
4448

45-
$input = $this->serializer->deserialize($data, $type, $format, $inputMetadata?->getContext() ?? []);
49+
try {
50+
$input = $this->serializer->deserialize($data, $type, $format, $inputMetadata?->getContext() ?? []);
51+
} catch (UnexpectedValueException $exception) {
52+
throw new DeserializationException('Deserialization Failed', $exception);
53+
}
54+
4655

4756
if (!$this->skipValidation) {
4857
$violations = $this->validator->validate($input, null, $inputMetadata?->getGroups() ?? ['Default']);

tests/EventListener/ExceptionListenerTest.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@
88
use Prophecy\PhpUnit\ProphecyTrait;
99
use Prophecy\Prophecy\ObjectProphecy;
1010
use Sfmok\RequestInput\EventListener\ExceptionListener;
11+
use Sfmok\RequestInput\Exception\DeserializationException;
1112
use Sfmok\RequestInput\Exception\ValidationException;
1213
use Symfony\Component\HttpFoundation\Request;
1314
use Symfony\Component\HttpFoundation\Response;
1415
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
1516
use Symfony\Component\HttpKernel\HttpKernelInterface;
17+
use Symfony\Component\Serializer\Exception\NotEncodableValueException;
18+
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
1619
use Symfony\Component\Serializer\SerializerInterface;
1720
use Symfony\Component\Validator\ConstraintViolationList;
1821

@@ -53,6 +56,58 @@ public function testOnKernelExceptionWithValidationException(): void
5356
$this->assertSame('application/problem+json; charset=utf-8', $response->headers->get('Content-Type'));
5457
}
5558

59+
public function testOnKernelExceptionWithDenormalizationExceptionDataError(): void
60+
{
61+
$previous = NotNormalizableValueException::createForUnexpectedDataType('test', [], ['string'], 'foo');
62+
$listener = new ExceptionListener($this->serializer->reveal());
63+
$event = new ExceptionEvent(
64+
$this->httpKernel->reveal(),
65+
new Request(),
66+
HttpKernelInterface::MAIN_REQUEST,
67+
new DeserializationException('Deserialization Failed', $previous)
68+
);
69+
70+
$listener->onKernelException($event);
71+
72+
$response = $event->getResponse();
73+
$this->assertInstanceOf(Response::class, $response);
74+
$this->assertSame(Response::HTTP_BAD_REQUEST, $response->getStatusCode());
75+
$this->assertSame('application/problem+json; charset=utf-8', $response->headers->get('Content-Type'));
76+
$this->assertSame(json_encode([
77+
'title' => 'Deserialization Failed',
78+
'detail' => 'Data error',
79+
'violations' => [[
80+
'propertyPath' => 'foo',
81+
'message' => 'This value should be of type string',
82+
'currentType' => 'array'
83+
]],
84+
]), $response->getContent());
85+
}
86+
87+
public function testOnKernelExceptionWithDenormalizationExceptionSyntaxError(): void
88+
{
89+
$previous = new NotEncodableValueException('Syntax error');
90+
$listener = new ExceptionListener($this->serializer->reveal());
91+
$event = new ExceptionEvent(
92+
$this->httpKernel->reveal(),
93+
new Request(),
94+
HttpKernelInterface::MAIN_REQUEST,
95+
new DeserializationException('Deserialization Failed', $previous)
96+
);
97+
98+
$listener->onKernelException($event);
99+
100+
$response = $event->getResponse();
101+
$this->assertInstanceOf(Response::class, $response);
102+
$this->assertSame(Response::HTTP_BAD_REQUEST, $response->getStatusCode());
103+
$this->assertSame('application/problem+json; charset=utf-8', $response->headers->get('Content-Type'));
104+
$this->assertSame(json_encode([
105+
'title' => 'Deserialization Failed',
106+
'detail' => 'Syntax error',
107+
'violations' => [],
108+
]), $response->getContent());
109+
}
110+
56111
public function testOnKernelExceptionWithoutValidationException(): void
57112
{
58113
$this->serializer->serialize()->shouldNotBeCalled();

tests/Factory/InputFactoryTest.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@
77
use PHPUnit\Framework\TestCase;
88
use Prophecy\PhpUnit\ProphecyTrait;
99
use Sfmok\RequestInput\Attribute\Input;
10+
use Sfmok\RequestInput\Exception\DeserializationException;
1011
use Sfmok\RequestInput\Exception\UnexpectedFormatException;
12+
use Sfmok\RequestInput\Exception\ValidationException;
1113
use Sfmok\RequestInput\Factory\InputFactory;
1214
use Sfmok\RequestInput\Factory\InputFactoryInterface;
1315
use Sfmok\RequestInput\Tests\Fixtures\Input\DummyInput;
1416
use Symfony\Component\HttpFoundation\Request;
17+
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
1518
use Symfony\Component\Serializer\SerializerInterface;
19+
use Symfony\Component\Validator\ConstraintViolation;
1620
use Symfony\Component\Validator\ConstraintViolationList;
1721
use Symfony\Component\Validator\Validator\ValidatorInterface;
1822
use Prophecy\Prophecy\ObjectProphecy;
@@ -137,6 +141,55 @@ public function testCreateFormRequestWithInputMetadata(Request $request): void
137141
$this->assertEquals($input, $inputFactory->createFromRequest($request, $input::class, $request->getContentType()));
138142
}
139143

144+
/**
145+
* @dataProvider provideDataRequestWithFrom
146+
*/
147+
public function testCreateFormRequestWithDeserializationException(Request $request): void
148+
{
149+
$this->expectException(DeserializationException::class);
150+
151+
$input = $this->getDummyInput();
152+
$data = json_encode($request->request->all());
153+
154+
$this->serializer
155+
->deserialize($data, $input::class, 'json', [])
156+
->willThrow(UnexpectedValueException::class)
157+
->shouldBeCalledOnce()
158+
;
159+
160+
$this->validator->validate()->shouldNotBeCalled();
161+
162+
$inputFactory = $this->createInputFactory(false);
163+
$inputFactory->createFromRequest($request, $input::class, $request->getContentType());
164+
}
165+
166+
/**
167+
* @dataProvider provideDataRequestWithFrom
168+
*/
169+
public function testCreateFormRequestWithValidationException(Request $request): void
170+
{
171+
$this->expectException(ValidationException::class);
172+
173+
$input = $this->getDummyInput();
174+
$violations = new ConstraintViolationList([new ConstraintViolation('foo', null, [], null, null, null)]);
175+
$data = json_encode($request->request->all());
176+
177+
$this->serializer
178+
->deserialize($data, $input::class, 'json', [])
179+
->willReturn($input)
180+
->shouldBeCalledOnce()
181+
;
182+
183+
$this->validator
184+
->validate($input, null, ['Default'])
185+
->willReturn($violations)
186+
->shouldBeCalledOnce()
187+
;
188+
189+
$inputFactory = $this->createInputFactory(false);
190+
$inputFactory->createFromRequest($request, $input::class, $request->getContentType());
191+
}
192+
140193
public function provideDataRequestWithContent(): iterable
141194
{
142195
yield [new Request(server: ['CONTENT_TYPE' => 'application/json'])];

0 commit comments

Comments
 (0)