Entities, by definition, have an identity, which we can use to save it and get it back from storage again.
$this->orderRepository->ofId($orderId);
Identity as an auto-incrementing integer
If the ID is an auto-incrementing integer, its value will only be known when the repository has saved the entity.
$orderId = $orderRepository−>save($order);
The real issue is that we're still not replacing the underlying storage technology:
- Not every database will support auto-incrementing ID columns.
- Only some databases will be able to generate an ID and return it.
- Some persistence mechanisms might not return an identifier to the client synchronously.
Another problem is the Entity Consistency; the entity is supposed to be complete from the beginning. Given that an entity only has an ID once saved, we come to the opposite conclusion: the entity is consistent once the database has saved it.
What we'd like instead is a way to provide an entity with an ID the moment we instantiate it.
final class Order
{
public function __construct(
public readonly int $id,
// ...
) {
}
}
Clients will then have to supply an ID upfront when they want to create a new entity. But how could a client find out what the next available ID is? Using the entity's repository method nextIdentity()
.
interface OrderRepository
{
public function nextIdentity(): int;
// ...
}
nextIdentity()
returns the next available ID.A robust implementation to avoid concurrency issues is using a sequence at the database level.
public function nextIdentity(): int
{
return $this−>connection−>transactional(function () {
$nextId = (int)$this−>connection−>execute(
'SELECT last_id FROM order_id_sequence'
)−>fetchColumn(0) + 1;
$this−>connection−>execute(
'UPDATE order_id_sequence SET last_id = :last_id',
[
'last_id' => $nextId
]
);
return $nextId;
});
}
nextIdentity()
uses a sequence table.Identity as a UUID
A better alternative to using incrementing integers would be to use a Universally Unique Identifier (UUID). A UUID is often represented as a string but can be converted to a big integer and back again. A UUID is based on the current time and a random number generated by the system's random device.
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;
final class SqlOrderRepository implements OrderRepository
{
public function nextIdentity(): UuidInterface
{
return Uuid::uuid4();
}
// ...
}
nextIdentity()
uses the ramsey/uuid
library for generating a random UUID.Strongly Typed IDs: Using a Value Object for the identifier
Now that we're changing an Order's identifier type let's wrap the identifier inside a Value Object. That way, we can fully encapsulate the actual data type of the identifier. Strongly Typed IDs lead us to a more expressive design and solve primitive obsession from entity identifiers.
final class OrderId
{
private function __construct(
public readonly UuidInterface $id,
) {
}
public static function fromUuid(UuidInterface $id): self
{
return new self($id);
}
}
final class Order
{
public function __construct(
public readonly OrderId $id,
// ...
) {
}
}
final class SqlOrderRepository implements OrderRepository
{
public function nextIdentity(): OrderId
{
return OrderId::fromUuid(Uuid::uuid4());
}
// ...
}
OrderId
hides the underlying ID type.Also, we can delegate the OrderId
value object to create a new value, so we can stop using the repository's nextIdentity()
.
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;
final class OrderId
{
private function __construct(
public readonly UuidInterface $id,
) {
}
public static function fromUuid(UuidInterface $id): self
{
return new self($id);
}
public static function random(): self
{
return new self(Uuid::uuid4());
}
}