如何使用symfony/validator验证不可更新字段
解决Doctrine实体不可更新字段的前置校验问题
你遇到的问题是#[ORM\Column(updatable: false)]仅控制Doctrine生成UPDATE语句时不包含该字段,但不会阻止代码层面修改实体属性,也不会在persist阶段触发校验。以下是几种在SQL执行前检测非法更新的方案:
方案一:在Setter方法中直接校验
最直接的方式是修改实体的setJoinedAt方法,当字段已有值时抛出异常,从源头阻止修改:
public function setJoinedAt(\DateTimeImmutable $joinedAt): self { if ($this->joinedAt !== null) { throw new \RuntimeException('joinedAt字段一旦设置后无法修改'); } $this->joinedAt = $joinedAt; return $this; }
对应的PHPUnit测试可以直接捕捉这个异常:
public function testInvalidJoinedAt(): void { $manager = static::getContainer()->get(EntityManagerInterface::class); $now = new DateTimeImmutable(); $existingUser = $this->getRepository(UserRepository::class)->find(0); $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('joinedAt字段一旦设置后无法修改'); $existingUser->setJoinedAt($now); }
方案二:自定义Symfony校验断言
如果你想类似其他校验注解那样统一管理,可以自定义一个NotUpdatable断言:
1. 创建约束类
// src/Validator/Constraints/NotUpdatable.php namespace App\Validator\Constraints; use Symfony\Component\Validator\Constraint; #[Attribute] class NotUpdatable extends Constraint { public string $message = '字段 {{ property }} 不允许被修改'; public function getTargets(): string|array { return self::PROPERTY_CONSTRAINT; } }
2. 创建校验器类
// src/Validator/Constraints/NotUpdatableValidator.php namespace App\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; class NotUpdatableValidator extends ConstraintValidator { public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof NotUpdatable) { throw new UnexpectedTypeException($constraint, NotUpdatable::class); } // 仅当实体已持久化(存在ID)且字段原有值与新值不同时触发校验 $entity = $this->context->getObject(); if ($entity->getId() !== null && $this->context->getOriginalValue() !== null && $value !== $this->context->getOriginalValue()) { $this->context->buildViolation($constraint->message) ->setParameter('{{ property }}', $this->context->getPropertyName()) ->addViolation(); } } }
3. 在实体字段上使用
#[ORM\Column(updatable: false)] #[Assert\Type( type: 'object', message: '值 {{ value }} 不是有效的 {{ type }} 类型' )] #[App\Validator\Constraints\NotUpdatable] private ?\DateTimeImmutable $joinedAt = null;
4. 编写测试验证
手动触发校验或通过flush触发:
public function testInvalidJoinedAt(): void { $manager = static::getContainer()->get(EntityManagerInterface::class); $validator = static::getContainer()->get(ValidatorInterface::class); $now = new DateTimeImmutable(); $existingUser = $this->getRepository(UserRepository::class)->find(0); $existingUser->setJoinedAt($now); // 手动触发校验 $violations = $validator->validate($existingUser); $this->assertCount(1, $violations); $this->assertEquals('字段 joinedAt 不允许被修改', $violations[0]->getMessage()); // 或验证flush时抛出校验异常 $this->expectException(\Symfony\Component\Validator\Exception\ValidationException::class); $manager->flush(); }
方案三:验证数据库最终状态
如果只需要确保字段不会被更新到数据库,可以测试flush后的数据是否保持原值:
public function testJoinedAtRemainsUnchanged(): void { $manager = static::getContainer()->get(EntityManagerInterface::class); $now = new DateTimeImmutable(); $existingUser = $this->getRepository(UserRepository::class)->find(0); $originalJoinedAt = $existingUser->getJoinedAt(); $existingUser->setJoinedAt($now); $manager->persist($existingUser); $manager->flush(); // 重新从数据库获取验证 $updatedUser = $this->getRepository(UserRepository::class)->find(0); $this->assertEquals($originalJoinedAt, $updatedUser->getJoinedAt()); }
内容的提问来源于stack exchange,提问作者Naico04
相关产品推荐
相关产品推荐

