Add support for object pools / context #7582

Open
opened 2026-01-22 15:53:48 +01:00 by admin · 7 comments
Owner

Originally created by @lyrixx on GitHub (Dec 23, 2025).

RFC: Independent Persistence Contexts (Persistence Pools)

Abstract

This RFC proposes a mechanism to create isolated "pools" or "sub-contexts" within the EntityManager. The goal is to allow developers to persist and flush specific entities without interfering with the global UnitOfWork state.

Problem Statement

In many scenarios, developers need to perform "side-effect" writes that are independent of the main business transaction.

Current issues:

  • Global Flush: Calling $em->flush() persists every scheduled change in the UnitOfWork. If the main transaction has pending invalid entities, the side-effect write fails.
  • Partial Flushes: Passing an entity to $em->flush($entity) is deprecated or discouraged because it doesn't guarantee consistency and ignores many UnitOfWork states.
  • Side-effect Rollbacks: If the main transaction fails later, the side-effect (like an audit log) might be rolled back if they share the same underlying transaction/connection.

Use Cases

  • Audit Logging: Persisting call logs or user actions mid-service.
  • Security: Updating a last_used_at timestamp on an API token without flushing the entire user profile.
  • Feature Tracking: Incrementing usage counters during a complex read process.

Proposed Solution

Introduce a Context or Pool object that acts as a lightweight, isolated UnitOfWork.

// Proposed API
$pool = $this->entityManager->createPool();

$log = new AuditLog('AI_CALL', $payload);
$pool->persist($log);

// Only flushes entities managed by this pool
// No interference with the global UnitOfWork
$pool->flush();

Benefits

  1. Isolation: Zero interference with previously scheduled changes in the main EntityManager.
  2. ORM Features: Keeps all advantages (Type conversion, Lifecycle Events, UUID generation).
  3. Developer Experience: Avoids manual SQL queries for "internal" database updates.

Notes

It might be possible with 2 EM, but I'll lead to the same issue. The "internal" EM could have many pending changes

It might be possible with nested transaction, but I'm not sure it has been built for this use case


WDYT ?

Originally created by @lyrixx on GitHub (Dec 23, 2025). ## RFC: Independent Persistence Contexts (Persistence Pools) ### Abstract This RFC proposes a mechanism to create isolated "pools" or "sub-contexts" within the `EntityManager`. The goal is to allow developers to `persist` and `flush` specific entities without interfering with the global `UnitOfWork` state. ### Problem Statement In many scenarios, developers need to perform "side-effect" writes that are independent of the main business transaction. **Current issues:** * **Global Flush:** Calling `$em->flush()` persists every scheduled change in the `UnitOfWork`. If the main transaction has pending invalid entities, the side-effect write fails. * **Partial Flushes:** Passing an entity to `$em->flush($entity)` is deprecated or discouraged because it doesn't guarantee consistency and ignores many `UnitOfWork` states. * **Side-effect Rollbacks:** If the main transaction fails later, the side-effect (like an audit log) might be rolled back if they share the same underlying transaction/connection. ### Use Cases * **Audit Logging:** Persisting call logs or user actions mid-service. * **Security:** Updating a `last_used_at` timestamp on an API token without flushing the entire user profile. * **Feature Tracking:** Incrementing usage counters during a complex read process. ### Proposed Solution Introduce a `Context` or `Pool` object that acts as a lightweight, isolated `UnitOfWork`. ```php // Proposed API $pool = $this->entityManager->createPool(); $log = new AuditLog('AI_CALL', $payload); $pool->persist($log); // Only flushes entities managed by this pool // No interference with the global UnitOfWork $pool->flush(); ``` ### Benefits 1. **Isolation:** Zero interference with previously scheduled changes in the main `EntityManager`. 2. **ORM Features:** Keeps all advantages (Type conversion, Lifecycle Events, UUID generation). 3. **Developer Experience:** Avoids manual SQL queries for "internal" database updates. ### Notes It might be possible with 2 EM, but I'll lead to the same issue. The "internal" EM could have many pending changes It might be possible with nested transaction, but I'm not sure it has been built for this use case --- WDYT ?
Author
Owner

@VincentLanglet commented on GitHub (Jan 1, 2026):

Isolation: Zero interference with previously scheduled changes in the main EntityManager.

Let's say I have to also add the current User doing the modification to the AuditLog, how you deal with this situation ?

  1. The user can come from the EntityManager
$user = $this->entityManager->find(User::class, 42);
$pool = $this->entityManager->createPool();
$log = new AuditLog('AI_CALL', $payload);
$log->setUser($user);
$pool->persist($log);
$pool->flush();
  1. The user need to be manage by the pool
$pool = $this->entityManager->createPool();
$user = $this->pool->find(User::class, 42);
$log = new AuditLog('AI_CALL', $payload);
$log->setUser($user);
$pool->persist($log);
$pool->flush();

The second solution might be the simplest to have a perfect isolation but I feel like in real life it will just force the developper to "duplicate" the ownership of the managed entity. It also introduce some weird question about what if a pool is updating a value about the user while the "original" entity manager still have the "old" managed entity.

A similar/related topic is the "Cascade" configuration, does the "Cascade persist" is applied to pool/context or only to the original entityManager ? I.E. if I write

$user->setFirstName('Foo');
$pool = $this->entityManager->createPool();
$log = new AuditLog('AI_CALL', $payload);
$log->setUser($user);
$pool->persist($log);
$pool->flush();

Does the pool will just persist a new AuditLog related to the User, but still does not flush the user firstName (since I didn't persist the $user changes) or does persisting the log will cascade-persist the user ?

@VincentLanglet commented on GitHub (Jan 1, 2026): > Isolation: Zero interference with previously scheduled changes in the main EntityManager. Let's say I have to also add the current User doing the modification to the AuditLog, how you deal with this situation ? 1) The user can come from the EntityManager ``` $user = $this->entityManager->find(User::class, 42); $pool = $this->entityManager->createPool(); $log = new AuditLog('AI_CALL', $payload); $log->setUser($user); $pool->persist($log); $pool->flush(); ```` 2) The user need to be manage by the pool ``` $pool = $this->entityManager->createPool(); $user = $this->pool->find(User::class, 42); $log = new AuditLog('AI_CALL', $payload); $log->setUser($user); $pool->persist($log); $pool->flush(); ```` The second solution might be the simplest to have a perfect isolation but I feel like in real life it will just force the developper to "duplicate" the ownership of the managed entity. It also introduce some weird question about what if a pool is updating a value about the user while the "original" entity manager still have the "old" managed entity. A similar/related topic is the "Cascade" configuration, does the "Cascade persist" is applied to pool/context or only to the original entityManager ? I.E. if I write ``` $user->setFirstName('Foo'); $pool = $this->entityManager->createPool(); $log = new AuditLog('AI_CALL', $payload); $log->setUser($user); $pool->persist($log); $pool->flush(); ``` Does the pool will just persist a new AuditLog related to the User, but still does not flush the user firstName (since I didn't persist the `$user` changes) or does persisting the log will cascade-persist the user ?
Author
Owner

@beberlei commented on GitHub (Jan 4, 2026):

I am thinking about this problem for a long time now, and had different ideas as well.

From a naming perspective, i would consider using Transaction and variants, looking at inspiration from the TransactionStatus API from JPA.

Initially an idea was this API, but there are other more recent ideas that are not ready to share: https://gist.github.com/beberlei/4b39c4ed1f172890e03fc6e2fa83146e

One thought I had is that maybe switching the change tracking policy from implicit to explicit for certain use-cases could work.

But recently from the Doctrine hackathon we also started considering to replace the UnitOfWork in clear to get rid of the "EM is closed" state, which could also open the door to multiple UnitOfWorks.

However there are a lot of potentially ugly edge cases around this, as Vincent also points out.

@beberlei commented on GitHub (Jan 4, 2026): I am thinking about this problem for a long time now, and had different ideas as well. From a naming perspective, i would consider using `Transaction` and variants, looking at inspiration from the TransactionStatus API from JPA. Initially an idea was this API, but there are other more recent ideas that are not ready to share: https://gist.github.com/beberlei/4b39c4ed1f172890e03fc6e2fa83146e One thought I had is that maybe switching the change tracking policy from implicit to explicit for certain use-cases could work. But recently from the Doctrine hackathon we also started considering to replace the UnitOfWork in clear to get rid of the "EM is closed" state, which could also open the door to multiple UnitOfWorks. However there are a lot of potentially ugly edge cases around this, as Vincent also points out.
Author
Owner

@beberlei commented on GitHub (Jan 4, 2026):

Related https://github.com/doctrine/orm/issues/5933

@beberlei commented on GitHub (Jan 4, 2026): Related https://github.com/doctrine/orm/issues/5933
Author
Owner

@stof commented on GitHub (Jan 6, 2026):

having multiple active unit of work is totally different from the case of replacing the unit of work to re-open a closed entity manager.

Multiple active unit of work have issues regarding the ownership of change tracking (either they both manage the same entity instance, which can create weird cases, or they require using separate entity instances but this leads to the possibility of mutating those instances in non-synchronized ways).

@stof commented on GitHub (Jan 6, 2026): having multiple *active* unit of work is totally different from the case of replacing the unit of work to re-open a closed entity manager. Multiple *active* unit of work have issues regarding the ownership of change tracking (either they both manage the same entity instance, which can create weird cases, or they require using separate entity instances but this leads to the possibility of mutating those instances in non-synchronized ways).
Author
Owner

@lyrixx commented on GitHub (Jan 7, 2026):

@VincentLanglet I would forbid your first example. I'll lead to headache, and it defeats the purpose. Second option is the say to go!

@lyrixx commented on GitHub (Jan 7, 2026): @VincentLanglet I would forbid your first example. I'll lead to headache, and it defeats the purpose. Second option is the say to go!
Author
Owner

@stof commented on GitHub (Jan 7, 2026):

@lyrixx but in that second option, you can have 2 different manager User instance for the id 42, one on the main entity manager and one in the pool. If the pool saves a change in the DB for that id, the instance in the main entity manager won't reflect it. And this becomes even worse if the pool deletes that row, as the main entity manager now has a managed instance that corresponds to no row in the DB (causing weird things when saving changes).

And things become even worse in case the main entity manager has an uninitialized lazy instance for the id 42 and the pool deletes that row.

@stof commented on GitHub (Jan 7, 2026): @lyrixx but in that second option, you can have 2 different manager `User` instance for the id 42, one on the main entity manager and one in the pool. If the pool saves a change in the DB for that id, the instance in the main entity manager won't reflect it. And this becomes even worse if the pool *deletes* that row, as the main entity manager now has a managed instance that corresponds to no row in the DB (causing weird things when saving changes). And things become even worse in case the main entity manager has an uninitialized lazy instance for the id 42 and the pool deletes that row.
Author
Owner

@lyrixx commented on GitHub (Jan 7, 2026):

@lyrixx but in that second option, you can have 2 different manager User instance for the id 42, one on the main entity manager and one in the pool. If the pool saves a change in the DB for that id, the instance in the main entity manager won't reflect it. And this becomes even worse if the pool deletes that row, as the main entity manager now has a managed instance that corresponds to no row in the DB (causing weird things when saving changes).

yes! No magic here. Anyway, usually, my use cases are really limited, and have a really narrow scoped.

For example, in a log line, there is never a strong relation to another entity. Like in an invoice. You don't want to loose something if a user unregister.

Anyway, the Benjamin RFC looks nice

@lyrixx commented on GitHub (Jan 7, 2026): > [@lyrixx](https://github.com/lyrixx) but in that second option, you can have 2 different manager `User` instance for the id 42, one on the main entity manager and one in the pool. If the pool saves a change in the DB for that id, the instance in the main entity manager won't reflect it. And this becomes even worse if the pool _deletes_ that row, as the main entity manager now has a managed instance that corresponds to no row in the DB (causing weird things when saving changes). yes! No magic here. Anyway, usually, my use cases are really limited, and have a really narrow scoped. For example, in a log line, there is never a strong relation to another entity. Like in an invoice. You don't want to loose something if a user unregister. Anyway, the Benjamin RFC looks nice
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: doctrine/archived-orm#7582