|
1 | 1 | # Identifier |
2 | 2 |
|
3 | | -The `aggregate id` is a unique identifier for an aggregate. |
4 | | -It is used to identify the aggregate in the event store. |
5 | | -The `aggregate` does not care how the id is generated, |
6 | | -since only an aggregate-wide unique string is expected in the store. |
| 3 | +Identifiers are small, immutable value objects that represent IDs across the library in a type‑safe and consistent way. They are primarily used for aggregate root IDs, but can also be applied anywhere a stable string identifier is required (snapshots, repositories, your own domain objects, etc.). |
7 | 4 |
|
8 | | -This library provides you with a few options for generating the id. |
| 5 | +All identifiers must implement `Patchlevel\EventSourcing\Identifier\Identifier` and provide a stable string representation for storage and serialization. |
9 | 6 |
|
10 | | -!!! warning |
11 | | - |
12 | | - Performance reasons, the default configuration of the store require an uuid string for `aggregate id`. |
13 | | - But technically, for the library, it can be any string. |
14 | | - If you want to use a custom id, you have to change the `aggregate_id_type` in the [store](store.md) configuration. |
15 | | - |
16 | | -## Uuid |
17 | | - |
18 | | -The easiest way is to use an `uuid` as an aggregate ID. |
19 | | -For this, we have the `Uuid` class, which is a simple wrapper for the [ramsey/uuid](https://github.com/ramsey/uuid) library. |
20 | | - |
21 | | -You can use it like this: |
| 7 | +## Interface |
22 | 8 |
|
23 | 9 | ```php |
24 | | -use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; |
25 | | -use Patchlevel\EventSourcing\Attribute\Aggregate; |
26 | | -use Patchlevel\EventSourcing\Attribute\Id; |
27 | | -use Patchlevel\EventSourcing\Identifier\Uuid; |
| 10 | +use Patchlevel\EventSourcing\Identifier\Identifier; |
28 | 11 |
|
29 | | -#[Aggregate('profile')] |
30 | | -final class Profile extends BasicAggregateRoot |
| 12 | +interface Identifier |
31 | 13 | { |
32 | | - #[Id] |
33 | | - private Uuid $id; |
| 14 | + public function toString(): string; |
| 15 | + |
| 16 | + public static function fromString(string $id): static; |
34 | 17 | } |
35 | 18 | ``` |
36 | | -You have multiple options for generating an uuid: |
| 19 | + |
| 20 | +The string value returned by `toString()` is what gets persisted (e.g. in the event store) and serialized. `fromString()` reconstructs the value object from that string. |
| 21 | + |
| 22 | +## Built‑in identifiers |
| 23 | + |
| 24 | +We provide two ready‑to‑use implementations: |
| 25 | + |
| 26 | +### `Uuid` |
| 27 | + |
| 28 | +`Uuid` wraps [ramsey/uuid](https://github.com/ramsey/uuid) and uses UUID v7 by default, which is a good fit for event‑sourcing. |
37 | 29 |
|
38 | 30 | ```php |
39 | 31 | use Patchlevel\EventSourcing\Identifier\Uuid; |
40 | 32 |
|
41 | 33 | $uuid = Uuid::generate(); |
42 | 34 | $uuid = Uuid::fromString('d6e8d7a0-4b0b-4e6a-8a9a-3a0b2d9d0e4e'); |
43 | 35 | ``` |
44 | | -!!! Note |
45 | 36 |
|
46 | | - We implemented the version 7 of the uuid, because it is most suitable for event sourcing. |
47 | | - More information about uuid versions can be found [here](https://uuid.ramsey.dev/en/stable/rfc4122.html). |
48 | | - |
49 | | -## Custom ID |
| 37 | +!!! note |
| 38 | + |
| 39 | + UUID v7 provides k‑sortable identifiers that work well with append‑only streams and database indexes. See the ramsey docs for details. |
50 | 40 |
|
51 | | -If you don't want to use an uuid, you can also use the custom ID implementation. |
52 | | -This is a value object that holds any string. |
| 41 | +### `CustomId` |
| 42 | + |
| 43 | +`CustomId` is a minimal string‑backed identifier. Use it if you want full control over the string format or if your IDs are provided by an external system. |
53 | 44 |
|
54 | 45 | ```php |
55 | | -use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; |
56 | | -use Patchlevel\EventSourcing\Attribute\Aggregate; |
57 | | -use Patchlevel\EventSourcing\Attribute\Id; |
58 | 46 | use Patchlevel\EventSourcing\Identifier\CustomId; |
59 | 47 |
|
60 | | -#[Aggregate('profile')] |
61 | | -final class Profile extends BasicAggregateRoot |
62 | | -{ |
63 | | - #[Id] |
64 | | - private CustomId $id; |
65 | | -} |
| 48 | +$id = CustomId::fromString('my-id'); |
66 | 49 | ``` |
67 | | -!!! warning |
68 | 50 |
|
69 | | - If you want to use a custom id that is not an uuid, |
70 | | - you need to change the `aggregate_id_type` to `string` in the store configuration. |
71 | | - More information can be found [here](store.md). |
72 | | - |
73 | | -So you can use any string as an id: |
| 51 | +## Domain‑specific identifiers |
| 52 | + |
| 53 | +For better domain modeling, define your own identifier types by implementing `Identifier` and encapsulating creation rules and validation. Two traits are available to make this easy: |
| 54 | + |
| 55 | +- `RamseyUuidV7Behaviour` for UUID v7 identifiers |
| 56 | +- `CustomIdBehaviour` for string‑backed identifiers |
74 | 57 |
|
75 | 58 | ```php |
76 | | -use Patchlevel\EventSourcing\Identifier\CustomId; |
| 59 | +use Patchlevel\EventSourcing\Identifier\Identifier; |
| 60 | +use Patchlevel\EventSourcing\Identifier\RamseyUuidV7Behaviour; |
77 | 61 |
|
78 | | -$id = CustomId::fromString('my-id'); |
| 62 | +final class ProfileId implements Identifier |
| 63 | +{ |
| 64 | + use RamseyUuidV7Behaviour; // adds generate(), fromString(), toString() |
| 65 | +} |
79 | 66 | ``` |
80 | | -## Implement own ID |
81 | 67 |
|
82 | | -Or even better, you create your own aggregate-specific ID class. |
83 | | -This allows you to ensure that the correct id is always used. |
84 | | -The whole thing looks like this: |
| 68 | +or |
85 | 69 |
|
86 | 70 | ```php |
87 | 71 | use Patchlevel\EventSourcing\Identifier\Identifier; |
| 72 | +use Patchlevel\EventSourcing\Identifier\CustomIdBehaviour; |
88 | 73 |
|
89 | | -class ProfileId implements Identifier |
| 74 | +final class OrderNumber implements Identifier |
90 | 75 | { |
91 | | - private function __construct( |
92 | | - private readonly string $id, |
93 | | - ) { |
94 | | - } |
95 | | - |
96 | | - public function toString(): string |
97 | | - { |
98 | | - return $this->id; |
99 | | - } |
100 | | - |
101 | | - public static function fromString(string $id): static |
102 | | - { |
103 | | - return new self($id); |
104 | | - } |
| 76 | + use CustomIdBehaviour; // simple string‑backed identifier |
105 | 77 | } |
106 | 78 | ``` |
107 | | -So you can use it like this: |
| 79 | + |
| 80 | +## Using identifiers with aggregates |
| 81 | + |
| 82 | +Aggregates expose and persist their IDs as `Identifier` instances. You can choose the concrete implementation that fits your domain. |
108 | 83 |
|
109 | 84 | ```php |
110 | 85 | use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; |
111 | 86 | use Patchlevel\EventSourcing\Attribute\Aggregate; |
112 | 87 | use Patchlevel\EventSourcing\Attribute\Id; |
| 88 | +use Patchlevel\EventSourcing\Identifier\Uuid; |
113 | 89 |
|
114 | 90 | #[Aggregate('profile')] |
115 | 91 | final class Profile extends BasicAggregateRoot |
116 | 92 | { |
117 | 93 | #[Id] |
118 | | - private ProfileId $id; |
| 94 | + private Uuid $id; |
119 | 95 | } |
120 | 96 | ``` |
121 | | -We also offer you some traits, so that you don't have to implement the `AggregateRootId` interface yourself. |
122 | | -Here for the uuid: |
| 97 | + |
| 98 | +Or use your domain‑specific identifier: |
123 | 99 |
|
124 | 100 | ```php |
125 | | -use Patchlevel\EventSourcing\Identifier\Identifier; |
126 | | -use Patchlevel\EventSourcing\Identifier\RamseyUuidV7Behaviour; |
| 101 | +use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; |
| 102 | +use Patchlevel\EventSourcing\Attribute\Aggregate; |
| 103 | +use Patchlevel\EventSourcing\Attribute\Id; |
127 | 104 |
|
128 | | -class ProfileId implements Identifier |
| 105 | +#[Aggregate('profile')] |
| 106 | +final class Profile extends BasicAggregateRoot |
129 | 107 | { |
130 | | - use RamseyUuidV7Behaviour; |
| 108 | + #[Id] |
| 109 | + private ProfileId $id; |
131 | 110 | } |
132 | 111 | ``` |
133 | | -Or for the custom id: |
134 | 112 |
|
135 | | -```php |
136 | | -use Patchlevel\EventSourcing\Identifier\CustomIdBehaviour; |
137 | | -use Patchlevel\EventSourcing\Identifier\Identifier; |
| 113 | +## Serialization and normalization |
| 114 | + |
| 115 | +Identifiers integrate with the serializer via `IdNormalizer`. The `Identifier` interface is annotated so that instances are automatically normalized to strings and denormalized back to the correct class. |
| 116 | + |
| 117 | +This means you can safely use identifier types in command payloads, events, or snapshots without writing custom normalizers. |
| 118 | + |
| 119 | +## Testing |
| 120 | + |
| 121 | +For deterministic tests involving UUIDs, use `FakeRamseyUuidFactory` to generate predictable UUID v7 values. |
| 122 | + |
| 123 | +## Notes |
| 124 | + |
| 125 | +- Identifiers are stored as strings in the backing store, but you should always use concrete identifier types in your domain code for type‑safety and clarity. |
| 126 | +- You can use identifiers beyond aggregates wherever an opaque, stable ID is needed. |
138 | 127 |
|
139 | | -class ProfileId implements Identifier |
140 | | -{ |
141 | | - use CustomIdBehaviour; |
142 | | -} |
143 | | -``` |
144 | 128 | ### Learn more |
145 | 129 |
|
146 | 130 | * [How to create an aggregate](aggregate.md) |
|
0 commit comments