You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 08:57:15