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 @@ +