@@ -21,30 +21,30 @@ namespace App\Hotel\Domain\Event;
2121
2222use Patchlevel\EventSourcing\Aggregate\Uuid;
2323use Patchlevel\EventSourcing\Attribute\Event;
24- use Patchlevel\EventSourcing\Serializer\Normalizer\IdNormalizer;
2524
2625#[Event('hotel.created')]
2726final class HotelCreated
2827{
2928 public function __construct(
30- #[IdNormalizer]
31- public readonly Uuid $id,
29+ public readonly Uuid $hotelId,
3230 public readonly string $hotelName,
3331 ) {
3432 }
3533}
3634```
37- A guest can check in by ` name ` :
35+ A guest can check in by ` guestName ` :
3836
3937``` php
4038namespace App\Hotel\Domain\Event;
4139
40+ use Patchlevel\EventSourcing\Aggregate\Uuid;
4241use Patchlevel\EventSourcing\Attribute\Event;
4342
4443#[Event('hotel.guest_is_checked_in')]
4544final class GuestIsCheckedIn
4645{
4746 public function __construct(
47+ public readonly Uuid $hotelId,
4848 public readonly string $guestName,
4949 ) {
5050 }
@@ -55,12 +55,14 @@ And also check out again:
5555``` php
5656namespace App\Hotel\Domain\Event;
5757
58+ use Patchlevel\EventSourcing\Aggregate\Uuid;
5859use Patchlevel\EventSourcing\Attribute\Event;
5960
6061#[Event('hotel.guest_is_checked_out')]
6162final class GuestIsCheckedOut
6263{
6364 public function __construct(
65+ public readonly Uuid $hotelId,
6466 public readonly string $guestName,
6567 ) {
6668 }
@@ -128,7 +130,7 @@ final class Hotel extends BasicAggregateRoot
128130 throw new GuestHasAlreadyCheckedIn($guestName);
129131 }
130132
131- $this->recordThat(new GuestIsCheckedIn($guestName));
133+ $this->recordThat(new GuestIsCheckedIn($this->id, $ guestName));
132134 }
133135
134136 public function checkOut(string $guestName): void
@@ -137,7 +139,7 @@ final class Hotel extends BasicAggregateRoot
137139 throw new IsNotAGuest($guestName);
138140 }
139141
140- $this->recordThat(new GuestIsCheckedOut($guestName));
142+ $this->recordThat(new GuestIsCheckedOut($this->id, $ guestName));
141143 }
142144
143145 #[Apply]
@@ -172,74 +174,97 @@ final class Hotel extends BasicAggregateRoot
172174
173175## Define projections
174176
175- So that we can see all the hotels on our website and also see how many guests are currently visiting the hotels,
176- we need a projection for it. To create a projection we need a projector.
177+ Now we want to see which guests are currently checked in at a hotel or when a guest checked in and out.
178+ For this we need a projection and to create a projection we need a projector.
177179Each projector is then responsible for a specific projection.
178180
179181``` php
180182namespace App\Hotel\Infrastructure\Projection;
181183
182184use App\Hotel\Domain\Event\GuestIsCheckedIn;
183185use App\Hotel\Domain\Event\GuestIsCheckedOut;
184- use App\Hotel\Domain\Event\HotelCreated;
185186use Doctrine\DBAL\Connection;
187+ use Patchlevel\EventSourcing\Aggregate\Uuid;
186188use Patchlevel\EventSourcing\Attribute\Projector;
187189use Patchlevel\EventSourcing\Attribute\Setup;
188190use Patchlevel\EventSourcing\Attribute\Subscribe;
189191use Patchlevel\EventSourcing\Attribute\Teardown;
190192use Patchlevel\EventSourcing\Subscription\Subscriber\SubscriberUtil;
191193
192- #[Projector('hotel')]
193- final class HotelProjection
194+ /**
195+ * @psalm-type GuestData = array{
196+ * guest_name: string,
197+ * hotel_id: string,
198+ * check_in_date: string,
199+ * check_out_date: string|null
200+ * }
201+ */
202+ #[Projector('guests')]
203+ final class GuestProjection
194204{
195205 use SubscriberUtil;
196206
197207 public function __construct(
198- private Connection $projectionConnection ,
208+ private Connection $db ,
199209 ) {
200210 }
201211
202- /** @return list<array {id: string, name: string, guests: int} > */
203- public function getHotels( ): array
212+ /** @return list<GuestData > */
213+ public function findGuestsByHotelId(Uuid $hotelId ): array
204214 {
205- return $this->db->fetchAllAssociative("SELECT id, name, guests FROM {$this->table()};");
215+ return $this->db->createQueryBuilder()
216+ ->select('*')
217+ ->from($this->table())
218+ ->where('hotel_id = :hotel_id')
219+ ->setParameter('hotel_id', $hotelId->toString())
220+ ->fetchAllAssociative();
206221 }
207222
208- #[Subscribe(HotelCreated::class)]
209- public function handleHotelCreated(HotelCreated $event): void
210- {
223+ #[Subscribe(GuestIsCheckedIn::class)]
224+ public function onGuestIsCheckedIn(
225+ GuestIsCheckedIn $event,
226+ DateTimeImmutable $recordedOn,
227+ ): void {
211228 $this->db->insert(
212229 $this->table(),
213230 [
214- 'id' => $event->id->toString(),
215- 'name' => $event->hotelName,
216- 'guests' => 0,
231+ 'hotel_id' => $event->hotelId->toString(),
232+ 'guest_name' => $event->guestName,
233+ 'check_in_date' => $recordedOn->format('Y-m-d H:i:s'),
234+ 'check_out_date' => null,
217235 ],
218236 );
219237 }
220238
221- #[Subscribe(GuestIsCheckedIn::class)]
222- public function handleGuestIsCheckedIn(Uuid $hotelId): void
223- {
224- $this->db->executeStatement(
225- "UPDATE {$this->table()} SET guests = guests + 1 WHERE id = ?;",
226- [$hotelId->toString()],
227- );
228- }
229-
230239 #[Subscribe(GuestIsCheckedOut::class)]
231- public function handleGuestIsCheckedOut(Uuid $hotelId): void
232- {
233- $this->db->executeStatement(
234- "UPDATE {$this->table()} SET guests = guests - 1 WHERE id = ?;",
235- [$hotelId->toString()],
240+ public function onGuestIsCheckedOut(
241+ GuestIsCheckedOut $event,
242+ DateTimeImmutable $recordedOn,
243+ ): void {
244+ $this->db->update(
245+ $this->table(),
246+ [
247+ 'check_out_date' => $recordedOn->format('Y-m-d H:i:s'),
248+ ],
249+ [
250+ 'hotel_id' => $event->hotelId->toString(),
251+ 'guest_name' => $event->guestName,
252+ 'check_out_date' => null,
253+ ],
236254 );
237255 }
238256
239257 #[Setup]
240258 public function create(): void
241259 {
242- $this->db->executeStatement("CREATE TABLE IF NOT EXISTS {$this->table()} (id VARCHAR PRIMARY KEY, name VARCHAR, guests INTEGER);");
260+ $this->db->executeStatement(
261+ "CREATE TABLE {$this->table()} (
262+ hotel_id VARCHAR(36) NOT NULL,
263+ guest_name VARCHAR(255) NOT NULL,
264+ check_in_date TIMESTAMP NOT NULL,
265+ check_out_date TIMESTAMP NULL
266+ );",
267+ );
243268 }
244269
245270 #[Teardown]
@@ -271,15 +296,16 @@ namespace App\Hotel\Application\Processor;
271296
272297use App\Hotel\Domain\Event\GuestIsCheckedIn;
273298use Patchlevel\EventSourcing\Attribute\Processor;
299+ use Patchlevel\EventSourcing\Attribute\Subscribe;
274300use Symfony\Component\Mailer\MailerInterface;
275301use Symfony\Component\Mime\Email;
276302
277303use function sprintf;
278304
279305#[Processor('admin_emails')]
280- final class SendCheckInEmailListener
306+ final class SendCheckInEmailProcessor
281307{
282- private function __construct(
308+ public function __construct(
283309 private readonly MailerInterface $mailer,
284310 ) {
285311 }
@@ -312,7 +338,12 @@ So that we can actually write the data to a database, we need the associated sch
312338``` bash
313339bin/console event-sourcing:database:create
314340bin/console event-sourcing:schema:create
315- bin/console event-sourcing:subscription:setup
341+ ```
342+ or you can use doctrine migrations:
343+
344+ ``` bash
345+ bin/console event-sourcing:migrations:diff
346+ bin/console event-sourcing:migrations:migrate
316347```
317348!!! note
318349
@@ -326,7 +357,7 @@ We are now ready to use the Event Sourcing System. We can load, change and save
326357namespace App\Hotel\Infrastructure\Controller;
327358
328359use App\Hotel\Domain\Hotel;
329- use App\Hotel\Infrastructure\Projection\HotelProjection ;
360+ use App\Hotel\Infrastructure\Projection\GuestProjection ;
330361use Patchlevel\EventSourcing\Aggregate\Uuid;
331362use Patchlevel\EventSourcing\Repository\Repository;
332363use Symfony\Component\HttpFoundation\JsonResponse;
@@ -337,26 +368,26 @@ use Symfony\Component\Routing\Annotation\Route;
337368#[AsController]
338369final class HotelController
339370{
371+ /** @param Repository<Hotel > $hotelRepository */
340372 public function __construct(
341- private readonly HotelProjection $hotelProjection,
342- /** @var Repository<Hotel > */
343373 private readonly Repository $hotelRepository,
374+ private readonly GuestProjection $guestProjection,
344375 ) {
345376 }
346377
347- #[Route('/', methods:['GET'])]
348- public function listAction( ): JsonResponse
378+ #[Route('/{hotelId}/guests ', methods:['GET'])]
379+ public function hotelGuestsAction(Uuid $hotelId ): JsonResponse
349380 {
350381 return new JsonResponse(
351- $this->hotelProjection->getHotels( ),
382+ $this->guestProjection->findGuestsByHotelId($hotelId ),
352383 );
353384 }
354385
355386 #[Route('/create', methods:['POST'])]
356387 public function createAction(Request $request): JsonResponse
357388 {
358- $hotelName = $request->request ->get('name'); // need validation!
359- $id = Uuid::v7 ();
389+ $hotelName = $request->getPayload() ->get('name'); // need validation!
390+ $id = Uuid::generate ();
360391
361392 $hotel = Hotel::create($id, $hotelName);
362393 $this->hotelRepository->save($hotel);
@@ -367,7 +398,7 @@ final class HotelController
367398 #[Route('/{hotelId}/check-in', methods:['POST'])]
368399 public function checkInAction(Uuid $hotelId, Request $request): JsonResponse
369400 {
370- $guestName = $request->request ->get('name'); // need validation!
401+ $guestName = $request->getPayload() ->get('name'); // need validation!
371402
372403 $hotel = $this->hotelRepository->load($hotelId);
373404 $hotel->checkIn($guestName);
@@ -379,7 +410,7 @@ final class HotelController
379410 #[Route('/{hotelId}/check-out', methods:['POST'])]
380411 public function checkOutAction(Uuid $hotelId, Request $request): JsonResponse
381412 {
382- $guestName = $request->request ->get('name'); // need validation!
413+ $guestName = $request->getPayload() ->get('name'); // need validation!
383414
384415 $hotel = $this->hotelRepository->load($hotelId);
385416 $hotel->checkOut($guestName);
0 commit comments