Add grabEntityManager(), resetDoctrineManager() and grabContainer() - #248
Merged
TavoNiievez merged 3 commits intoSep 4, 2026
Conversation
_getEntityManager() is underscore-prefixed, so ModuleContainer excludes it from
the Actor and it cannot be called as $I->_getEntityManager(). Reaching the
EntityManager from a test therefore meant a getModule('Symfony') reach-through,
a grabService() call with a hardcoded service id, or a custom Helper module.
grabEntityManager() exposes it on the Actor. It delegates to _getEntityManager(),
so it keeps resolving the manager from the current container on every call and
honours the em_service option.
Extract the service resolution into resolveEntityManager(), which also improves
the failure message: the previous one reported "is not an instance of
EntityManagerInterface" both when the service was missing entirely and when it
was the wrong type, without hinting that doctrine-bundle might not be installed
or that em_service might be misconfigured.
This was referenced Sep 4, 2026
Doctrine closes the EntityManager when an exception escapes a flush(), and every write after that throws EntityManagerClosed. Users worked around this by shipping an EntityManagerReset helper module that resets the manager in _before; it is the most copy-pasted workaround in the dependent ecosystem. Add resetDoctrineManager() as the supported replacement: an open manager is cleared, a closed one is rebuilt through Doctrine's registry, which swaps the lazy service in place so the application and the Doctrine module see the reopened manager too. When the registry cannot do it, because the manager service is not lazy on Symfony 5.4 or the app has no DoctrineBundle, the client kernel is rebooted instead. The DBAL connection stays a permanent service either way, so the open test transaction survives. _getEntityManager() applies the same registry recovery before handing the manager out, so a single broken write no longer cascades through the rest of the test. Nothing is cached; the container remains the source of truth. The fixture app wires Doctrine by hand and had no registry to reset through, so add a minimal TestManagerRegistry, rebuild the manager on the existing connection (the fixture database is in-memory, reconnecting would drop the schema), and stop sharing the entity manager service so the container hands out the rebuilt one.
_getContainer() is underscore-prefixed, so ModuleContainer excludes it from the
Actor. Tests that need the container reached it through a getModule('Symfony')
call in a custom Helper, or wrote $I->grabService('kernel')->getContainer().
That second form is subtly wrong: the kernel exposes the application container,
which cannot see private services, while _getContainer() returns Symfony's
test.service_container, which can. grabContainer() puts the correct one on the
Actor, tagged as part of the services part alongside grabService().
TavoNiievez
force-pushed
the
feature/entity-manager-and-container-accessors
branch
from
September 4, 2026 08:12
e4b5e5e to
b854515
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds three accessors that dependent projects have been hand-writing for years. The counts below come from a survey of the test suites of 349 public repositories that depend on this module.
1.
grabEntityManager()— 44 repositories wrote it themselves_getEntityManager()is underscore-prefixed, soModuleContainerexcludes it from the Actor. Reaching the EntityManager meant agetModule('Symfony')reach-through, agrabService()call with a hardcoded id, or a custom Helper module. The corpus contains six different spellings of the same line, including four unrelated tutorial repos reading the publicgetModule('Doctrine2')->emproperty directly.It delegates to
_getEntityManager(), so it keeps resolving from the current container on every call and honoursem_service. The service resolution moved intoresolveEntityManager(), which also fixes a misleading message: the old one said "is not an instance of EntityManagerInterface" both when the service was missing entirely and when it was the wrong type.2.
resetDoctrineManager()— 41 repositories, the largest copy-paste cluster in the corpusTen repositories ship a byte-identical
EntityManagerResetHelper module, with the same docblock — "Fix "The EntityManager is closed" after test failure" — and 31 more wire that class into a suite YAML.This PR does not fix a live cross-test bug: 6cd2150 (#236) already did. Before that commit
_getEntityManager()pinned the manager intopermanentServices, so one failed flush left a closed EM that the connector re-injected into every subsequent test's container for the rest of the suite. Every repository in the corpus is pinned to^3.1/^3.5, i.e. before that fix. What remains onmainis a narrower window: a write after a failed flush within a single test, with no intervening request. So this gives those 41 projects a supported API instead of a copied Helper, and closes that window.Semantics mirror doctrine-bundle's own private
Registry::resetOrClearManager(): an open manager is cleared, a closed one is rebuilt. Recovery goes through Doctrine's registry, which swaps the lazy service in place so the application andmodule-doctrinesee the reopened manager too. On Symfony 5.4 the entity manager service is not lazy (noLazyServiceDumper, noproxy-manager-bridge), andManagerRegistry::resetService()throws there, so the client kernel is rebooted instead.doctrine.dbal.default_connectionstays a permanent service in both paths, so the open test transaction survives._getEntityManager()applies the same registry recovery before handing the manager out. It is bounded: it only runs whenisOpen()is already false, i.e. when the manager was going to throw anyway. Nothing is cached — the container remains the source of truth.A module config key was considered and rejected: it would have to act in
_before, where the container is milliseconds old and the manager is never closed yet.3.
grabContainer()— 41 repositories wrote it themselvesSame root cause:
_getContainer()is underscore-prefixed. Most projects settled on$I->grabService('kernel')->getContainer(), which is subtly wrong — that is the application container, which cannot see private services, while_getContainer()returnstest.service_container, which can. Tagged@part servicesalongsidegrabService().Notes
composer.jsonchange.Doctrine\Persistence\ManagerRegistryis already present viadoctrine/orm(require-dev) and::classnever autoloads; every Doctrine touch is guarded byinterface_exists()+instanceof+try/catch.TestManagerRegistry, signature-compatible withdoctrine/persistence3.x and 4.x. The manager is rebuilt on the existing connection because the fixture database is in-memory, and the entity manager service is no longer shared so the container hands out the rebuilt one.Verification
composer phpstan(level max) andvendor/bin/phpunit testsgreen: 181 tests.doctrine/persistence3.4.5 and 4.2.0, by rewriting the constraint the way CI does.symfony-module-testsfunctional suite passes on 5.4 and 8.1 against this branch. Functional tests are proposed separately, one PR per version branch:dev-main(b336cca) incomposer.lock, so no dependency bump is needed on them now; they go green once the lock points at a revision containing this PR.