Skip to content

Commit 2aa4022

Browse files
committed
Configure request object normalizer
1 parent 9a574dc commit 2aa4022

6 files changed

Lines changed: 116 additions & 14 deletions

File tree

config/services/normalizers.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,11 @@ services:
1515
$classMetadataFactory: '@?serializer.mapping.class_metadata_factory'
1616
$nameConverter: '@Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter'
1717

18+
phplist.request_serializer:
19+
class: Symfony\Component\Serializer\Serializer
20+
arguments:
21+
$normalizers:
22+
- '@Symfony\Component\Serializer\Normalizer\ObjectNormalizer'
23+
1824
PhpList\RestBundle\:
1925
resource: '../../src/*/Serializer/*'

config/services/validators.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
services:
22
PhpList\RestBundle\Common\Validator\RequestValidator:
33
arguments:
4-
$serializer: '@Symfony\Component\Serializer\Normalizer\ObjectNormalizer'
4+
$serializer: '@phplist.request_serializer'
55
$validator: '@validator'
66

77
PhpList\RestBundle\Identity\Validator\Constraint\UniqueEmailValidator:
@@ -50,4 +50,3 @@ services:
5050
autowire: true
5151
autoconfigure: true
5252
tags: [ 'validator.constraint_validator' ]
53-

src/Common/EventListener/ExceptionListener.php

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
1616
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
1717
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
18+
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
1819
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
20+
use Symfony\Component\Validator\Exception\ValidationFailedException;
1921
use Symfony\Component\Validator\Exception\ValidatorException;
2022

2123
class ExceptionListener
@@ -24,7 +26,6 @@ class ExceptionListener
2426
SubscriptionCreationException::class => null,
2527
AttributeDefinitionCreationException::class => null,
2628
AdminAttributeCreationException::class => null,
27-
ValidatorException::class => 400,
2829
AccessDeniedException::class => 403,
2930
AccessDeniedHttpException::class => 403,
3031
AttachmentFileNotFoundException::class => 404,
@@ -36,33 +37,85 @@ public function onKernelException(ExceptionEvent $event): void
3637
{
3738
$exception = $event->getThrowable();
3839

40+
if ($exception instanceof ValidationFailedException
41+
|| $exception instanceof ValidatorException
42+
|| $exception instanceof UnprocessableEntityHttpException
43+
) {
44+
$event->setResponse(
45+
new JsonResponse([
46+
'message' => 'Validation failed',
47+
'errors' => $this->parseFlatValidationMessage($exception->getMessage()),
48+
], 422)
49+
);
50+
51+
return;
52+
}
53+
3954
foreach (self::EXCEPTION_STATUS_MAP as $class => $statusCode) {
4055
if ($exception instanceof $class) {
41-
$status = $statusCode ?? $exception->getStatusCode();
56+
$status = $statusCode ?? (
57+
method_exists($exception, 'getStatusCode')
58+
? $exception->getStatusCode()
59+
: 400
60+
);
61+
4262
$event->setResponse(
4363
new JsonResponse([
44-
'message' => $exception->getMessage()
64+
'message' => $exception->getMessage(),
4565
], $status)
4666
);
67+
4768
return;
4869
}
4970
}
5071

5172
if ($exception instanceof HttpExceptionInterface) {
5273
$event->setResponse(
5374
new JsonResponse([
54-
'message' => $exception->getMessage()
75+
'message' => $exception->getMessage(),
5576
], $exception->getStatusCode())
5677
);
78+
5779
return;
5880
}
5981

6082
if ($exception instanceof Exception) {
6183
$event->setResponse(
6284
new JsonResponse([
63-
'message' => $exception->getMessage()
85+
'message' => $exception->getMessage(),
6486
], 500)
6587
);
6688
}
6789
}
90+
91+
/**
92+
* @return array<string, array<int, string>>
93+
*/
94+
private function parseFlatValidationMessage(string $message): array
95+
{
96+
$errors = [];
97+
$lines = preg_split('/\r\n|\r|\n/', $message) ?: [];
98+
99+
foreach ($lines as $line) {
100+
$line = trim($line);
101+
102+
if ($line === '') {
103+
continue;
104+
}
105+
106+
$parts = explode(':', $line, 2);
107+
108+
if (count($parts) !== 2) {
109+
$errors['_global'][] = $line;
110+
continue;
111+
}
112+
113+
$field = trim($parts[0]);
114+
$errorMessage = trim($parts[1]);
115+
116+
$errors[$field][] = $errorMessage;
117+
}
118+
119+
return $errors;
120+
}
68121
}

src/Common/SwaggerSchemasResponse.php

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,22 @@
66

77
use OpenApi\Attributes as OA;
88

9+
#[OA\Schema(
10+
schema: 'ErrorDetails',
11+
type: 'object',
12+
example: [
13+
'format.formatOptions[0]' => ['The value you selected is not a valid choice.'],
14+
'schedule.repeatUntil' => ['This value is not a valid datetime.'],
15+
'schedule.requeueUntil' => ['This value is not a valid datetime.'],
16+
],
17+
additionalProperties: new OA\AdditionalProperties(
18+
type: 'array',
19+
items: new OA\Items(type: 'string')
20+
)
21+
)]
922
#[OA\Schema(
1023
schema: 'UnauthorizedResponse',
24+
required: ['message'],
1125
properties: [
1226
new OA\Property(
1327
property: 'message',
@@ -19,17 +33,23 @@
1933
)]
2034
#[OA\Schema(
2135
schema: 'ValidationErrorResponse',
36+
required: ['message', 'errors'],
2237
properties: [
2338
new OA\Property(
2439
property: 'message',
2540
type: 'string',
26-
example: 'Some fields are invalid'
41+
example: 'Validation failed'
42+
),
43+
new OA\Property(
44+
property: 'errors',
45+
ref: '#/components/schemas/ErrorDetails'
2746
)
2847
],
2948
type: 'object'
3049
)]
3150
#[OA\Schema(
3251
schema: 'BadRequestResponse',
52+
required: ['message'],
3353
properties: [
3454
new OA\Property(
3555
property: 'message',
@@ -41,6 +61,7 @@
4161
)]
4262
#[OA\Schema(
4363
schema: 'AlreadyExistsResponse',
64+
required: ['message'],
4465
properties: [
4566
new OA\Property(
4667
property: 'message',
@@ -62,7 +83,18 @@
6283
],
6384
type: 'object'
6485
)]
65-
86+
#[OA\Schema(
87+
schema: 'GenericErrorResponse',
88+
required: ['message'],
89+
properties: [
90+
new OA\Property(
91+
property: 'message',
92+
type: 'string',
93+
example: 'An unexpected error occurred.'
94+
)
95+
],
96+
type: 'object'
97+
)]
6698
#[OA\Schema(
6799
schema: 'CursorPagination',
68100
properties: [

src/Messaging/Request/CreateMessageRequest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class CreateMessageRequest implements RequestInterface
3838
public MessageOptionsRequest $options;
3939

4040
#[TemplateExists]
41-
public ?int $templateId;
41+
public ?int $templateId = null;
4242

4343
public function getDto(): MessageDtoInterface
4444
{

tests/Integration/Common/EventListener/ExceptionListenerTest.php

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ public function testAccessDeniedExceptionHandled(): void
3434

3535
$this->assertInstanceOf(JsonResponse::class, $response);
3636
$this->assertEquals(403, $response->getStatusCode());
37-
$this->assertEquals(['message' => 'Forbidden'], json_decode($response->getContent(), true));
37+
$this->assertEquals(
38+
['message' => 'Forbidden'],
39+
json_decode($response->getContent(), true)
40+
);
3841
}
3942

4043
public function testHttpExceptionHandled(): void
@@ -47,7 +50,10 @@ public function testHttpExceptionHandled(): void
4750

4851
$this->assertInstanceOf(JsonResponse::class, $response);
4952
$this->assertEquals(404, $response->getStatusCode());
50-
$this->assertEquals(['message' => 'Not found'], json_decode($response->getContent(), true));
53+
$this->assertEquals(
54+
['message' => 'Not found'],
55+
json_decode($response->getContent(), true)
56+
);
5157
}
5258

5359
public function testSubscriptionCreationExceptionHandled(): void
@@ -61,7 +67,10 @@ public function testSubscriptionCreationExceptionHandled(): void
6167

6268
$this->assertInstanceOf(JsonResponse::class, $response);
6369
$this->assertEquals(409, $response->getStatusCode());
64-
$this->assertEquals(['message' => 'Subscription error'], json_decode($response->getContent(), true));
70+
$this->assertEquals(
71+
['message' => 'Subscription error'],
72+
json_decode($response->getContent(), true)
73+
);
6574
}
6675

6776
public function testGenericExceptionHandled(): void
@@ -74,6 +83,9 @@ public function testGenericExceptionHandled(): void
7483

7584
$this->assertInstanceOf(JsonResponse::class, $response);
7685
$this->assertEquals(500, $response->getStatusCode());
77-
$this->assertEquals(['message' => 'Something went wrong'], json_decode($response->getContent(), true));
86+
$this->assertEquals(
87+
['message' => 'Something went wrong'],
88+
json_decode($response->getContent(), true)
89+
);
7890
}
7991
}

0 commit comments

Comments
 (0)