Entity — an object with identity that persists over time
Concept
Entity — in Domain-Driven Design (DDD), an object that has a distinct identity that persists over time and across different representations.
Defined by identity, not attributes: If I rename "Alice" to "Alicia," she's still the same person — same identity, different attributes. If I copy her profile and make a new account with the same details, it's a DIFFERENT person — same attributes, different identity.
Entity vs Value Object:
- Entity: Identity matters. Two entities with the same attributes are still different entities if they have different IDs.
User(id=1, name='Alice')≠User(id=2, name='Alice'). - Value Object: Identity doesn't matter. Two value objects with the same attributes ARE the same thing.
Money(100, 'USD')==Money(100, 'USD')— they're equal.
Entity lifecycle: Entities are created, change over time, and are eventually deleted. They have a continuous identity through all those changes.
In PHP (Eloquent model as Entity): An Eloquent model with a primary key id is an entity in the DDD sense. The id is the identity. Two User models with the same id represent the same entity.
Aggregate root: In DDD, an entity that serves as the single entry point for a cluster of objects. Order is an aggregate root. OrderItem entities live inside the Order aggregate — you don't manipulate OrderItems directly, only through the Order.
Entity identity in PHP: Entities are compared by their identity (ID), not by value. $user1->is($user2) in Eloquent — checks if two models represent the same row.
Code Example
<?php
// ENTITY — identity matters
class User
{
public function __construct(
public readonly int $id, // THE identity
public string $name,
public string $email,
) {}
// Entities are equal when they have the SAME ID
public function equals(self $other): bool
{
return $this->id === $other->id;
}
}
$alice1 = new User(1, 'Alice', 'alice@example.com');
$alice2 = new User(1, 'Alice Smith', 'alice@work.com'); // different attributes, same ID
var_dump($alice1->equals($alice2)); // true — same identity
$alice3 = new User(2, 'Alice', 'alice@example.com'); // same attributes, different ID
var_dump($alice1->equals($alice3)); // false — different identity
// AGGREGATE ROOT — entry point for a cluster of entities
class Order // aggregate root
{
private array $items = [];
public function __construct(
public readonly int $id,
public readonly int $userId,
private string $status = 'pending',
) {}
// ONLY manipulate items through the aggregate root
public function addItem(int $productId, int $quantity, float $price): void
{
if ($this->status !== 'pending') {
throw new \DomainException('Cannot add items to a non-pending order');
}
$this->items[] = new OrderItem($productId, $quantity, $price); // creates a child entity
}
public function total(): float
{
return array_sum(array_map(fn($i) => $i->subtotal(), $this->items));
}
public function confirm(): void
{
if (empty($this->items)) throw new \DomainException('Cannot confirm empty order');
$this->status = 'confirmed';
}
}
class OrderItem // child entity — exists within the Order aggregate
{
public readonly int $id;
public function __construct(
public readonly int $productId,
public readonly int $quantity,
public readonly float $price,
) {}
public function subtotal(): float { return $this->quantity * $this->price; }
}
// ELOQUENT ENTITY — Eloquent model is an entity
class User extends \Illuminate\Database\Eloquent\Model
{
// $user->id is the identity
}
$user1 = User::find(1);
$user2 = User::find(1); // same row, different PHP object
$user1->is($user2); // true — same identity (same primary key)
$user1 === $user2; // false — different PHP object references
// Eloquent's is() method is the correct way to compare entities