diff --git a/config/orm.php b/config/orm.php
index 819c10f72..b26a7af91 100644
--- a/config/orm.php
+++ b/config/orm.php
@@ -4,6 +4,8 @@
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
+use Doctrine\Bundle\DoctrineBundle\Command\DebugEntityListenersDoctrineCommand;
+use Doctrine\Bundle\DoctrineBundle\Command\DebugEventManagerDoctrineCommand;
use Doctrine\Bundle\DoctrineBundle\Command\ImportMappingDoctrineCommand;
use Doctrine\Bundle\DoctrineBundle\ManagerConfigurator;
use Doctrine\Bundle\DoctrineBundle\Mapping\ContainerEntityListenerResolver;
@@ -359,6 +361,18 @@
])
->tag('console.command', ['command' => 'doctrine:schema:validate'])
+ ->set('doctrine.event_manager_debug_command', DebugEventManagerDoctrineCommand::class)
+ ->args([
+ service('doctrine'),
+ ])
+ ->tag('console.command', ['command' => 'doctrine:debug:event-manager'])
+
+ ->set('doctrine.entity_listeners_debug_command', DebugEntityListenersDoctrineCommand::class)
+ ->args([
+ service('doctrine'),
+ ])
+ ->tag('console.command', ['command' => 'doctrine:debug:entity-listeners'])
+
->set('doctrine.mapping_import_command', ImportMappingDoctrineCommand::class)
->args([
service('doctrine'),
diff --git a/src/Command/DebugEntityListenersDoctrineCommand.php b/src/Command/DebugEntityListenersDoctrineCommand.php
new file mode 100644
index 000000000..7c9e347f7
--- /dev/null
+++ b/src/Command/DebugEntityListenersDoctrineCommand.php
@@ -0,0 +1,147 @@
+setName('doctrine:debug:entity-listeners')
+ ->setDescription('Lists entity listeners for a given entity')
+ ->addArgument('entity', InputArgument::OPTIONAL, 'The fully-qualified entity class name')
+ ->addArgument('event', InputArgument::OPTIONAL, 'The event name to filter by (e.g. postPersist)')
+ ->setHelp(<<<'EOT'
+The %command.name% command lists all entity listeners for a given entity:
+
+ php %command.full_name% 'App\Entity\User'
+
+To show only listeners for a specific event, pass the event name:
+
+ php %command.full_name% 'App\Entity\User' postPersist
+EOT);
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $io = new SymfonyStyle($input, $output);
+
+ $entityName = $input->getArgument('entity');
+
+ if ($entityName === null) {
+ $entityName = $io->choice('Which entity do you want to list listeners for?', $this->listAllEntities());
+ }
+
+ $entityName = ltrim($entityName, '\\');
+ $entityManager = $this->getDoctrine()->getManagerForClass($entityName);
+
+ if ($entityManager === null) {
+ $io->error(sprintf('No entity manager found for class "%s".', $entityName));
+
+ return self::FAILURE;
+ }
+
+ $classMetadata = $entityManager->getClassMetadata($entityName);
+ assert($classMetadata instanceof ClassMetadata);
+
+ $eventName = $input->getArgument('event');
+
+ $allListeners = $eventName === null
+ ? $classMetadata->entityListeners
+ : [$eventName => $classMetadata->entityListeners[$eventName] ?? []];
+
+ ksort($allListeners);
+
+ $io->title(sprintf('Entity listeners for %s', $entityName));
+
+ if (! $allListeners) {
+ $io->text('No listeners are configured for this entity.');
+
+ return self::SUCCESS;
+ }
+
+ foreach ($allListeners as $event => $listeners) {
+ $io->section(sprintf('"%s" event', $event));
+
+ if (! $listeners) {
+ $io->text('No listeners are configured for this event.');
+ continue;
+ }
+
+ $rows = [];
+ foreach ($listeners as $order => $listener) {
+ $rows[] = [sprintf('#%d', ++$order), sprintf('%s::%s()', $listener['class'], $listener['method'])];
+ }
+
+ $io->table(['Order', 'Listener'], $rows);
+ }
+
+ return self::SUCCESS;
+ }
+
+ public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
+ {
+ if ($input->mustSuggestArgumentValuesFor('entity')) {
+ $suggestions->suggestValues($this->listAllEntities());
+
+ return;
+ }
+
+ if ($input->mustSuggestArgumentValuesFor('event')) {
+ $entityName = ltrim($input->getArgument('entity'), '\\');
+
+ if (! class_exists($entityName)) {
+ return;
+ }
+
+ $entityManager = $this->getDoctrine()->getManagerForClass($entityName);
+
+ if ($entityManager === null) {
+ return;
+ }
+
+ $classMetadata = $entityManager->getClassMetadata($entityName);
+ assert($classMetadata instanceof ClassMetadata);
+
+ $suggestions->suggestValues(array_keys($classMetadata->entityListeners));
+
+ return;
+ }
+ }
+
+ /** @return array */
+ private function listAllEntities(): array
+ {
+ $entities = [];
+ foreach (array_keys($this->getDoctrine()->getManagerNames()) as $managerName) {
+ $entities[] = $this->getEntityManager($managerName)->getConfiguration()->getMetadataDriverImpl()->getAllClassNames();
+ }
+
+ $entities = array_values(array_unique(array_merge(...$entities)));
+
+ sort($entities);
+
+ return $entities;
+ }
+}
diff --git a/src/Command/DebugEventManagerDoctrineCommand.php b/src/Command/DebugEventManagerDoctrineCommand.php
new file mode 100644
index 000000000..9ce61cac8
--- /dev/null
+++ b/src/Command/DebugEventManagerDoctrineCommand.php
@@ -0,0 +1,105 @@
+setName('doctrine:debug:event-manager')
+ ->setDescription('Lists event listeners for an entity manager')
+ ->addArgument('event', InputArgument::OPTIONAL, 'The event name to filter by (e.g. postPersist)')
+ ->addOption('em', null, InputOption::VALUE_REQUIRED, 'The entity manager to use for this command')
+ ->setHelp(<<<'EOT'
+The %command.name% command lists all event listeners for the default entity manager:
+
+ php %command.full_name%
+
+You can also specify an entity manager:
+
+ php %command.full_name% --em=default
+
+To show only listeners for a specific event, pass the event name as an argument:
+
+ php %command.full_name% postPersist
+EOT);
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $io = new SymfonyStyle($input, $output);
+
+ $entityManagerName = $input->getOption('em') ?: $this->getDoctrine()->getDefaultManagerName();
+ $eventManager = $this->getEntityManager($entityManagerName)->getEventManager();
+
+ $eventName = $input->getArgument('event');
+
+ $allListeners = $eventName === null
+ ? $eventManager->getAllListeners()
+ : [$eventName => $eventManager->hasListeners($eventName) ? $eventManager->getListeners($eventName) : []];
+
+ ksort($allListeners);
+
+ $io->title(sprintf('Event listeners for %s entity manager', $entityManagerName));
+
+ if (! $allListeners) {
+ $io->text('No listeners are configured for this entity manager.');
+
+ return self::SUCCESS;
+ }
+
+ foreach ($allListeners as $event => $listeners) {
+ $io->section(sprintf('"%s" event', $event));
+
+ if (! $listeners) {
+ $io->text('No listeners are configured for this event.');
+ continue;
+ }
+
+ $rows = [];
+ foreach (array_values($listeners) as $order => $listener) {
+ $method = method_exists($listener, '__invoke') ? '__invoke' : $event;
+ $rows[] = [sprintf('#%d', ++$order), sprintf('%s::%s()', $listener::class, $method)];
+ }
+
+ $io->table(['Order', 'Listener'], $rows);
+ }
+
+ return self::SUCCESS;
+ }
+
+ public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
+ {
+ if ($input->mustSuggestArgumentValuesFor('event')) {
+ $entityManagerName = $input->getOption('em') ?: $this->getDoctrine()->getDefaultManagerName();
+ $eventManager = $this->getEntityManager($entityManagerName)->getEventManager();
+
+ $suggestions->suggestValues(array_keys($eventManager->getAllListeners()));
+
+ return;
+ }
+
+ if ($input->mustSuggestOptionValuesFor('em')) {
+ $suggestions->suggestValues(array_keys($this->getDoctrine()->getManagerNames()));
+
+ return;
+ }
+ }
+}
diff --git a/tests/Command/DebugEntityListenersDoctrineCommandTest.php b/tests/Command/DebugEntityListenersDoctrineCommandTest.php
new file mode 100644
index 000000000..6c135d43e
--- /dev/null
+++ b/tests/Command/DebugEntityListenersDoctrineCommandTest.php
@@ -0,0 +1,157 @@
+getMockManagerRegistry());
+
+ $application = new SymfonyApp();
+ $application->addCommand($command);
+
+ $commandTester = new CommandTester($command);
+ $commandTester->execute(
+ ['command' => $command->getName(), 'entity' => self::class],
+ );
+
+ self::assertSame(<<<'TXT'
+
+Entity listeners for Command\DebugEntityListenersDoctrineCommandTest
+====================================================================
+
+"postPersists" event
+--------------------
+
+ ------- -----------------------------------------------------------------------------------
+ Order Listener
+ ------- -----------------------------------------------------------------------------------
+ #1 Doctrine\Bundle\DoctrineBundle\Tests\Command\Fixtures\BazListener::postPersists()
+ ------- -----------------------------------------------------------------------------------
+
+"preUpdate" event
+-----------------
+
+ ------- --------------------------------------------------------------------------------
+ Order Listener
+ ------- --------------------------------------------------------------------------------
+ #1 Doctrine\Bundle\DoctrineBundle\Tests\Command\Fixtures\FooListener::preUpdate()
+ #2 Doctrine\Bundle\DoctrineBundle\Tests\Command\Fixtures\BarListener::__invoke()
+ ------- --------------------------------------------------------------------------------
+
+
+TXT
+, $commandTester->getDisplay(true));
+ }
+
+ public function testExecuteWithEvent(): void
+ {
+ $command = new DebugEntityListenersDoctrineCommand($this->getMockManagerRegistry());
+
+ $application = new SymfonyApp();
+ $application->addCommand($command);
+
+ $commandTester = new CommandTester($command);
+ $commandTester->execute(
+ ['command' => $command->getName(), 'entity' => self::class, 'event' => 'postPersists'],
+ );
+
+ self::assertSame(<<<'TXT'
+
+Entity listeners for Command\DebugEntityListenersDoctrineCommandTest
+====================================================================
+
+"postPersists" event
+--------------------
+
+ ------- -----------------------------------------------------------------------------------
+ Order Listener
+ ------- -----------------------------------------------------------------------------------
+ #1 Doctrine\Bundle\DoctrineBundle\Tests\Command\Fixtures\BazListener::postPersists()
+ ------- -----------------------------------------------------------------------------------
+
+
+TXT
+, $commandTester->getDisplay(true));
+ }
+
+ public function testExecuteWithMissingEvent(): void
+ {
+ $command = new DebugEntityListenersDoctrineCommand($this->getMockManagerRegistry());
+
+ $application = new SymfonyApp();
+ $application->addCommand($command);
+
+ $commandTester = new CommandTester($command);
+ $commandTester->execute(
+ ['command' => $command->getName(), 'entity' => self::class, 'event' => 'preRemove'],
+ );
+
+ self::assertSame(<<<'TXT'
+
+Entity listeners for Command\DebugEntityListenersDoctrineCommandTest
+====================================================================
+
+"preRemove" event
+-----------------
+
+ No listeners are configured for this event.
+
+TXT
+, $commandTester->getDisplay(true));
+ }
+
+ /** @return MockObject&ManagerRegistry */
+ private function getMockManagerRegistry(): MockObject
+ {
+ $mappingDriverMock = $this->createMock(MappingDriver::class);
+ $mappingDriverMock->method('getAllClassNames')->willReturn([self::class]);
+
+ $config = new Configuration();
+ $config->setMetadataDriverImpl($mappingDriverMock);
+
+ $classMetadata = new ClassMetadata(self::class);
+ $classMetadata->addEntityListener('preUpdate', FooListener::class, 'preUpdate');
+ $classMetadata->addEntityListener('preUpdate', BarListener::class, '__invoke');
+ $classMetadata->addEntityListener('postPersists', BazListener::class, 'postPersists');
+
+ $emMock = $this->createMock(EntityManagerInterface::class);
+ $emMock->method('getConfiguration')->willReturn($config);
+ $emMock->method('getClassMetadata')->willReturn($classMetadata);
+
+ $doctrineMock = $this->createMock(ManagerRegistry::class);
+ $doctrineMock->method('getManagerNames')->willReturn(['default']);
+ $doctrineMock->method('getManager')->willReturn($emMock);
+ $doctrineMock->method('getManagerForClass')->willReturn($emMock);
+
+ return $doctrineMock;
+ }
+}
diff --git a/tests/Command/DebugEventManagerDoctrineCommandTest.php b/tests/Command/DebugEventManagerDoctrineCommandTest.php
new file mode 100644
index 000000000..294a36224
--- /dev/null
+++ b/tests/Command/DebugEventManagerDoctrineCommandTest.php
@@ -0,0 +1,147 @@
+getMockManagerRegistry());
+
+ $application = new SymfonyApp();
+ $application->addCommand($command);
+
+ $commandTester = new CommandTester($command);
+ $commandTester->execute(
+ ['command' => $command->getName()],
+ );
+
+ self::assertSame(<<<'TXT'
+
+Event listeners for default entity manager
+==========================================
+
+"postPersists" event
+--------------------
+
+ ------- -----------------------------------------------------------------------------------
+ Order Listener
+ ------- -----------------------------------------------------------------------------------
+ #1 Doctrine\Bundle\DoctrineBundle\Tests\Command\Fixtures\BazListener::postPersists()
+ ------- -----------------------------------------------------------------------------------
+
+"preUpdate" event
+-----------------
+
+ ------- --------------------------------------------------------------------------------
+ Order Listener
+ ------- --------------------------------------------------------------------------------
+ #1 Doctrine\Bundle\DoctrineBundle\Tests\Command\Fixtures\FooListener::preUpdate()
+ #2 Doctrine\Bundle\DoctrineBundle\Tests\Command\Fixtures\BarListener::__invoke()
+ ------- --------------------------------------------------------------------------------
+
+
+TXT
+ , $commandTester->getDisplay(true));
+ }
+
+ public function testExecuteWithEvent(): void
+ {
+ $command = new DebugEventManagerDoctrineCommand($this->getMockManagerRegistry());
+
+ $application = new SymfonyApp();
+ $application->addCommand($command);
+
+ $commandTester = new CommandTester($command);
+ $commandTester->execute(
+ ['command' => $command->getName(), 'event' => 'postPersists'],
+ );
+
+ self::assertSame(<<<'TXT'
+
+Event listeners for default entity manager
+==========================================
+
+"postPersists" event
+--------------------
+
+ ------- -----------------------------------------------------------------------------------
+ Order Listener
+ ------- -----------------------------------------------------------------------------------
+ #1 Doctrine\Bundle\DoctrineBundle\Tests\Command\Fixtures\BazListener::postPersists()
+ ------- -----------------------------------------------------------------------------------
+
+
+TXT
+ , $commandTester->getDisplay(true));
+ }
+
+ public function testExecuteWithMissingEvent(): void
+ {
+ $command = new DebugEventManagerDoctrineCommand($this->getMockManagerRegistry());
+
+ $application = new SymfonyApp();
+ $application->addCommand($command);
+
+ $commandTester = new CommandTester($command);
+ $commandTester->execute(
+ ['command' => $command->getName(), 'event' => 'preRemove'],
+ );
+
+ self::assertSame(<<<'TXT'
+
+Event listeners for default entity manager
+==========================================
+
+"preRemove" event
+-----------------
+
+ No listeners are configured for this event.
+
+TXT
+ , $commandTester->getDisplay(true));
+ }
+
+ /** @return MockObject&ManagerRegistry */
+ private function getMockManagerRegistry(): MockObject
+ {
+ $eventManager = new EventManager();
+ $eventManager->addEventListener('preUpdate', new FooListener());
+ $eventManager->addEventListener('preUpdate', new BarListener());
+ $eventManager->addEventListener('postPersists', new BazListener());
+
+ $emMock = $this->createMock(EntityManagerInterface::class);
+ $emMock->method('getEventManager')->willReturn($eventManager);
+
+ $doctrineMock = $this->createMock(ManagerRegistry::class);
+ $doctrineMock->method('getDefaultManagerName')->willReturn('default');
+ $doctrineMock->method('getManager')->willReturn($emMock);
+
+ return $doctrineMock;
+ }
+}
diff --git a/tests/Command/Fixtures/BarListener.php b/tests/Command/Fixtures/BarListener.php
new file mode 100644
index 000000000..fe399dc4e
--- /dev/null
+++ b/tests/Command/Fixtures/BarListener.php
@@ -0,0 +1,12 @@
+