Skip to content

Unit of Work & Identity Map

This content is for v1.0. Switch to the latest version for up-to-date documentation.

EntityManager tracks entity state (NEW → MANAGED → REMOVED) via UnitOfWork. Changes are collected by a ChangeAggregator and flushed as a batch. Each context (EntityManager instance) has its own IdentityMap — no process-wide singletons.

  1. Entities are persist()-ed or loaded via find() / a repository, entering the IdentityMap as MANAGED.
  2. Property mutations are tracked by the ChangeAggregator against a change-tracking snapshot taken at load/persist time.
  3. flush() wraps all pending work in one transaction and computes the minimal set of INSERT / UPDATE / DELETE statements.
  4. On success, the transaction commits and in-memory state (generated IDs, bumped version columns) is reconciled with what was written.
  5. On any throw, the transaction rolls back. In-memory mutations that happened inside the flush loop must be revertable, or the entity is left poisoned relative to the (reverted) database state.
  • Clear entities from memory that are no longer needed within specific operations via $em->clear().
  • Different units of work can track their own entities independently.
  • EntityManager combines all unit-of-work changes into minimal database queries during flush.

Useful for processing large datasets, complex business operations spanning multiple contexts, and long-running processes with varying entity lifecycles.

There’s no built-in read/write routing — intentionally. Let infrastructure handle it (PgBouncer, ProxySQL, RDS Proxy), or use the per-context design directly:

// Write context → primary
$primary = new EntityManager($primaryConnection, ...);
$primary->persist($entity);
$primary->flush();
// Read context → replica
$replica = new EntityManager($replicaConnection, ...);
$users = $replica->findAll(User::class);

Each EntityManager owns its IdentityMap and UnitOfWork — calling flush() on a replica-backed instance is a design smell, not a supported write path.