Symfony5中Doctrine PrePersist/PreUpdate事件触发异常问题
你遇到的核心问题很明确:Doctrine的PreUpdate事件只会跟踪被映射到数据库的实体字段变更,而你用来接收输入的plainPassword是临时字段(没有添加@ORM\Column注解,不属于Doctrine的跟踪范围)。所以当你只修改plainPassword时,Doctrine检测不到任何数据库映射字段的变化,自然不会触发PreUpdate事件。
另外还有一种可能:如果你是在表单提交后、Doctrine变更检测完成之后才把plainPassword加密为password字段(这个是Doctrine映射的字段),Doctrine也会错过这个字段的变更,导致事件不触发。
1. 提前处理密码加密,让Doctrine捕捉到字段变更
当提交包含plainPassword的表单时,一定要在Doctrine执行变更检测之前,把加密后的密码设置到实体的password字段(这个是被Doctrine映射到数据库的字段)。
比如在控制器里结合Symfony的UserPasswordHasherInterface处理:
// 控制器中的更新逻辑 $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { /** @var YourEntity $entity */ $entity = $form->getData(); // 如果plainPassword有值,就加密后设置到password字段 if ($plainPassword = $entity->getPlainPassword()) { $hashedPassword = $passwordHasher->hashPassword($entity, $plainPassword); $entity->setPassword($hashedPassword); // 此时Doctrine已经能检测到password字段的变化 } $entityManager->flush(); // ... 后续跳转逻辑 }
2. 修正实体内部PreUpdate的处理逻辑(如果用生命周期回调)
如果坚持使用实体内部的@ORM\HasLifecycleCallbacks,要注意:在PreUpdate事件中修改实体字段后,需要手动通知Doctrine重新计算变更集,否则修改的时间戳不会被保存。调整你的setModifiedTime方法:
/** * @ORM\PrePersist * @ORM\PreUpdate */ public function setModifiedTime(): void { $this->modifiedTime = new \DateTimeImmutable(); // 仅在PreUpdate时需要这步:告诉Doctrine字段已变更 if ($this->id !== null) { // 有id说明是更新操作 $entityManager = \Doctrine\ORM\EntityManager::create( $this->getEntityManager()->getConnection(), $this->getEntityManager()->getConfiguration() ); $entityManager->getUnitOfWork()->recomputeSingleEntityChangeSet( $entityManager->getClassMetadata(self::class), $this ); } }
不过更推荐用下面的全局事件订阅者方案,代码更清晰且解耦。
3. 可选:用全局事件订阅者替代实体内部回调
创建一个Doctrine事件订阅者,专门处理时间戳逻辑,这样不仅解耦实体代码,还能更灵活地控制触发时机:
// src/EventListener/TimestampSubscriber.php namespace App\EventListener; use Doctrine\Bundle\DoctrineBundle\EventSubscriber\EventSubscriberInterface; use Doctrine\ORM\Events; use Doctrine\Persistence\Event\LifecycleEventArgs; use App\Entity\YourEntity; class TimestampSubscriber implements EventSubscriberInterface { public function getSubscribedEvents(): array { return [ Events::prePersist, Events::preUpdate, ]; } public function prePersist(LifecycleEventArgs $args): void { $this->updateModifiedTime($args); } public function preUpdate(LifecycleEventArgs $args): void { $this->updateModifiedTime($args); } private function updateModifiedTime(LifecycleEventArgs $args): void { $entity = $args->getObject(); // 只处理你的目标实体 if (!$entity instanceof YourEntity) { return; } $entity->setModifiedTime(new \DateTimeImmutable()); // 通知Doctrine重新计算变更集 $args->getObjectManager()->getUnitOfWork()->recomputeSingleEntityChangeSet( $args->getObjectManager()->getClassMetadata(get_class($entity)), $entity ); } }
这个订阅者会自动监听所有实体的prePersist和preUpdate事件,只对你的目标实体生效,逻辑更清晰。
4. 验证实体字段配置
最后确认你的modifiedTime字段注解配置正确:
/** * @ORM\Column(type="datetime_immutable") */ private \DateTimeImmutable $modifiedTime; public function setModifiedTime(\DateTimeImmutable $modifiedTime): self { $this->modifiedTime = $modifiedTime; return $this; }
核心就是让Doctrine检测到数据库映射字段的变更:当你提交plainPassword时,必须先把它加密为password字段(Doctrine跟踪的字段),这样Doctrine才会认为实体有更新,进而触发PreUpdate事件。如果在PreUpdate中修改了实体字段,一定要手动通知Doctrine重新计算变更集,否则修改不会被持久化。
内容的提问来源于stack exchange,提问作者Kevin

