Skip to content

Context-Bounded Entities

Multiple entity classes can point to the same database table, each exposing only the fields and relationships needed for that context — a narrow, context-bounded slice of the table rather than a full row model. Articulate merges compatible column definitions and validates for conflicts. This is the primary differentiator from standard ORMs.

A users table may be touched by authentication, administration, billing, public APIs, reporting, and background workers. Those contexts do not need the same fields, relations, invariants, or lifecycle behavior. A single shared User entity gradually becomes a coupling point between modules.

#[Entity]
class User
{
public int $id;
public string $login;
public string $password;
public string $name;
public array $phones; // Auth doesn't need this
public array $groups; // Auth doesn't need this
public Cart $cart; // Auth doesn't need this
}
// Auth: loads full user + all relations
$user = $userRepo->find($id);
return $auth->validate($user->login, $user->password);

Mark a context-bounded entity as read-only when it intentionally omits required columns — for example, a LoginUser that exposes only login and password from a users table that has many more non-nullable columns.

#[Entity(tableName: 'user', readOnly: true)]
class LoginUser
{
#[PrimaryKey]
public int $id;
#[Property]
public string $login;
#[Property]
public string $password;
}
// find() and QueryBuilder work normally:
$loginUser = $em->getRepository(LoginUser::class)->find($id);
$auth->validate($loginUser->login, $loginUser->password);
// persist() and remove() throw ReadOnlyEntityException:
$em->persist($loginUser); // throws

When remove() is called on an entity, UnitOfWork automatically marks all other MANAGED entities in the same IdentityMap that share the same table and primary key as REMOVED. Only one DELETE is issued — sibling entities are just dropped from tracking, preventing ghost IdentityMap reads and phantom UPDATE calls on the deleted row.

Good fit

Different bounded contexts need different views of the same data; adding a relation for one workflow shouldn’t affect every other workflow; long-running processes need tighter control over tracked entities.

Probably not needed yet

Your application has one stable entity model per table and your current ORM already handles that well — Articulate still fits, and the slice model can pay off later as contexts diverge, but adopting it now likely won’t solve a problem you have today.

  • Entity Mapping guide — the #[Entity], #[Property], and index attributes in practice.
  • Optimistic Locking guide — how version guards stay per-slice instead of row-wide.
  • articulate:warm-metadata-cache — pre-builds metadata for every discovered entity class (including each slice of a shared table) so the first request after deploy doesn’t pay the discovery cost. See Module Map.