Skip to content

Commit b1de32a

Browse files
committed
api: set rfc_7807_compliant_errors to true
It was a little tricky, but finally i figured out how to do it: https://api-platform.com/docs/core/errors/ The big impact is, that the serialization of the errors is now done with the same infrastructure as serializing entities. That makes it less format dependent. It seems a little weird that we use a provider to "replace" the exception path of the response by extracting the exception to the context, and then continuing like when we would serialize a response. But i can also see the benefits. See api-platform/core#5974 Issue: ecamp#6618
1 parent 9c2f1ad commit b1de32a

8 files changed

+191
-138
lines changed

api/config/packages/api_platform.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ api_platform:
3030
stateless: true
3131
extra_properties:
3232
standard_put: true
33-
rfc_7807_compliant_errors: false
33+
rfc_7807_compliant_errors: true
3434
pagination_enabled: false
3535
itemOperations: [ 'get', 'patch', 'delete' ]
3636
collection_operations:

api/config/services.yaml

+11-4
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,13 @@ services:
7373
App\Serializer\Normalizer\CollectionItemsNormalizer:
7474
decorates: 'api_platform.hal.normalizer.collection'
7575

76-
App\Serializer\Normalizer\TranslationConstraintViolationListNormalizer:
76+
App\State\ValidationErrorProvider:
77+
decorates: 'api_platform.validator.state.error_provider'
7778
arguments:
78-
- '@api_platform.hydra.normalizer.constraint_violation_list'
79-
- '@api_platform.problem.normalizer.constraint_violation_list'
79+
- '@.inner'
80+
- '@serializer.normalizer.error.translation_info_of_constraint_violation'
81+
- '@service.translation_to_all_locales'
82+
- '@serializer.name_converter.metadata_aware'
8083

8184
App\Serializer\SerializerContextBuilder:
8285
decorates: 'api_platform.serializer.context_builder'
@@ -137,13 +140,17 @@ services:
137140
- "%env(MAIL_FROM_EMAIL)%"
138141
- "%env(MAIL_FROM_NAME)%"
139142

140-
App\Service\TranslateToAllLocalesService:
143+
service.translation_to_all_locales:
144+
class: App\Service\TranslateToAllLocalesService
141145
public: true
142146
arguments:
143147
- '@translator'
144148
- '@translator'
145149
- '%env(csv:TRANSLATE_ERRORS_TO_LOCALES)%'
146150

151+
serializer.normalizer.error.translation_info_of_constraint_violation:
152+
class: App\Serializer\Normalizer\Error\TranslationInfoOfConstraintViolation
153+
147154
App\EventListener\JWTCreatedListener:
148155
tags:
149156
- { name: kernel.event_listener, event: lexik_jwt_authentication.on_jwt_created, method: onJWTCreated }

api/src/DTO/ValidationError.php

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace App\DTO;
4+
5+
use ApiPlatform\ApiResource\Error;
6+
use ApiPlatform\Metadata\ApiProperty;
7+
8+
class ValidationError extends Error {
9+
public function __construct(
10+
private readonly ?string $title,
11+
private readonly int $status,
12+
private readonly ?string $detail,
13+
private readonly ?string $instance,
14+
private readonly array $violations = [],
15+
private readonly string $type = 'about:blank',
16+
) {
17+
parent::__construct(
18+
type: $this->type,
19+
title: $this->title,
20+
status: $this->status,
21+
detail: $this->detail,
22+
instance: $this->instance,
23+
);
24+
}
25+
26+
#[ApiProperty]
27+
public function getViolations(): array {
28+
return $this->violations;
29+
}
30+
}

api/src/Serializer/Normalizer/Error/TranslationInfoOfConstraintViolation.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
namespace App\Serializer\Normalizer\Error;
44

5-
use Symfony\Component\Validator\ConstraintViolation;
5+
use Symfony\Component\Validator\ConstraintViolationInterface;
66

77
class TranslationInfoOfConstraintViolation {
8-
public function extract(ConstraintViolation $constraintViolation): TranslationInfo {
8+
public function extract(ConstraintViolationInterface $constraintViolation): TranslationInfo {
99
$constraint = $constraintViolation->getConstraint();
1010
$constraintClass = get_class($constraint);
1111
$key = str_replace('\\', '.', $constraintClass);

api/src/Serializer/Normalizer/TranslationConstraintViolationListNormalizer.php

-85
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
namespace App\Serializer\Normalizer;
4+
5+
use App\DTO\ValidationError;
6+
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
7+
8+
class ValidationErrorNormalizer implements NormalizerInterface {
9+
public const TYPE = 'type';
10+
public const TITLE = 'title';
11+
private array $defaultContext = [
12+
self::TYPE => 'https://tools.ietf.org/html/rfc2616#section-10',
13+
self::TITLE => 'An error occurred',
14+
];
15+
16+
public function normalize(mixed $data, ?string $format = null, array $context = []): null|array|\ArrayObject|bool|float|int|string {
17+
return [
18+
'type' => $context[self::TYPE] ?? $this->defaultContext[self::TYPE],
19+
'title' => $context[self::TITLE] ?? $this->defaultContext[self::TITLE],
20+
'detail' => $data->getDetail(),
21+
'violations' => $data->getViolations(),
22+
];
23+
}
24+
25+
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool {
26+
return $data instanceof ValidationError;
27+
}
28+
29+
public function getSupportedTypes(?string $format): array {
30+
return [ValidationError::class => true];
31+
}
32+
}
+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
namespace App\State;
4+
5+
use ApiPlatform\Metadata\HttpOperation;
6+
use ApiPlatform\Metadata\Operation;
7+
use ApiPlatform\State\ApiResource\Error;
8+
use ApiPlatform\State\ProviderInterface;
9+
use ApiPlatform\Validator\Exception\ValidationException;
10+
use App\DTO\ValidationError;
11+
use App\Serializer\Normalizer\Error\TranslationInfoOfConstraintViolation;
12+
use App\Service\TranslateToAllLocalesService;
13+
use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter;
14+
15+
/**
16+
* @psalm-suppress MissingTemplateParam
17+
*/
18+
class ValidationErrorProvider implements ProviderInterface {
19+
public function __construct(
20+
private readonly ProviderInterface $decorated,
21+
private readonly TranslationInfoOfConstraintViolation $translationInfoOfConstraintViolation,
22+
private readonly TranslateToAllLocalesService $translateToAllLocalesService,
23+
private readonly MetadataAwareNameConverter $nameConverter
24+
) {}
25+
26+
/**
27+
* @psalm-suppress InvalidReturnStatement
28+
*/
29+
public function provide(Operation $operation, array $uriVariables = [], array $context = []): null|array|object {
30+
$request = $context['request'];
31+
$exception = $request?->attributes->get('exception');
32+
if (!($request ?? null) || !$operation instanceof HttpOperation || null === $exception) {
33+
throw new \RuntimeException('Not an HTTP request');
34+
}
35+
36+
$status = $operation->getStatus() ?? 500;
37+
$error = Error::createFromException($exception, $status);
38+
if (!$exception instanceof ValidationException) {
39+
return $this->decorated->provide($operation, $uriVariables, $context);
40+
}
41+
42+
/**
43+
* @var ValidationException $exception
44+
*/
45+
$violationInfos = [];
46+
foreach ($exception->getConstraintViolationList() as $violation) {
47+
$violationInfo = $this->translationInfoOfConstraintViolation->extract($violation);
48+
$translations = $this->translateToAllLocalesService->translate(
49+
$violation->getMessageTemplate(),
50+
array_merge(
51+
$violation->getPlural() ? ['%count%' => $violation->getPlural()] : [],
52+
$violation->getParameters()
53+
)
54+
);
55+
56+
$propertyPath = $this->nameConverter->normalize($violation->getPropertyPath(), $violation->getRoot()::class, 'jsonproblem');
57+
58+
$violationInfos[] = [
59+
'code' => $violation->getCode(),
60+
'propertyPath' => $propertyPath,
61+
'message' => $violation->getMessage(),
62+
'i18n' => [
63+
...(array) $violationInfo,
64+
'translations' => $translations,
65+
],
66+
];
67+
}
68+
69+
return new ValidationError(
70+
type: $error->getType(),
71+
title: $error->getTitle(),
72+
status: $status,
73+
detail: $error->getDetail(),
74+
instance: $error->getInstance(),
75+
violations: $violationInfos
76+
);
77+
}
78+
}

0 commit comments

Comments
 (0)