软删除本质是将DELETE转为UPDATE并添加deletedAt字段,需配合DQL过滤器或自定义Repository;Bundle方案(stof/doctrine-extensions-bundle v1.9+)最省力但需注意Symfony 6.2+/Doctrine 3.x兼容性;手动实现更可控,须禁用PreRemove默认行为、重写查询方法,并谨慎处理关联关系与级联逻辑。

软删除不是数据库层面的 DELETE
Symfony 6 本身不提供软删除功能,它依赖 Doctrine ORM 的 SoftDeleteable 扩展(来自 stof/doctrine-extensions-bundle)或手动实现。直接调用 $em->remove($entity) 仍会触发物理删除,除非你显式禁用或重写行为。
核心前提是:软删除本质是把 DELETE 转成 UPDATE,给实体加一个标记字段(如 deletedAt),再让所有查询自动过滤掉已“删除”的记录。
- 不能只改实体类加字段——必须配合 Doctrine 的 DQL 过滤器或自定义 Repository 方法
- Bundle 方案(
stof/doctrine-extensions-bundle)在 Symfony 6.2+ 中需注意 PHP 8.1+ 兼容性和配置方式变更 - 如果你用的是 Doctrine 3.x(Symfony 6 默认),
stof/doctrine-extensions-bundle的v1.9+才支持
用 stof/doctrine-extensions-bundle 实现(推荐)
这是最省力且被广泛验证的方式,但安装和配置稍有门槛:
- 运行
composer require stof/doctrine-extensions-bundle - 确认
config/packages/stof_doctrine_extensions.yaml中启用了softdeleteable:stof_doctrine_extensions: default_locale: en orm: default: softdeleteable: true - 在实体上加注解:
#[ORM\Entity] #[Gedmo\SoftDeleteable(fieldName: "deletedAt", timeAware: false)] class Post { #[ORM\Column(type: 'datetime_immutable', nullable: true)] private ?\DateTimeImmutable $deletedAt = null; - 注意
timeAware: false表示不自动设时间戳,你得在业务逻辑里手动赋值$entity->setDeletedAt(new \DateTimeImmutable());设为true则由扩展自动处理
手动实现更可控,也更易调试
跳过 Bundle,自己控制逻辑更清晰,尤其适合需要多条件软删(如按用户角色、租户隔离)的场景:
- 在实体中添加
deletedAt字段和对应 getter/setter - 禁用默认删除行为:
#[ORM\PreRemove] public function onPreRemove(): void { $this->deletedAt = new \DateTimeImmutable(); // 不调用 $this->entityManager->flush() —— 让上层决定是否 flush } - 在 Repository 中重写
findAll()或新增findActive():public function findActive(): array { return $this->createQueryBuilder('p') ->where('p.deletedAt IS NULL') ->getQuery() ->getResult(); } - 全局查询过滤需启用 Doctrine 过滤器(
doctrine.orm.filters.soft_delete),否则关联查询(如Post->comments)仍可能拉出已软删数据
关联关系与级联删除要特别小心
Doctrine 的 cascade={"remove"} 默认走物理删除,软删除下它毫无意义,甚至引发意外数据丢失:
- 不要在关联映射中写
cascade={"remove"},改用业务层手动处理子项软删 - 例如删除
Post时,需显式遍历并设置每个Comment的deletedAt - 使用
orphanRemoval=true也不适用——它只响应集合变更,不响应软删标记 - 如果用 DQL 过滤器,记得在
Query创建前启用:$em->getFilters()->enable('soft_delete');,否则JOIN查询可能漏过滤
真正麻烦的从来不是加个字段,而是让整个应用——包括表单提交、API 返回、管理后台列表、搜索索引、缓存失效——都一致地理解“已删除”只是状态切换,而不是数据消失。


















