You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Symfony/Doctrine实体监听器仅首个单元测试触发异常排查

Doctrine postPersist实体监听器触发异常问题

我在使用Doctrine事件监听器时遇到了极为棘手的异常问题,具体场景、配置与现象如下:

业务需求为在以下场景触发时,刷新用户所有已获取权限的汇总数据:

  • 用户账号创建完成
  • 为用户新增权限
  • 为用户关联的用户配置文件新增权限
  • 其他实体更新场景(仅上述三类场景即可稳定复现问题)

服务配置

我为对应实体配置了事件监听器,用于派发消息以重新计算权限汇总数据,服务配置如下:

# services.yaml
App\EventListener\DroitResume\ListenerOne:
    tags:
        - { name: 'doctrine.orm.entity_listener', event: 'postPersist', entity: 'App\Entity\Auth\UserAccount', method: 'postPersistUserAccount', lazy: true}
        - { name: 'doctrine.orm.entity_listener', event: 'postPersist', entity: 'App\Entity\Auth\UserRight', method: 'postChangeUserRight', lazy: true}
        - { name: 'doctrine.orm.entity_listener', event: 'postPersist', entity: 'App\Entity\Auth\UserProfileRight', method: 'postChangeUserProfileRight', lazy: true}
App\EventListener\DroitResume\ListenerTwo:
    tags:
        - { name: 'doctrine.orm.entity_listener', event: 'postPersist', entity: 'App\Entity\Auth\UserAccount', method: 'postPersistUserAccount', lazy: true}

说明:第二个监听器仅用于演示和测试,正式业务代码中仅保留一个监听器

监听器实现代码

// ListenerOne.php
class ListenerOne
{
    private MessageBusInterface $messageBus;

    public function __construct(MessageBusInterface $messageBus)
    {
        $this->messageBus = $messageBus;
    }

    public function postChangeUserRight(UserRight $userRight): void
    {
        print "ListenerOne => dispatch postChangeUserRight\n";
        $this->messageBus->dispatch(new UserRefreshResumeRightsMessage($userRight->getUserAccount()->getId()));
    }

    public function postChangeUserProfileRight(UserProfileRight $userProfileRight): void
    {
        print "ListenerOne => dispatch postChangeUserProfileRight\n";
        $this->messageBus->dispatch(new ProfileRefreshResumeRightsMessage($userProfileRight->getUserProfile()->getId()));
    }

    public function postPersistUserAccount(UserAccount $userAccount)
    {
        print "ListenerOne => dispatch postPersistUserAccount\n";
        $this->messageBus->dispatch(new UserRefreshResumeRightsMessage($userAccount->getId()));
    }
}
// ListenerTwo.php
class ListenerTwo
{
    private MessageBusInterface $messageBus;

    public function __construct(MessageBusInterface $messageBus)
    {
        $this->messageBus = $messageBus;
    }

    public function postPersistUserAccount(UserAccount $userAccount)
    {
        print "ListenerTwo => dispatch postPersistUserAccount\n";
        $this->messageBus->dispatch(new UserRefreshResumeRightsMessage($userAccount->getId()));
    }
}

单元测试代码

// RefreshRightTest.php
class RefreshRightTest extends MyTestCase {  //MyTestCase extends KernelTestCase 封装了通用测试方法
    public function testPersistUserOne() {
        $em = $this->getEntityManager();
        
        $user = new UserAccount();
        $user->setName('blabla One');
        $user->setEmail('blablaone@test.com');
        print "Persist User Blabla One\n";
        $em->persist($user);
        $em->flush();

        $user = new UserAccount();
        $user->setName('blabla Two');
        $user->setEmail('blablatwo@test.com');
        print "Persist User Blabla Two\n";
        $em->persist($user);
        $em->flush();
    }

    public function testPersistUserRightOne() {
        $em = $this->getEntityManager();
        $user = $this->getExistingUserFromName('Existing User One');
        
        $userRight = new UserRight();
        $userRight->setUserAccount($user);
        $userRight->setRightKey('RIGHT_ONE');
        $userRight->setValue(1);
        print "Persist UserRight One\n";
        $em->persist($userRight);
        $em->flush();
    }

    public function testPersistUserTwo() {
        $em = $this->getEntityManager();
        
        $user = new UserAccount();
        $user->setName('blabla Three');
        $user->setEmail('blablathree@test.com');
        print "Persist User Blabla Three\n";
        $em->persist($user);
        $em->flush();
    }

    public function testPersistUserRightTwo() {
        $em = $this->getEntityManager();
        $user = $this->getExistingUserFromName('Existing User Two');
        
        $userRight = new UserRight();
        $userRight->setUserAccount($user);
        $userRight->setRightKey('RIGHT_ANY');
        $userRight->setValue(2);
        print "Persist UserRight Two\n";
        $em->persist($userRight);
        $em->flush();
    }
}

异常现象

测试运行的控制台输出如下:

Persist User Blabla One
ListenerOne => dispatch postPersistUserAccount
ListenerTwo => dispatch postPersistUserAccount
Persist User Blabla Two
ListenerOne => dispatch postPersistUserAccount
ListenerTwo => dispatch postPersistUserAccount
Persist UserRight One
ListenerOne => dispatch postChangeUserRight
Persist User Blabla Three
Persist UserRight Two
ListenerOne => dispatch postChangeUserRight

核心异常现象:

  • 绑定UserAccount实体的postPersistUserAccount监听器仅在首个执行的单元测试中会被触发,一旦其他监听器被触发后,该监听器后续永远不会触发
  • 绑定UserRight、UserProfileRight实体的对应监听器可在所有测试中正常触发

已完成排查项

  • 所有相关实体上未配置其他任何事件监听器
  • 全量代码中不存在任何调用stopPropagation终止事件传播的逻辑
  • 实体间唯一差异为UserAccount实现了UserInterface接口(作为系统用户表实体)

复现规律

不同测试执行顺序下的复现规律:

顺序1:
T1: persist UserAccount
T2: persist UserRight
T3: persist UserAccount
=> postPersistUserAccount仅在T1测试中触发

顺序2:
T1: persist UserRight
T2: persist UserAccount
T3: persist UserAccount
T4: persist UserRight
=> postPersistUserAccount全程不触发,postPersistUserRight在T1、T4测试中按预期触发

目前已尝试多种排查方案均未定位问题根因,寻求同类问题的排查思路与解决方案。


内容的提问来源于stack exchange,提问作者Xavier Derrey

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 14:48:24