Doctrine/Symfony中孤儿移除、preRemove事件与软删除失效问题
解决Doctrine孤儿实体preRemove事件中字段无法持久化的问题
问题原因
当实体因orphanRemoval=true成为孤儿被标记删除时,Doctrine的执行流程是:在触发preRemove事件之前就已经生成了针对该实体的DELETE SQL语句。此时即便在preRemove中修改实体字段,Doctrine也不会跟踪这些变更——因为它认为该实体即将被删除,不会再同步任何字段更新到数据库。而普通实体的删除流程(显式调用$em->remove())中,Doctrine会在事件触发后重新校验变更,所以字段修改能生效。
解决方案:使用onFlush事件拦截并调整处理逻辑
onFlush事件在Doctrine生成SQL语句之前触发,此时可以捕获所有被计划删除的实体(包括孤儿实体),通过调整UnitOfWork的状态来实现字段持久化或软删除。
方案1:硬删除前先更新字段(适用于需留存删除记录后再删除的场景)
如果需要硬删除实体,但要先将deletedBy和deletedAt写入数据库,可在onFlush中先更新字段,再让Doctrine执行后续的DELETE操作:
#[AsDoctrineListener(event: Events::onFlush, priority: 500, connection: 'default')] class DeleteTrackingListener { private $security; public function __construct(Security $security) { $this->security = $security; } public function onFlush(OnFlushEventArgs $args): void { $em = $args->getEntityManager(); $uow = $em->getUnitOfWork(); $user = $this->security->getUser(); if (!$user) { return; } foreach ($uow->getScheduledEntityDeletions() as $entity) { // 仅处理包含删除跟踪字段的实体 if (method_exists($entity, 'setDeletedBy') && method_exists($entity, 'setDeletedAt')) { $entity->setDeletedBy($user->getId()); $entity->setDeletedAt(new DateTimeImmutable()); // 手动通知Doctrine重新计算实体变更集 $meta = $em->getClassMetadata(get_class($entity)); $uow->recomputeSingleEntityChangeSet($meta, $entity); } } } }
方案2:改为软删除(适用于需保留实体仅标记删除状态的场景)
如果不需要硬删除实体,仅需标记其为删除状态,可在onFlush中取消实体的删除计划,改为更新软删除字段:
// 先定义软删除接口,统一规范实体方法 interface SoftDeletable { public function setDeletedBy(?int $userId): void; public function setDeletedAt(?DateTimeImmutable $dateTime): void; } // 监听事件 #[AsDoctrineListener(event: Events::onFlush, priority: 500, connection: 'default')] class SoftDeleteOrphanListener { private $security; public function __construct(Security $security) { $this->security = $security; } public function onFlush(OnFlushEventArgs $args): void { $em = $args->getEntityManager(); $uow = $em->getUnitOfWork(); $user = $this->security->getUser(); if (!$user) { return; } $scheduledDeletions = $uow->getScheduledEntityDeletions(); foreach ($scheduledDeletions as $key => $entity) { if ($entity instanceof SoftDeletable) { // 从删除队列中移除实体,避免硬删除 unset($scheduledDeletions[$key]); $uow->setScheduledEntityDeletions($scheduledDeletions); // 设置软删除字段 $entity->setDeletedBy($user->getId()); $entity->setDeletedAt(new DateTimeImmutable()); // 通知Doctrine跟踪字段变更 $meta = $em->getClassMetadata(get_class($entity)); $uow->recomputeSingleEntityChangeSet($meta, $entity); } } } }
关键说明
recomputeSingleEntityChangeSet方法是核心:它会强制Doctrine重新计算实体的变更,将我们设置的字段更新加入到待执行的SQL队列中。- 若使用软删除方案,需确保实体的Repository或全局过滤条件(如Doctrine Filters)能自动排除已标记删除的实体,避免业务逻辑中获取到无效数据。
内容的提问来源于stack exchange,提问作者Rudgemen
相关产品推荐
相关产品推荐

