Skip to content

Commit aeb6265

Browse files
committed
Fix security context missing in Doctrine entity listeners after kernel reboot
Persisting an EntityManager across kernel reboots binds its internal ListenersInvoker to the old container. Lazy entity listeners then resolve dependencies like security.token_storage from that stale container, making the logged-in user invisible inside the listener while it is visible in the controller. In pure Symfony (WebTestCase) there is no reboot, one container, so the listener and the controller share the same security context. Fix, in two parts that share one idea -- only the DBAL connection survives a reboot; rebuild everything else on top of it from the current container: - injectPersistentServices() no longer re-injects EntityManagerInterface or ManagerRegistry instances. The freshly booted container rebuilds the EntityManager on the persisted connection, preserving the open test transaction while wiring every dependency (security included) to the current request. - _getEntityManager() resolves the EntityManager fresh from the current container on every call instead of caching it as a permanent service. This keeps the module's Doctrine helpers and the application on a single identity map, matching pure Symfony, and avoids a stale cached manager after a reboot. Only doctrine.dbal.default_connection stays persistent, which is what keeps the transaction open. Also rename persistDoctrineConnections() -> keepDoctrineConnectionsOpen() to describe what it does: it removes the doctrine.connections parameter before shutdown so DoctrineBundle::shutdown() cannot close the persisted connection and drop the test transaction. Fixes #34
1 parent 81c0365 commit aeb6265

2 files changed

Lines changed: 22 additions & 10 deletions

File tree

src/Codeception/Lib/Connector/Symfony.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
namespace Codeception\Lib\Connector;
66

7+
use Doctrine\ORM\EntityManagerInterface;
8+
use Doctrine\Persistence\ManagerRegistry;
79
use InvalidArgumentException;
810
use Symfony\Component\DependencyInjection\ContainerInterface;
911
use Symfony\Component\HttpFoundation\Response;
@@ -55,7 +57,7 @@ public function rebootKernel(): void
5557
{
5658
$this->updatePersistentServices();
5759

58-
$this->persistDoctrineConnections();
60+
$this->keepDoctrineConnectionsOpen();
5961

6062
if ($this->kernel instanceof Kernel) {
6163
$this->ensureKernelShutdown();
@@ -96,7 +98,7 @@ private function getProfiler(): ?Profiler
9698
return $profiler instanceof Profiler ? $profiler : null;
9799
}
98100

99-
private function persistDoctrineConnections(): void
101+
private function keepDoctrineConnectionsOpen(): void
100102
{
101103
(function (): void {
102104
if (property_exists($this, 'parameters') && is_array($this->parameters)) {
@@ -117,6 +119,9 @@ private function updatePersistentServices(): void
117119
private function injectPersistentServices(): void
118120
{
119121
foreach ($this->persistentServices as $name => $service) {
122+
if ($service instanceof EntityManagerInterface || $service instanceof ManagerRegistry) {
123+
continue;
124+
}
120125
try {
121126
$this->container->set($name, $service);
122127
} catch (InvalidArgumentException $e) {

src/Codeception/Module/Symfony.php

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -272,24 +272,31 @@ protected function onReconfigure(array $settings = []): void
272272

273273
/**
274274
* Retrieve the Doctrine EntityManager.
275-
* EntityManager service is retrieved once and then reused.
275+
*
276+
* The EntityManager is resolved fresh from the current container on every
277+
* call instead of being cached, so it always matches the one the kernel
278+
* uses for the request in flight. After a reboot a persisted EntityManager
279+
* would keep an immutable ListenersInvoker bound to the old container,
280+
* splitting the identity map (and the security context lazy entity
281+
* listeners see) from the application's. Only the DBAL connection is kept
282+
* persistent: that preserves the open test transaction across reboots while
283+
* the freshly rebuilt EntityManager runs on top of it.
284+
*
285+
* @see https://github.com/Codeception/module-symfony/issues/34
276286
*/
277287
public function _getEntityManager(): EntityManagerInterface
278288
{
279289
/** @var non-empty-string $emService */
280290
$emService = $this->config['em_service'];
281291

282-
if (!isset($this->permanentServices[$emService])) {
283-
$this->persistPermanentService($emService);
292+
if (!isset($this->permanentServices['doctrine.dbal.default_connection'])) {
284293
$container = $this->_getContainer();
285-
foreach (['doctrine', 'doctrine.orm.default_entity_manager', 'doctrine.dbal.default_connection'] as $service) {
286-
if ($container->has($service)) {
287-
$this->persistPermanentService($service);
288-
}
294+
if ($container->has('doctrine.dbal.default_connection')) {
295+
$this->persistPermanentService('doctrine.dbal.default_connection');
289296
}
290297
}
291298

292-
$em = $this->permanentServices[$emService];
299+
$em = $this->getService($emService);
293300
if (!$em instanceof EntityManagerInterface) {
294301
Assert::fail(sprintf('Service "%s" is not an instance of EntityManagerInterface.', $emService));
295302
}

0 commit comments

Comments
 (0)