From d72a2d3243936735ee82e97eeb58ce7dea10c8e5 Mon Sep 17 00:00:00 2001 From: Aaron Gustavo Nieves Date: Sun, 21 Jun 2026 23:12:15 -0500 Subject: [PATCH] Fix security context missing in Doctrine entity listeners after kernel reboot _getEntityManager() persisted the EntityManager as a permanent service, so it was carried across kernel reboots and re-injected into the freshly booted container. An EntityManager cannot survive a reboot: its ListenersInvoker is bound (readonly) to the container it was built in, so lazy entity listeners resolve dependencies like security.token_storage from that now-stale container. The logged-in user was therefore invisible inside the listener while visible in the controller. In pure Symfony (WebTestCase) there is no reboot, one container, so listener and controller share the same context. Fix: resolve the EntityManager fresh from the current container on every call instead of caching it as a permanent service. Only doctrine.dbal.default_connection stays persistent, which is what keeps the open test transaction alive across reboots; the freshly rebuilt EntityManager runs on top of it, with every dependency (security included) wired to the current request. This also keeps the module's Doctrine helpers and the application on a single identity map, matching pure Symfony. Fixes #34 --- src/Codeception/Module/Symfony.php | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/Codeception/Module/Symfony.php b/src/Codeception/Module/Symfony.php index a130e0ee..4d4447fe 100644 --- a/src/Codeception/Module/Symfony.php +++ b/src/Codeception/Module/Symfony.php @@ -272,24 +272,31 @@ protected function onReconfigure(array $settings = []): void /** * Retrieve the Doctrine EntityManager. - * EntityManager service is retrieved once and then reused. + * + * The EntityManager is resolved fresh from the current container on every + * call instead of being cached, so it always matches the one the kernel + * uses for the request in flight. After a reboot a persisted EntityManager + * would keep an immutable ListenersInvoker bound to the old container, + * splitting the identity map (and the security context lazy entity + * listeners see) from the application's. Only the DBAL connection is kept + * persistent: that preserves the open test transaction across reboots while + * the freshly rebuilt EntityManager runs on top of it. + * + * @see https://github.com/Codeception/module-symfony/issues/34 */ public function _getEntityManager(): EntityManagerInterface { /** @var non-empty-string $emService */ $emService = $this->config['em_service']; - if (!isset($this->permanentServices[$emService])) { - $this->persistPermanentService($emService); + if (!isset($this->permanentServices['doctrine.dbal.default_connection'])) { $container = $this->_getContainer(); - foreach (['doctrine', 'doctrine.orm.default_entity_manager', 'doctrine.dbal.default_connection'] as $service) { - if ($container->has($service)) { - $this->persistPermanentService($service); - } + if ($container->has('doctrine.dbal.default_connection')) { + $this->persistPermanentService('doctrine.dbal.default_connection'); } } - $em = $this->permanentServices[$emService]; + $em = $this->getService($emService); if (!$em instanceof EntityManagerInterface) { Assert::fail(sprintf('Service "%s" is not an instance of EntityManagerInterface.', $emService)); }