From 8cc0c4bb639e9ee95b635e9786f67bb61793d11e Mon Sep 17 00:00:00 2001 From: Sandro Gehri Date: Tue, 27 May 2025 11:39:05 +0200 Subject: [PATCH] Add event alias map --- src/Event.php | 76 +++++++++++++++++++ src/Exceptions/EventMapViolationException.php | 26 +++++++ src/Lifecycle/EventStore.php | 2 +- src/Support/Serializer.php | 1 + tests/Feature/EventMapTest.php | 42 ++++++++++ 5 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 src/Exceptions/EventMapViolationException.php create mode 100644 tests/Feature/EventMapTest.php diff --git a/src/Event.php b/src/Event.php index 23951c41..d67f2057 100644 --- a/src/Event.php +++ b/src/Event.php @@ -6,6 +6,7 @@ use Illuminate\Support\Str; use LogicException; use Throwable; +use Thunk\Verbs\Exceptions\EventMapViolationException; use Thunk\Verbs\Exceptions\EventNotAuthorized; use Thunk\Verbs\Exceptions\EventNotValid; use Thunk\Verbs\Exceptions\EventNotValidForCurrentState; @@ -23,6 +24,18 @@ abstract class Event { public int $id; + /** + * An array to map event names to their class names in the database. + * + * @var array> + */ + public static array $eventMap = []; + + /** + * Prevents storing events without an entry in the event map. + */ + protected static bool $requireEventMap = false; + public static function __callStatic(string $name, array $arguments) { return static::make()->$name(...$arguments); @@ -102,4 +115,67 @@ protected function assert($assertion, ?string $exception = null, ?string $messag throw new $exception($message); } + + /** + * Prevents storing events without an entry in the event map. + */ + public static function requireEventMap(bool $requireEventMap = true) + { + static::$requireEventMap = $requireEventMap; + } + + /** + * Determine if the event map is required. + */ + public static function requiresEventMap(): bool + { + return static::$requireEventMap; + } + + /** + * Set or get the event map. + * + * @param array>|null $map + * @return array> + */ + public static function eventMap(?array $map = null, bool $merge = true): array + { + if (is_array($map)) { + static::$eventMap = $merge && static::$eventMap + ? $map + static::$eventMap + : $map; + } + + return static::$eventMap; + } + + /** + * Get the event associated with a custom event name. + * + * @return class-string|null + */ + public static function getMappedEvent(string $alias): ?string + { + return static::$eventMap[$alias] ?? null; + } + + /** + * Get the alias associated with an event class. + * + * @param class-string $className + */ + public static function getAlias(string $className): string + { + $alias = array_search($className, static::$eventMap, strict: true); + + if ($alias !== false) { + return $alias; + } + + if (self::requiresEventMap()) { + throw new EventMapViolationException($className); + } + + return $className; + } } diff --git a/src/Exceptions/EventMapViolationException.php b/src/Exceptions/EventMapViolationException.php new file mode 100644 index 00000000..5a45acab --- /dev/null +++ b/src/Exceptions/EventMapViolationException.php @@ -0,0 +1,26 @@ + $event + */ + public function __construct(string $event) + { + parent::__construct("No alias defined for event [{$event}]."); + + $this->event = $event; + } +} diff --git a/src/Lifecycle/EventStore.php b/src/Lifecycle/EventStore.php index 40b8fd44..6930dd64 100644 --- a/src/Lifecycle/EventStore.php +++ b/src/Lifecycle/EventStore.php @@ -125,7 +125,7 @@ protected function formatForWrite(array $event_objects): array { return array_map(fn (Event $event) => [ 'id' => Id::from($event->id), - 'type' => $event::class, + 'type' => Event::getAlias($event::class), 'data' => app(Serializer::class)->serialize($event), 'metadata' => app(Serializer::class)->serialize($this->metadata->get($event)), 'created_at' => app(MetadataManager::class)->getEphemeral($event, 'created_at', now()), diff --git a/src/Support/Serializer.php b/src/Support/Serializer.php index d8fa8709..ad9638dd 100644 --- a/src/Support/Serializer.php +++ b/src/Support/Serializer.php @@ -41,6 +41,7 @@ public function deserialize( string|array $data, bool $call_constructor = false, ) { + $target = is_string($target) ? (Event::getMappedEvent($target) ?? $target) : $target; $type = $target; $context = $this->context; diff --git a/tests/Feature/EventMapTest.php b/tests/Feature/EventMapTest.php new file mode 100644 index 00000000..3ba068a3 --- /dev/null +++ b/tests/Feature/EventMapTest.php @@ -0,0 +1,42 @@ + EventMapEventWithAlias::class, + ]); + + EventMapEventWithAlias::fire(); + EventMapEvent::fire(); + + Verbs::commit(); + + [$eventWithAlias, $eventWithoutAlias] = \Thunk\Verbs\Models\VerbEvent::all(); + + expect($eventWithAlias) + ->type->toBe('with-alias') + ->event()->toBeInstanceOf(EventMapEventWithAlias::class); + + expect($eventWithoutAlias) + ->type->toBe(EventMapEvent::class) + ->event()->toBeInstanceOf(EventMapEvent::class); +}); + +test('using an event without an entry in the event map throws an exception when event map is required', function () { + Event::eventMap([ + 'with-alias' => EventMapEventWithAlias::class, + ]); + EventMapEvent::requireEventMap(); + + EventMapEvent::fire(); + + Verbs::commit(); +}) + ->throws(\Thunk\Verbs\Exceptions\EventMapViolationException::class, 'No alias defined for event [EventMapEvent].') + ->after(fn () => EventMapEvent::requireEventMap(false)); + +class EventMapEventWithAlias extends Event {} + +class EventMapEvent extends Event {}