symfony7之使用Validator的三种方式
在App\Validator自定义如下Constraint
<?php
namespace App\Validator;
use Symfony\Component\Validator\Constraint;
#[\Attribute]
class RegistrationInformation extends Constraint
{
public function validatedBy(): string
{
return static::class . 'Validator';
}
public function getTargets(): string
{
return self::CLASS_CONSTRAINT;
}
}
<?php
namespace App\Validator;
use App\Entity;
use App\Enum\EventTypeEnum;
use App\Model\Dto\Input\RegistrationInformationDto;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class RegistrationInformationValidator extends ConstraintValidator
{
public function __construct(
private readonly EntityManagerInterface $em,
) {
}
/**
* @param RegistrationInformationDto $registrationInformationDto
*/
public function validate($registrationInformationDto, Constraint $constraint): void
{
//some validate logic here.
}
}
- 使用方式1
在DTO上
use App\Validator as MainAssert;
#[MainAssert\RegistrationInformation]
class RegistrationInformationDto
{
// define DTO here
}
在控制器上
public function process(
#[MapRequestPayload]
Dto\Input\RegistrationInformationDto $registrationInformationDto,
Registration\InformationService $informationService,
): JsonResponse {
//logic here.
}
那么 运行之后RegistrationInformation Validator 会自动生效
- 使用方式2
不加#[MapRequestPayload]
直接在servicce里:
use Symfony\Component\Validator\Validator\ValidatorInterface;
public function __construct(
private readonly ValidatorInterface $validator,
) {
}
$validationErrors = $this->validator->validate($registrationInformationDto);
也可以, 因为 $registrationInformationDto 这个DTO 前面使用了 #[MainAssert\RegistrationInformation] 注释, Validator 会根据 DTO 上的 attribute(如果有)自动查找所有的 constraint
- 使用方式3
加#[MapRequestPayload]
但是在DTO里没有
#[MainAssert\RegistrationInformation]
然后在service里
$violations = $this->validator->validate(['action' => $emailTemplateAction, 'order' => $order], new RegistrationInformation()); 也可以,当然还有 validateProperty 可以用
这种方式其实就是,使用了标准的validator结构:
$validator->validate(array, new Constraint())
上面就是通过 new RegistrationInformation() 这个Constraint 来验证 ['action' => $emailTemplateAction, 'order' => $order],和 DTO 没关系.
- 其实还有第四种方式
通过对DTO的resolver的方式
symfony7下有如下自定义resolver, 我认为实际功能包括
1.在request里做validate
2.动态修改请求参数
//基础类
<?php
namespace App\Model\Dto\Resolver;
use App\Model\Dto\Interface\ResolvableInterface;
use App\Service\AuthenticationService;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Validator\Exception\ValidationFailedException;
use Symfony\Component\Validator\Validator\ValidatorInterface;
/**
* @template TResolvableInterface as ResolvableInterface
*/
abstract class AbstractDtoResolver implements ValueResolverInterface
{
public function __construct(
protected readonly AuthenticationService $security,
protected readonly SerializerInterface $serializer,
protected readonly ValidatorInterface $validator,
) {
}
/**
* @return iterable<TResolvableInterface>
*/
public function resolve(Request $request, ArgumentMetadata $argument): iterable
{
if ($argument->getType() !== $this->getSupportedType()) {
return [];
}
$deserializedDto = $this->serializer->deserialize($request->getContent(), $this->getSupportedType(), 'json');
$dtoObject = $this->postResolve($deserializedDto);
$violations = $this->validator->validate($dtoObject);
if (\count($violations)) {
throw new HttpException(Response::HTTP_UNPROCESSABLE_ENTITY, implode("\n", array_map(static fn ($e) => $e->getMessage(), iterator_to_array($violations))), new ValidationFailedException($request->getPayload(), $violations));
}
return [$dtoObject];
}
/**
* @param TResolvableInterface $resolvable
*
* @return TResolvableInterface
*/
abstract protected function postResolve(ResolvableInterface $resolvable): ResolvableInterface;
abstract protected function getSupportedType(): string;
}
// 次基础类
<?php
namespace App\Model\Dto\Resolver;
use App\Model\Dto\Interface\ResolvableInterface;
use App\Service\AuthenticationService;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
/**
* @extends AbstractDtoResolver<ResolvableInterface>
*/
abstract class AbstractRequestDtoResolver extends AbstractDtoResolver
{
public function __construct(
private readonly RequestStack $requestStack,
AuthenticationService $security,
SerializerInterface $serializer,
ValidatorInterface $validator,
) {
parent::__construct($security, $serializer, $validator);
}
protected function getRequest(): ?Request
{
return $this->requestStack->getCurrentRequest();
}
}
//使用
<?php
namespace App\Model\Dto\Resolver;
use App\Model\Dto\Input\Submission\Assignment\MakeRecommendationDto;
use App\Model\Dto\Interface\ResolvableInterface;
use PHPUnit\Framework\Attributes\UsesClass;
#[UsesClass(MakeRecommendationDto::class)]
class MakeRecommendationDtoResolver extends AbstractRequestDtoResolver
{
protected function getSupportedType(): string
{
return MakeRecommendationDto::class;
}
protected function postResolve(ResolvableInterface $resolvable): ResolvableInterface
{
$request = $this->getRequest();
if (null !== $request && $resolvable instanceof MakeRecommendationDto) {
$assignmentId = $request->attributes->get('assignment');//这个其实路由参数,赋值给DTO
if (null !== $assignmentId) {
$resolvable->setAssignmentId($assignmentId);
}
}
return $resolvable;
}
}
然后在控制器里,直接使用 MakeRecommendationDto, 不需要 #[MapRequestPayload], 但是需要在 MakeRecommendationDto 前面添加类的 #[MainAssert\xxxxx],这个其实和前面的方式2 很像,但是却是在 resolver 下生效的
上面代码其实具体实现了如下两个功能
1️⃣ 主要职责
AbstractDtoResolver + AbstractRequestDtoResolver + MakeRecommendationDtoResolver 组合起来做了两个核心工作:
(A) 请求内容的验证(Validation)
resolve()方法会:- 反序列化请求的 JSON body 到指定 DTO 类:
$deserializedDto = $this->serializer->deserialize($request->getContent(), $this->getSupportedType(), 'json'); - 调用
postResolve()做进一步处理(可选)。 - 使用 Symfony Validator 验证 DTO:
$violations = $this->validator->validate($dtoObject); if (\count($violations)) { throw new HttpException(...); }
- 反序列化请求的 JSON body 到指定 DTO 类:
- 如果验证失败,会抛出
422 Unprocessable Entity并附带详细的验证错误。
(B) 动态修改请求参数 / DTO 填充
postResolve()是抽象方法,由子类实现。MakeRecommendationDtoResolver中实现了:$assignmentId = $request->attributes->get('assignment'); if (null !== $assignmentId) { $resolvable->setAssignmentId($assignmentId); }这实际上是根据请求的路由参数动态给 DTO 填充
assignmentId。
🔹 注意:这里修改的是 DTO 对象,而不是直接修改
$request->request。但由于 Controller 会直接接收这个 DTO 参数,所以从 Controller 角度看,参数已经被动态修改了。后面控制器里面可以直接使用这个 resolve 后的DTO
2️⃣ 工作流程总结
- Symfony 解析 Controller 参数时,会调用对应的
ValueResolver。 AbstractDtoResolver检查 Controller 参数类型是否匹配。- JSON body → DTO(反序列化)。
postResolve()做额外处理,例如动态填充 DTO 字段。- Validator 校验 DTO。
- 返回 DTO,注入到 Controller 方法参数中。
3️⃣ 小结
你的理解几乎正确:
- ✅ 在 request 里做 validate —
ValidatorInterface校验 DTO。 - ✅ 动态修改请求参数 — 通过
postResolve()给 DTO 动态赋值,通常基于路由参数或其它 request 属性。 额外补充:
- 这种方式可以把 DTO 处理逻辑完全抽象出去,Controller 只需要接收 DTO,干净简洁。
- 也保证了请求在进入 Controller 前就被验证和准备好。
关于DTO上同时使用了自定义的Constraint和属性上使用了字通自带的验证的话,那么它们的执行顺序是什么样的呢
use Symfony\Component\Validator\Constraints as Assert;
use App\Validator as MainAssert;
#[MainAssert\RegistrationInformation]
class RegistrationInformationDto
{
#[Assert\NotBlank(message: 'Password cannot be blank')]
private readonly string $password;
// 其他字段
}
执行顺序如下:
字段级别(Property constraints)
逐个属性执行验证,如 #[Assert\NotBlank]、#[Assert\Email] 等。
如果字段验证失败,返回字段的 ConstraintViolation。
类级别(Class constraints)
在所有字段验证之后,执行如 #[RegistrationInformation] 这类作用在类上的 constraint。
为什么是这个顺序?
字段级别先运行,可以优先捕捉输入格式类错误,避免在类级别中处理无效字段数据;
类级别更适合进行组合判断或逻辑验证(如 if A then B),这些往往依赖字段的前置有效性。
