From 1f14d22d1b75828f6673346a5dd3b58405eb6bc5 Mon Sep 17 00:00:00 2001 From: David Badura Date: Mon, 16 Dec 2024 13:02:32 +0100 Subject: [PATCH] update docs --- docs/pages/aggregate.md | 12 +++-- docs/pages/events.md | 1 + docs/pages/getting_started.md | 89 +++++++++++++++++++++++------------ docs/pages/subscription.md | 2 +- 4 files changed, 68 insertions(+), 36 deletions(-) diff --git a/docs/pages/aggregate.md b/docs/pages/aggregate.md index fafe6209a..12741db8f 100644 --- a/docs/pages/aggregate.md +++ b/docs/pages/aggregate.md @@ -170,12 +170,14 @@ In order to change the state of the aggregates afterwards, only further events h As example we can add a `NameChanged` event: ```php +use Patchlevel\EventSourcing\Aggregate\Uuid; use Patchlevel\EventSourcing\Attribute\Event; #[Event('profile.name_changed')] final class NameChanged { public function __construct( + public readonly Uuid $profileId, public readonly string $name, ) { } @@ -217,7 +219,7 @@ final class Profile extends BasicAggregateRoot public function changeName(string $name): void { - $this->recordThat(new NameChanged($name)); + $this->recordThat(new NameChanged($this->id, $name)); } #[Apply] @@ -538,7 +540,7 @@ final class Profile extends BasicAggregateRoot public function changeName(Name $name): void { - $this->recordThat(new NameChanged($name)); + $this->recordThat(new NameChanged($this->id, $name)); } #[Apply] @@ -552,12 +554,14 @@ In order for the whole thing to work, we still have to adapt our `NameChanged` e since we only expected a string before but now passed a `Name` value object. ```php +use Patchlevel\EventSourcing\Aggregate\Uuid; use Patchlevel\EventSourcing\Attribute\Event; #[Event('profile.name_changed')] final class NameChanged { public function __construct( + public readonly Uuid $profileId, #[NameNormalizer] public readonly Name $name, ) { @@ -604,13 +608,13 @@ final class Hotel extends BasicAggregateRoot throw new NoPlaceException($name); } - $this->recordThat(new RoomBocked($name)); + $this->recordThat(new RoomBocked($this->id, $name)); if ($this->people !== self::SIZE) { return; } - $this->recordThat(new FullyBooked()); + $this->recordThat(new FullyBooked($this->id)); } #[Apply] diff --git a/docs/pages/events.md b/docs/pages/events.md index 4541319fb..c7f981b2a 100644 --- a/docs/pages/events.md +++ b/docs/pages/events.md @@ -53,6 +53,7 @@ use Patchlevel\EventSourcing\Attribute\Event; #[Event(name: 'profile.registered', aliases: ['profile.created'])] final class ProfileRegistered { + // ... } ``` When saving, the name will always be used. However, when loading, aliases will also be taken into account. diff --git a/docs/pages/getting_started.md b/docs/pages/getting_started.md index f151bfeea..7d3661dc2 100644 --- a/docs/pages/getting_started.md +++ b/docs/pages/getting_started.md @@ -26,12 +26,14 @@ final class HotelCreated A guest can check in by `name`: ```php +use Patchlevel\EventSourcing\Aggregate\Uuid; use Patchlevel\EventSourcing\Attribute\Event; #[Event('hotel.guest_checked_in')] final class GuestIsCheckedIn { public function __construct( + public readonly Uuid $hotelId, public readonly string $guestName, ) { } @@ -40,12 +42,14 @@ final class GuestIsCheckedIn And also check out again: ```php +use Patchlevel\EventSourcing\Aggregate\Uuid; use Patchlevel\EventSourcing\Attribute\Event; #[Event('hotel.guest_checked_out')] final class GuestIsCheckedOut { public function __construct( + public readonly Uuid $hotelId, public readonly string $guestName, ) { } @@ -102,19 +106,19 @@ final class Hotel extends BasicAggregateRoot public function checkIn(string $guestName): void { if (in_array($guestName, $this->guests, true)) { - throw new GuestHasAlreadyCheckedIn($guestName); + throw new GuestHasAlreadyCheckedIn($this->id, $guestName); } - $this->recordThat(new GuestIsCheckedIn($guestName)); + $this->recordThat(new GuestIsCheckedIn($this->id, $guestName)); } public function checkOut(string $guestName): void { if (!in_array($guestName, $this->guests, true)) { - throw new IsNotAGuest($guestName); + throw new IsNotAGuest($this->id, $guestName); } - $this->recordThat(new GuestIsCheckedOut($guestName)); + $this->recordThat(new GuestIsCheckedOut($this->id, $guestName)); } #[Apply] @@ -162,57 +166,80 @@ use Patchlevel\EventSourcing\Attribute\Subscribe; use Patchlevel\EventSourcing\Attribute\Teardown; use Patchlevel\EventSourcing\Subscription\Subscriber\SubscriberUtil; -#[Projector('hotel')] -final class HotelProjector +/** + * @psalm-type GuestData = array{ + * guest_name: string, + * hotel_id: string, + * check_in_date: string, + * check_out_date: string|null + * } + */ +#[Projector('guests')] +final class GuestProjection { use SubscriberUtil; public function __construct( - private readonly Connection $db, + private Connection $db, ) { } - /** @return list */ - public function getHotels(): array + /** @return list */ + public function findGuestsByHotelId(Uuid $hotelId): array { - return $this->db->fetchAllAssociative("SELECT id, name, guests FROM {$this->table()};"); + return $this->db->createQueryBuilder() + ->select('*') + ->from($this->table()) + ->where('hotel_id = :hotel_id') + ->setParameter('hotel_id', $hotelId->toString()) + ->fetchAllAssociative(); } - #[Subscribe(HotelCreated::class)] - public function handleHotelCreated(HotelCreated $event, Uuid $aggregateId): void - { + #[Subscribe(GuestIsCheckedIn::class)] + public function onGuestIsCheckedIn( + GuestIsCheckedIn $event, + DateTimeImmutable $recordedOn, + ): void { $this->db->insert( $this->table(), [ - 'id' => $aggregateId->toString(), - 'name' => $event->hotelName, - 'guests' => 0, + 'hotel_id' => $event->hotelId->toString(), + 'guest_name' => $event->guestName, + 'check_in_date' => $recordedOn->format('Y-m-d H:i:s'), + 'check_out_date' => null, ], ); } - #[Subscribe(GuestIsCheckedIn::class)] - public function handleGuestIsCheckedIn(Uuid $aggregateId): void - { - $this->db->executeStatement( - "UPDATE {$this->table()} SET guests = guests + 1 WHERE id = ?;", - [$aggregateId->toString()], - ); - } - #[Subscribe(GuestIsCheckedOut::class)] - public function handleGuestIsCheckedOut(Uuid $aggregateId): void - { - $this->db->executeStatement( - "UPDATE {$this->table()} SET guests = guests - 1 WHERE id = ?;", - [$aggregateId->toString()], + public function onGuestIsCheckedOut( + GuestIsCheckedOut $event, + DateTimeImmutable $recordedOn, + ): void { + $this->db->update( + $this->table(), + [ + 'check_out_date' => $recordedOn->format('Y-m-d H:i:s'), + ], + [ + 'hotel_id' => $event->hotelId->toString(), + 'guest_name' => $event->guestName, + 'check_out_date' => null, + ], ); } #[Setup] public function create(): void { - $this->db->executeStatement("CREATE TABLE IF NOT EXISTS {$this->table()} (id VARCHAR PRIMARY KEY, name VARCHAR, guests INTEGER);"); + $this->db->executeStatement( + "CREATE TABLE {$this->table()} ( + hotel_id VARCHAR(36) NOT NULL, + guest_name VARCHAR(255) NOT NULL, + check_in_date TIMESTAMP NOT NULL, + check_out_date TIMESTAMP NULL + );", + ); } #[Teardown] diff --git a/docs/pages/subscription.md b/docs/pages/subscription.md index 2851e564f..b7e1335dc 100644 --- a/docs/pages/subscription.md +++ b/docs/pages/subscription.md @@ -498,7 +498,7 @@ At this step, you must process all the data. The `rollbackBatch` method is called when an error occurs and the batching needs to be aborted. Here, you can respond to the error and potentially perform a database rollback. -The method `forceCommit` is called after each handled event, +The method `forceCommit` is called after each handled event, and you can decide whether the batch commit process should start now. This helps to determine the batch size and thus avoid memory overflow.