diff --git a/src/Exceptions/UnableToReadEventException.php b/src/Exceptions/UnableToReadEventException.php new file mode 100644 index 00000000..da232c9a --- /dev/null +++ b/src/Exceptions/UnableToReadEventException.php @@ -0,0 +1,5 @@ +realNow(); } + + /** + * Takes a map of new event classes to legacy event classes + * + * @param array>|array $map + */ + public function mapLegacyEvents(array $map): void + { + app(EventStore::class)->mapLegacyEvents($map); + } } diff --git a/src/Lifecycle/EventStore.php b/src/Lifecycle/EventStore.php index b68e5c1d..e3594472 100644 --- a/src/Lifecycle/EventStore.php +++ b/src/Lifecycle/EventStore.php @@ -21,6 +21,8 @@ class EventStore implements StoresEvents { + private array $event_class_map = []; + public function __construct( protected MetadataManager $metadata, ) {} @@ -148,4 +150,25 @@ protected function formatRelationshipsForWrite(array $event_objects): array ])) ->all(); } + + /** + * @param array>|array $map + */ + public function mapLegacyEvents(array $map): void + { + foreach ($map as $new_event => $legacy_events) { + if (! is_array($legacy_events)) { + $legacy_events = [$legacy_events]; + } + + foreach ($legacy_events as $legacy_event) { + $this->event_class_map[$legacy_event] = $new_event; + } + } + } + + public function resolveType(string $type): string + { + return $this->event_class_map[$type] ?? $type; + } } diff --git a/src/Models/VerbEvent.php b/src/Models/VerbEvent.php index 63e20e6d..ae40b658 100644 --- a/src/Models/VerbEvent.php +++ b/src/Models/VerbEvent.php @@ -5,6 +5,8 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Thunk\Verbs\Event; +use Thunk\Verbs\Exceptions\UnableToReadEventException; +use Thunk\Verbs\Lifecycle\EventStore; use Thunk\Verbs\Lifecycle\MetadataManager; use Thunk\Verbs\Metadata; use Thunk\Verbs\State; @@ -40,7 +42,17 @@ public function getTable() public function event(): Event { - $this->event ??= app(Serializer::class)->deserialize($this->type, $this->data); + $type = app(EventStore::class)->resolveType($this->type); + + try { + $this->event ??= app(Serializer::class)->deserialize($type, $this->data); + } catch (\ReflectionException $e) { + if (preg_match('/Class "([^"]+)" does not exist/', $e->getMessage(), $matches)) { + throw new UnableToReadEventException("Event class {$matches[1]} not found, if this event has been renamed, consider using `mapLegacyEvents` to map the old event to the new event."); + } + throw $e; + } + $this->event->id = $this->id; app(MetadataManager::class)->setEphemeral($this->event, 'created_at', $this->created_at); diff --git a/tests/Feature/LegacyEventMapTest.php b/tests/Feature/LegacyEventMapTest.php new file mode 100644 index 00000000..32232dae --- /dev/null +++ b/tests/Feature/LegacyEventMapTest.php @@ -0,0 +1,60 @@ + LegacyEvent::class, + ]); + + LegacyEvent::fire(name: 'test'); + + Verbs::commit(); + + $events = app(\Thunk\Verbs\Lifecycle\EventStore::class)->read(); + + $this->assertInstanceOf(NewEvent::class, $events->first()); +}); + +it('can map legacy events to new events as a nested array', function () { + Verbs::mapLegacyEvents([ + NewEvent::class => [LegacyEvent::class], + ]); + + LegacyEvent::fire(name: 'test'); + + Verbs::commit(); + + $events = app(\Thunk\Verbs\Lifecycle\EventStore::class)->read(); + + $this->assertInstanceOf(NewEvent::class, $events->first()); +}); + +it('throws a helpful exception when an event is not found in the map', function () { + NewEvent::fire(name: 'test'); + + Verbs::commit(); + + $event = \Thunk\Verbs\Models\VerbEvent::first(); + $event->type = 'non-existent-event'; + $event->save(); + + $this->expectException(\Thunk\Verbs\Exceptions\UnableToReadEventException::class); + + $events = app(\Thunk\Verbs\Lifecycle\EventStore::class)->read(); +}); + +class LegacyEvent extends Event +{ + public function __construct( + public string $name, + ) {} +} + +class NewEvent extends Event +{ + public function __construct( + public string $name, + ) {} +}