指尖上的记忆指尖上的记忆
首页
  • 基础
  • Laravel框架
  • Symfony框架
  • 基础
  • Gin框架
  • 基础
  • Spring框架
  • 命令
  • Nginx
  • Ai
  • Deploy
  • Docker
  • K8s
  • Micro
  • RabbitMQ
  • Mysql
  • PostgreSsql
  • Redis
  • MongoDb
  • Html
  • Js
  • 前端
  • 后端
  • Git
  • 知识扫盲
  • Golang
🌟 gitHub
首页
  • 基础
  • Laravel框架
  • Symfony框架
  • 基础
  • Gin框架
  • 基础
  • Spring框架
  • 命令
  • Nginx
  • Ai
  • Deploy
  • Docker
  • K8s
  • Micro
  • RabbitMQ
  • Mysql
  • PostgreSsql
  • Redis
  • MongoDb
  • Html
  • Js
  • 前端
  • 后端
  • Git
  • 知识扫盲
  • Golang
🌟 gitHub
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() 方法会:
    1. 反序列化请求的 JSON body 到指定 DTO 类:
      $deserializedDto = $this->serializer->deserialize($request->getContent(), $this->getSupportedType(), 'json');
      
    2. 调用 postResolve() 做进一步处理(可选)。
    3. 使用 Symfony Validator 验证 DTO:
      $violations = $this->validator->validate($dtoObject);
      if (\count($violations)) {
          throw new HttpException(...);
      }
      
  • 如果验证失败,会抛出 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️⃣ 工作流程总结
  1. Symfony 解析 Controller 参数时,会调用对应的 ValueResolver。
  2. AbstractDtoResolver 检查 Controller 参数类型是否匹配。
  3. JSON body → DTO(反序列化)。
  4. postResolve() 做额外处理,例如动态填充 DTO 字段。
  5. Validator 校验 DTO。
  6. 返回 DTO,注入到 Controller 方法参数中。
3️⃣ 小结

你的理解几乎正确:

  1. ✅ 在 request 里做 validate — ValidatorInterface 校验 DTO。
  2. ✅ 动态修改请求参数 — 通过 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),这些往往依赖字段的前置有效性。