Symfony与Doctrine中解析getChangeSet后的数据及事件订阅器问题
在Symfony + Doctrine中解析getChangeSet并集成到日志记录逻辑
看了你已经写好的ChangeLogListener代码,接下来咱们一步步完善它,把getChangeSet的解析逻辑无缝集成进去,完成完整的实体变更日志记录功能。
第一步:补全事件处理方法
首先把你没写完的postUpdate和onDelete方法补全,确保这两个事件能正确触发日志创建:
public function postUpdate(LifecycleEventArgs $args) { $entity = $args->getEntity(); // 避免循环记录ChangeLog自身的变更 if (!$entity instanceof ChangeLog) { $this->createLog($args, 'update'); } } public function onDelete(LifecycleEventArgs $args) { $entity = $args->getEntity(); if (!$entity instanceof ChangeLog) { $this->createLog($args, 'delete'); } }
第二步:核心实现createLog方法(含getChangeSet解析)
这是最关键的部分,我们要在createLog里处理不同操作类型(创建/更新/删除),尤其是更新操作时解析getChangeSet返回的变更数据,同时处理当前登录用户信息、格式化复杂字段值:
use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Security\Core\User\UserInterface; // ... 其他已有的use语句 ... private function createLog(LifecycleEventArgs $args, string $action) { $entity = $args->getEntity(); $entityManager = $args->getEntityManager(); $unitOfWork = $entityManager->getUnitOfWork(); // 1. 获取当前登录用户信息 $user = $this->tokenStorage->getToken()?->getUser(); $username = $user instanceof UserInterface ? $user->getUserIdentifier() : '匿名用户'; // 2. 解析不同操作类型的数据 $changeData = []; switch ($action) { case 'update': // 从UnitOfWork获取变更集:键是字段名,值为[旧值, 新值]数组 $changeSet = $unitOfWork->getChangeSet($entity); foreach ($changeSet as $field => [$oldValue, $newValue]) { // 跳过无需记录的字段(比如自动更新的时间戳、敏感字段等) if (in_array($field, ['updatedAt', 'lastModified', 'password'], true)) { continue; } // 格式化字段值,处理日期、关联实体等复杂类型 $changeData[$field] = [ 'old' => $this->formatValue($oldValue), 'new' => $this->formatValue($newValue) ]; } break; case 'creation': // 新增时记录实体的所有字段值(可按需筛选) $metadata = $entityManager->getClassMetadata(get_class($entity)); foreach ($metadata->getFieldNames() as $field) { $changeData[$field] = [ 'value' => $this->formatValue($metadata->getFieldValue($entity, $field)) ]; } break; case 'delete': // 删除时记录实体的唯一标识和关键信息 $identifier = $entityManager->getClassMetadata(get_class($entity))->getIdentifierValues($entity); $changeData['entity_identifier'] = $identifier; // 可额外添加实体的关键字段(比如用户实体的用户名) if (method_exists($entity, 'getUserIdentifier')) { $changeData['key_info'] = ['username' => $entity->getUserIdentifier()]; } break; } // 3. 创建并保存ChangeLog实体 $changeLog = new ChangeLog(); $changeLog->setAction($action); $changeLog->setEntityClass(get_class($entity)); $changeLog->setEntityId($this->getEntityId($entity, $entityManager)); $changeLog->setChangedData(json_encode($changeData)); $changeLog->setCreatedBy($username); $changeLog->setCreatedAt(new \DateTimeImmutable()); // 注意:如果当前处于事务中,flush会提交所有变更,也可以根据需求调整 $entityManager->persist($changeLog); $entityManager->flush(); }
第三步:添加辅助方法处理字段格式化和ID获取
为了让日志里的字段值更易读,我们需要处理日期、关联实体、数组等复杂类型,同时统一获取实体ID的逻辑:
/** * 格式化字段值,处理复杂类型 */ private function formatValue($value) { if ($value instanceof \DateTimeInterface) { return $value->format('Y-m-d H:i:s'); } // 支持有__toString方法的关联实体 if (is_object($value) && method_exists($value, '__toString')) { return (string)$value; } // 关联实体记录类名+ID if (is_object($value)) { return sprintf('%s#%s', get_class($value), $this->getEntityId($value)); } // 数组递归格式化 if (is_array($value)) { return array_map([$this, 'formatValue'], $value); } return $value; } /** * 获取实体的ID(兼容Doctrine元数据和约定俗成的getId方法) */ private function getEntityId($entity, ?EntityManagerInterface $entityManager = null) { if ($entityManager) { $metadata = $entityManager->getClassMetadata(get_class($entity)); $identifiers = $metadata->getIdentifierValues($entity); // 处理复合主键,返回字符串拼接形式 return implode('-', $identifiers); } // 没有EntityManager时,尝试调用getId方法 return method_exists($entity, 'getId') ? $entity->getId() : '未知ID'; }
关键注意事项
- 避免循环日志:一定要判断
!$entity instanceof ChangeLog,防止ChangeLog实体自身的变更触发无限循环日志。 - 敏感字段过滤:在
update分支里跳过密码、token等敏感字段,不要把敏感数据写入日志。 - 事务处理:如果你的业务逻辑已经在事务中,
flush()会提交所有变更,若不想影响原事务,可以考虑使用$entityManager->getConnection()->beginTransaction()单独处理日志,或者改用onFlush事件(需要额外处理变更集的获取逻辑)。 - 性能优化:如果某些实体不需要记录日志,可以在
createLog开头添加判断,比如if ($entity instanceof SkipLogInterface) return;,通过接口标记跳过。
内容的提问来源于stack exchange,提问作者Dialkord
相关产品推荐
相关产品推荐

