Api Platform 3.x中覆盖PUT操作时原对象被修改的问题排查
问题:ApiPlatform 3.x PUT操作中,原对象意外被修改(期望仅创建新对象)
我需要自定义PUT操作的逻辑:当传入的请求数据与数据库中原对象存在差异时,创建一个新对象返回,完全不修改原对象。目前请求能成功生成新对象,但原对象也被同步修改了——即使我克隆了传入的对象并仅处理克隆实例,还尝试过refresh()和detach()方法,问题依然存在。使用的是ApiPlatform 3.0.2版本。
实体代码
#[ApiResource( operations: [ new Get(), new GetCollection(), new Post(controller: CreateAction::class), new Put(processor: EntityStateProcessor::class), ], paginationEnabled: false )] class Entity
自定义Processor代码
final class PageStateProcessor implements ProcessorInterface { private ProcessorInterface $decorated; private EntityCompare $entityCompare; public function __construct(ProcessorInterface $decorated, EntityCompare $entityCompare) { $this->decorated = $decorated; $this->entityCompare = $entityCompare; } public function process($data, Operation $operation, array $uriVariables = [], array $context = []) { if (($this->entityCompare)($data)) { // 检查对象是否有变更 $new_entity = clone $data; // (无ID) // 对新对象执行操作 return $this->decorated->process($new_entity, $operation, $uriVariables, $context); } return $data; } }
尝试过的无效方法
$this->entityManager->refresh($data);:试图用数据库数据刷新原对象,避免被请求数据修改$this->entityManager->getUnitOfWork()->detach($data);:试图让原对象脱离EntityManager管理,不被更新
问题原因
ApiPlatform的请求处理流程中,在进入自定义Processor之前,规范化器(Normalizer)已经把请求体的字段值映射到了从数据库加载的原对象实例上。也就是说,你在process()方法中接收的$data,已经是被请求数据修改后的原对象——此时EntityManager的UnitOfWork已经追踪到了原对象的变更,后续即使克隆或者操作新对象,原对象的变更依然会在事务提交时被同步到数据库。
你的克隆操作只是复制了已修改的原对象,并没有阻止原对象本身的变更被提交;而refresh()/detach()执行时机太晚,UnitOfWork已经记录了原对象的变更状态。
解决方案
核心思路是:先获取数据库中未被修改的原对象副本,对比后创建新对象,再重置原对象的状态以取消变更追踪。
修改后的Processor代码
use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; final class PageStateProcessor implements ProcessorInterface { private ProcessorInterface $decorated; private EntityCompare $entityCompare; private EntityManagerInterface $entityManager; public function __construct( ProcessorInterface $decorated, EntityCompare $entityCompare, EntityManagerInterface $entityManager ) { $this->decorated = $decorated; $this->entityCompare = $entityCompare; $this->entityManager = $entityManager; } public function process($data, Operation $operation, array $uriVariables = [], array $context = []) { // 1. 从数据库加载原对象的纯净副本(未被请求数据修改) $entityClass = get_class($data); $originalEntity = $this->entityManager->getRepository($entityClass)->find($uriVariables['id']); if (!$originalEntity) { throw new NotFoundHttpException('原对象不存在'); } // 2. 对比原对象与已被修改的传入对象 if ($this->entityCompare->hasChanges($originalEntity, $data)) { // 3. 创建新对象,复制修改后的属性 $newEntity = clone $originalEntity; $propertyAccessor = PropertyAccess::createPropertyAccessor(); // 遍历属性,把修改后的值复制到新对象(跳过ID) $reflection = new \ReflectionClass($entityClass); foreach ($reflection->getProperties() as $property) { $propertyName = $property->getName(); if ($propertyName === 'id') { $newEntity->setId(null); // 清除ID,触发创建逻辑 continue; } $propertyAccessor->setValue($newEntity, $propertyName, $propertyAccessor->getValue($data, $propertyName)); } // 4. 重置原对象状态,取消EntityManager的变更追踪 $this->entityManager->refresh($originalEntity); // 5. 仅处理新对象,返回创建结果 return $this->decorated->process($newEntity, $operation, $uriVariables, $context); } // 无变更时返回原对象 return $originalEntity; } }
调整EntityCompare逻辑
需要把原来的EntityCompare从接收单个对象,改成接收两个对象对比(原对象和修改后的对象),比如:
final class EntityCompare { private EntityManagerInterface $entityManager; public function __construct(EntityManagerInterface $entityManager) { $this->entityManager = $entityManager; } public function hasChanges($original, $modified): bool { // 利用Doctrine内置的变更检测 $uow = $this->entityManager->getUnitOfWork(); $uow->computeChangeSets(); $changes = $uow->getEntityChangeSet($modified); return !empty($changes); } }
关键说明
- 提前获取原对象副本:确保对比的是数据库中的原始数据和请求修改后的数据,而不是已经被修改的原对象实例。
- 重置原对象状态:
refresh()会从数据库重新加载原对象的数据,覆盖当前实例的修改,同时清除UnitOfWork中的变更记录,确保原对象不会被更新。 - 属性复制方式:使用
PropertyAccessor可以兼容私有/受保护属性,避免手动赋值的繁琐。
内容的提问来源于stack exchange,提问作者emomaliev
相关产品推荐
相关产品推荐

