我正在开发 Symfony 7 REST API。我正在按照官方文档实现自定义参数解析器来处理我的端点接收到的请求有效负载格式到 DTO 的映射(https://symfony.com/doc/current/controller/value_resolver.html#adding-自定义值解析器)。
这是端点请求负载的示例
{
"article": {
"title": "How to train your dragon",
"description": "Ever wonder how?",
"body": "You have to believe"
}
}
这是自定义的 ValueResolver
class NestedJsonValueResolver implements ValueResolverInterface
{
public function __construct(
private readonly SerializerInterface $serializer,
private readonly ValidatorInterface $validator,
) {
}
public function resolve(Request $request, ArgumentMetadata $argument): iterable
{
$argumentType = $argument->getType();
if (!in_array('NestedJsonDtoInterface', class_implements($argumentType))) {
return [];
}
return [$this->mapRequestPayload($request, $argumentType)];
}
/**
* @param Request $request
* @param class-string<NestedJsonDtoInterface> $type
*/
private function mapRequestPayload(Request $request, string $type): ?object
{
if (null === $format = $request->getContentTypeFormat()) {
throw new HttpException(Response::HTTP_UNSUPPORTED_MEDIA_TYPE, 'Unsupported format.');
}
if ('' === $data = $request->getContent()) {
return null;
}
try {
$nestedJsonObjectKey = $type::getNestedJsonObjectKey();
$decodedData = json_decode($data, true);
$nestedJsonObject = $decodedData[$nestedJsonObjectKey];
$payload = $this->serializer->deserialize($nestedJsonObject, $type, 'json');
$violations = $this->validator->validate($payload);
if ($violations->count() > 0) {
throw new HttpException(Response::HTTP_UNPROCESSABLE_ENTITY, implode("\n", array_map(static fn ($e) => $e->getMessage(), iterator_to_array($violations))), new ValidationFailedException($payload, $violations));
}
return $payload;
} catch (UnsupportedFormatException $e) {
throw new HttpException(Response::HTTP_UNSUPPORTED_MEDIA_TYPE, sprintf('Unsupported format: "%s".', $format), $e);
} catch (NotEncodableValueException $e) {
throw new HttpException(Response::HTTP_BAD_REQUEST, sprintf('Request payload contains invalid "%s" data.', $format), $e);
}
}
}
这是我的 services.yaml
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
App\ValueResolver\NestedJsonValueResolver:
tags:
- controller.argument_value_resolver:
name: nested_json
priority: 150
这是控制器操作
#[Route('', methods: ['POST'])]
public function store(
#[ValueResolver('nested_json')]
StoreArticleDto $dto
): JsonResponse
{
$article = $this->articleService->store($dto);
return $this->json([
'article' => $article,
], 201);
}
这是目标 DTO
final readonly class StoreArticleDto implements NestedJsonDtoInterface
{
private const NESTED_JSON_OBJECT_KEY = 'article';
public function __construct(
#[Assert\NotNull]
#[Assert\Type('string')]
private ?string $title,
#[Assert\NotNull]
#[Assert\Type('string')]
private ?string $body,
#[Assert\NotNull]
#[Assert\Type('string')]
private ?string $description,
)
{
}
public static function getNestedJsonObjectKey(): string
{
return self::NESTED_JSON_OBJECT_KEY;
}
public function getTitle(): ?string
{
return $this->title;
}
public function getBody(): ?string
{
return $this->body;
}
public function getDescription(): ?string
{
return $this->description;
}
}
到达端点后,将引发以下异常。
我应该如何让自定义 ValueResolver 工作?
使用这些嵌套DTO的目的是什么?您计划向其中添加其他属性还是只是多余的嵌套?
我的观点是,如果不需要嵌套,就不要嵌套。该路线清楚地表明您正在与哪个实体合作。如果你放弃嵌套并制作如下格式:
{
"title": "How to train your dragon",
"description": "Ever wonder how?",
"body": "You have to believe"
}
您可以使用框架本机
#[MapRequestPayload]
属性来解决到 DTO 的映射并处理验证。您可以放弃自定义解析器,而不必担心维护额外的代码。控制器看起来像
#[Route('', methods: ['POST'])]
public function store(
#[MapRequestPayload]
StoreArticleDto $dto
): JsonResponse
{
$article = $this->articleService->store($dto);
return $this->json([
'article' => $article,
], 201);
}
如果您需要嵌套,您可以通过将
StoreArticleDto
嵌套在另一个 DTO StoreArticleRequest
中来解决此问题,这将使您能够在将来添加属性,同时保留 #[MapRequestPayload]
属性。
use Symfony\Component\Validator\Constraints as Assert;
class StoreArticleRequest
{
#[Assert\NotNull]
private StoreArticleDto $article;
public function getArticle(): StoreArticleDto
{
return $this->article;
}
public function setArticle(StoreArticleDto $article): void
{
$this->article = $article;
}
}
在控制器中,您只需反序列化为
StoreArticleRequest
#[Route('', methods: ['POST'])]
public function store(
#[MapRequestPayload]
StoreArticleRequest $dto
): JsonResponse
{
$article = $this->articleService->store($dto->article);
return $this->json([
'article' => $article,
], 201);
}
无论如何,我建议在一周中的任何一天使用标准
#[MapRequestPayload]
属性而不是自定义属性。 这是 6.3 中引入的功能,因为您使用的是 7.0,所以没问题。只需确保您安装了 symfony/serializer-pack
和 symfony/validator
即可正常工作。