Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions src/Event.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<string, class-string<Event>>
*/
public static array $eventMap = [];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this should be public, and be on the Event abstract class. This is something that you would typically push into the service that handles event resolution, then from there it can be stored as a stateful, non-static property so that it can't get easily overwritten or altered outside of a service provider.

I would feel a lot more comfortable if this was managed via the Verbs service, and exposed via the Verbs facade, e.g.:

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Verbs::eventMap([
            'some-event' => \App\Events\SomeEvent::class,
        ]);
        Verbs::requireEventMap();
        Verbs::stateMap([
            'some-state' => \App\States\SomeState::class,
        ]);
        Verbs::requireStateMap();
    }
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯, see my comment below.

My initial intention was to make it more similar to Laravel where the morph map is on the Relation class.


/**
* 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);
Expand Down Expand Up @@ -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<string, class-string<Event>>|null $map
* @return array<string, class-string<Event>>
*/
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<Event>|null
*/
public static function getMappedEvent(string $alias): ?string
{
return static::$eventMap[$alias] ?? null;
}

/**
* Get the alias associated with an event class.
*
* @param class-string<Event> $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;
}
}
26 changes: 26 additions & 0 deletions src/Exceptions/EventMapViolationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace Thunk\Verbs\Exceptions;

use RuntimeException;
use Thunk\Verbs\Event;

class EventMapViolationException extends RuntimeException
{
/**
* The name of the affected event.
*/
public string $event;

/**
* Create a new exception instance.
*
* @param class-string<Event> $event
*/
public function __construct(string $event)
{
parent::__construct("No alias defined for event [{$event}].");

$this->event = $event;
}
}
2 changes: 1 addition & 1 deletion src/Lifecycle/EventStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand Down
1 change: 1 addition & 0 deletions src/Support/Serializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
42 changes: 42 additions & 0 deletions tests/Feature/EventMapTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

use Thunk\Verbs\Event;
use Thunk\Verbs\Facades\Verbs;

it('can store and restore an event with and without alias', function () {
Event::eventMap([
'with-alias' => 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 {}
Loading