pixelfederation/doctrine-resettable-em-bundle
Symfony bundle that provides a resettable Doctrine EntityManager, letting applications recover from closed or failed managers after database exceptions and continue handling requests and background work reliably.
What this package is (and isn't). pixelfederation/doctrine-resettable-em-bundle is a Symfony bundle, not a Laravel package. It decorates Doctrine's ManagerRegistry so every entity manager you get back is a ResettableEntityManager — a lazy proxy that can be torn down and transparently re-opened. Its job is to solve the long-running-process problem: the Doctrine identity map and metadata caches grow unbounded, and stale entities leak between jobs/messages/requests.
If you're coming from Laravel, read the next paragraph before running composer require.
Check the dependency surface first. Open composer.json in the repo. The bundle pulls symfony/http-kernel, symfony/dependency-injection, symfony/config, and doctrine/doctrine-bundle. Installing it into a vanilla Laravel app means dragging the Symfony DI container in for one decorator — almost never worth it. Copy the pattern, not the package: the ResettableEntityManager class itself is close to framework-agnostic (it wraps EntityManagerInterface and implements ResetInterface), so vendoring/adapting it is the pragmatic path for laravel-doctrine/orm users.
If you are on Symfony (or maintaining a Symfony service inside a Laravel monorepo), the setup is three steps:
config/bundles.php — it must come after DoctrineBundle.DependencyInjection/Configuration.php for the exact keys in your version) so entity managers reset between commands in a single process.bin/console debug:container doctrine --show-arguments should show the resettable registry.First use case to validate against: a messenger:consume worker processing thousands of messages. Before the bundle, memory climbs monotonically and a persist() on a stale entity silently no-ops. After, memory plateaus and each message starts from a clean identity map.
Laravel equivalent to build: bind a decorator around your ManagerRegistry, then call resetManager() from a job middleware (JobProcessed) and an Octane RequestTerminated listener. That's the same shape as the Symfony ResetServicesListener integration.
Version note: 11.1.0 adds Symfony 8.1 support (PR #500). This is a compatibility bump only — no API changes. If you're pinned to Symfony 8.0 or an older LTS, 11.0.0 works identically; if you're tracking Symfony 8.1, this is the release that unblocks you.
Messenger / queue workers (primary pattern). Symfony's ResetServicesListener already resets anything implementing ResetInterface between messages, so the bundle mostly works for free. Verify it by logging spl_object_id($em) in a handler — it should change per message. If it doesn't, your ResetInterface tag is missing.
Explicit reset in a custom worker loop. For RoadRunner, Swoole, FrankenPHP, or a hand-rolled while (true) consumer, call the reset yourself at the loop boundary:
try {
$handler->handle($message);
} finally {
$registry->resetManager(); // or resetManager('secondary') for one EM
gc_collect_cycles();
}
The finally matters — see Gotchas.
Targeting a single entity manager. In multi-EM setups (read replica, reporting EM, legacy DB), prefer resetManager($name) over resetting everything so you don't invalidate a healthy connection on every iteration.
Batch/bulk operation checkpoints. Even outside workers, resetting after a chunked import loop (every N thousand rows) keeps memory flat. Pair it with clear() for the in-loop case and reserve full reset() for chunk boundaries.
Laravel integration tips. With laravel-doctrine/orm:
handle()'s finally.RequestTerminated for Octane rather than RequestHandled — you want the reset after the response is flushed, not before.ManagerRegistry injection over EntityManagerInterface everywhere downstream, otherwise the container hands you a stale instance that survives the reset.Extension point. Don't try to decorate doctrine.orm.entity_manager directly — the registry is the supported seam. If you need custom pre/post-reset behavior (metrics, connection health checks), wrap the ResettableEntityManager in your own decorator registered on the same service id.
Resetting closes the entity manager. Any reference you cached to an EntityManagerInterface — in a singleton service, a repository property, a static — is now pointing at a closed EM and will throw EntityManagerClosed. This is the single most common production break. Inject ManagerRegistry and call getManager() at the point of use.
Open transactions are not magically handled. reset() on a manager with an active transaction will discard uncommitted work, sometimes loudly, sometimes silently depending on driver. Always structure worker code as try { … } catch { rollback } finally { reset }. Never reset from a finally that can run mid-transaction.
Entities become detached. After a reset, a $user you fetched before the boundary is detached — persist($user) creates a duplicate. Capture the ID across the boundary, not the object. This bites hardest in queue payloads that were serialized pre-reset.
Reset is not a full memory cure. Doctrine's second-level cache (if enabled via doctrine/orm cache config) survives reset(), as do your own in-process caches. Pair with gc_collect_cycles() in tight loops, and check memory_get_usage(true) per iteration to prove the fix.
Config ordering in bundles.php. DoctrineBundle must be registered before this one; Symfony's bundle ordering is otherwise silently forgiving about the failure mode, which makes it annoying to diagnose.
Debugging checklist when resets seem to not happen: confirm the registry service id is actually decorated in debug:container; log spl_object_id() of the manager before and after; check whether your code path uses a different registry (Laravel apps with two Doctrine bindings hit this); verify the console listener flag is true if the leak only shows up in a long-lived CLI process.
Symfony 8.1 support (11.1.0) is a floor, not a ceiling. The constraint addition means 11.1.0 is required if you're on Symfony 8.1. Downgrading Symfony to accommodate a pinned bundle version is the wrong direction — check whether an older 11.0.x patch is enough.
Not a Laravel package — resist the instinct. Every Laravel-Doctrine long-running-process bug is fixable with ~40 lines of your own decorator plus a job middleware. Pulling in Symfony's DI container to get that is a maintenance tax you'll pay at every major Symfony release, this one included.
How can I help you explore Laravel packages today?