diff --git a/docs/attributes.md b/docs/attributes.md index 7d05c899..e3e9f34a 100644 --- a/docs/attributes.md +++ b/docs/attributes.md @@ -13,11 +13,15 @@ class YourEvent extends Event } ``` -The `StateId` attribute takes a `state_type`, an optional [`alias`](https://verbs.thunk.dev/docs/reference/states#content-aliasstring-alias-state-state) string, and by default can [automatically generate](/docs/technical/ids#content-automatically-generating-ids)(`autofill`) a `snowflake_id` for you. +The `StateId` attribute takes a `state_type`, an optional [ +`alias`](https://verbs.thunk.dev/docs/reference/states#content-aliasstring-alias-state-state) string, and by default +can [automatically generate](/docs/technical/ids#content-automatically-generating-ids)(`autofill`) a `snowflake_id` for +you. ### `#[AppliesToState]` -Another way to link states and events; like [`StateId`](#content-stateid), but using the attributes above the class instead of on each individual id. +Another way to link states and events; like [`StateId`](#content-stateid), but using the attributes above the class +instead of on each individual id. ```php #[AppliesToState(GameState::class)] @@ -34,7 +38,8 @@ class RolledDice extends Event } ``` -`AppliesToState` has the same params as `StateId`, with an additional optional `id` param (after `state_type`) if you want to specify which prop belongs to which state. +`AppliesToState` has the same params as `StateId`, with an additional optional `id` param (after `state_type`) if you +want to specify which prop belongs to which state. ```php #[AppliesToState(state_type: GameState::class, id: foo_id)] @@ -51,24 +56,8 @@ class RolledDice extends Event } ``` -Otherwise, with `AppliesToState`, Verbs will find the `id` for you based on your State's prefix (i.e. `ExampleState` would be `example`, meaning `example_id` or `example_ids` would be associated automatically). - -### `#[AppliesToSingletonState]` - -Use the `AppliesToSingletonState` attribute on an event class to tell Verbs that it should always be applied to a single state (e.g. `CountState`) across the entire application (as opposed to having different counts for different states). - -Because we're using a [singleton state](/docs/reference/states#content-singleton-states), there is no need for the event to have a `$count_id`. - -```php -#[AppliesToSingletonState(CountState::class)] -class IncrementCount extends Event -{ - public function apply(CountState $state) - { - $state->count++; - } -} -``` +Otherwise, with `AppliesToState`, Verbs will find the `id` for you based on your State's prefix (i.e. `ExampleState` +would be `example`, meaning `example_id` or `example_ids` would be associated automatically). In addition to your `state_type` param, you may also set an optional `alias` string. @@ -76,7 +65,8 @@ In addition to your `state_type` param, you may also set an optional `alias` str Use the `AppliesToChildState` attribute on an event class to allow Verbs to access a nested state. -For our example, let's make sure our `ParentState` has a `child_id` property pointing to a `ChildState` by firing a `ChildAddedToParent` event: +For our example, let's make sure our `ParentState` has a `child_id` property pointing to a `ChildState` by firing a +`ChildAddedToParent` event: ```php ChildAddedToParent::fire(parent_id: 1, child_id: 2); @@ -103,6 +93,7 @@ class ParentState extends State public int $child_id; } ``` + ```php class ChildState extends State { @@ -110,7 +101,8 @@ class ChildState extends State } ``` -Now that `ParentState` has a record of our `ChildState`, we can load the child *through* the parent with `AppliesToChildState`. +Now that `ParentState` has a record of our `ChildState`, we can load the child *through* the parent with +`AppliesToChildState`. Let's show this by firing a `NestedStateAccessed` event with our new attribute: @@ -134,9 +126,12 @@ class NestedStateAccessed extends Event } } ``` -`AppliesToChildState` takes a `state_type` (your child state), `parent_type`, `id` (your child state id), and an optional `alias` string. -When you use `AppliesToChildState`, don't forget to also use `StateId` or [`AppliesToState`](/docs/technical/attributes#content-appliestostate) to identify the `parent_id`. +`AppliesToChildState` takes a `state_type` (your child state), `parent_type`, `id` (your child state id), and an +optional `alias` string. + +When you use `AppliesToChildState`, don't forget to also use `StateId` or [ +`AppliesToState`](/docs/technical/attributes#content-appliestostate) to identify the `parent_id`. -Verbs uses the [Symfony Serializer component](https://symfony.com/components/Serializer) to serialize your PHP Event objects to JSON. +Verbs uses the [Symfony Serializer component](https://symfony.com/components/Serializer) to serialize your PHP Event +objects to JSON. -The default normalizers should handle most stock Laravel applications, but you may need to add your own normalizers for certain object types, which you can do in `config/verbs.php`. +The default normalizers should handle most stock Laravel applications, but you may need to add your own normalizers for +certain object types, which you can do in `config/verbs.php`. -You can also use our interface `SerializedByVerbs` in tandem with trait `NormalizeToPropertiesAndClassName` on classes to support custom types. +You can also use our interface `SerializedByVerbs` in tandem with trait `NormalizeToPropertiesAndClassName` on classes +to support custom types. -You can see good implentation of this in one of our [examples](https://github.com/hirethunk/verbs/blob/main/examples/Monopoly/src/Game/Spaces/Space.php), `examples/Monopoly/src/Game/Spaces/Space.php` +You can see good implentation of this in one of +our [examples](https://github.com/hirethunk/verbs/blob/main/examples/Monopoly/src/Game/Spaces/Space.php), +`examples/Monopoly/src/Game/Spaces/Space.php` diff --git a/docs/states.md b/docs/states.md index 902607db..af6aa39c 100644 --- a/docs/states.md +++ b/docs/states.md @@ -118,9 +118,6 @@ in [event lifecycle](/docs/technical/event-lifecycle). ## Loading a State -All state instances are singletons, scoped to an [id](/docs/technical/ids). i.e. say we had a Card Game app--if we apply -a `CardDiscarded` event, we make sure only the `CardState` state with its globablly unique `card_id` is affected. - To retrieve the State, simply call load: ```php @@ -148,29 +145,22 @@ Route::get('/users/{user_state}', function(UserState $user_state) { ## Singleton States -You may want a state that only needs one iteration across the entire application--this is called a singleton state. -Singleton states require no id, since there is no need to differentiate among state instances. +You may want a state that only needs one iteration across the entire application—this is called a singleton state. +Singleton states require no ID because there is only ever one copy in existence across your entire app. -In our events that apply to a singleton state, we simply need to use the -`AppliesToSingletonState` [attribute](/docs/technical/attributes#content-appliestosingletonstate). +To tell Verbs to treat a State as a singleton, extend the `SingletonState` class, rather than `State`. ```php -#[AppliesToSingletonState(CountState::class)] -class IncrementCount extends Event +class CountState extends State implements SingletonState { - public function apply(CountState $state) - { - $state->count++; - } + // ... } ``` -This event uses `AppliesToSingletonState` to tell Verbs that it should always be applied to a single `CountState` across -the entire application (as opposed to having different counts for different situations). - ### Loading the singleton state -Since singleton's require no IDs, simply call the `singleton()` method. +Since singletons require no IDs, simply call the `singleton()` method. Trying to load a singleton state in any +other way will result in a `BadMethodCall` exception. ```php YourState::singleton(); diff --git a/docs/testing.md b/docs/testing.md index 1534ac23..fa757dbe 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -4,7 +4,8 @@ We enjoy improving Verbs by providing easy, readable testing affordances. When testing verbs events, you'll need to call [commit](/docs/reference/events#content-committing) manually. -You may continue manually adding `Verbs::commit()` after each `Event::fire()` method; however, we've created `Verbs::commitImmediately` to issue a blanket commit on all events you fire in tests. +You may continue manually adding `Verbs::commit()` after each `Event::fire()` method; however, we've created +`Verbs::commitImmediately` to issue a blanket commit on all events you fire in tests. ```php beforeEach(function () { @@ -19,7 +20,8 @@ You may also implement the `CommitsImmediately` interface directly on an Event. The following Test `assert()` methods are available to thoroughly check your committing granularly. -Before using these methods, add `Verbs::fake()` to your test so Verbs can set up a fake event store to isolate the testing environment. +Before using these methods, add `Verbs::fake()` to your test so Verbs can set up a fake event store to isolate the +testing environment. ```php Verbs::assertNothingCommitted(); @@ -29,9 +31,11 @@ Verbs::assertNotCommitted(...); ## State Factories -In tests, you may find yourself needing to fire and commit several events in order to bring your State to the point where it actually needs testing. +In tests, you may find yourself needing to fire and commit several events in order to bring your State to the point +where it actually needs testing. -The `State::factory()` method allows you to bypass manually building up the State, functioning similarly to `Model::factory()`. +The `State::factory()` method allows you to bypass manually building up the State, functioning similarly to +`Model::factory()`. This allows you to call: @@ -58,15 +62,20 @@ Or, in the case of a [singleton state](/docs/reference/states#content-singleton- ChurnState::factory()->create(['churn' => 40]); ``` -Next, we'll get into how these factories work, and continue after with some [Verbs factory methods](testing#content-factory-methods) you may already be familiar with from Eloquent factories. +Next, we'll get into how these factories work, and continue after with +some [Verbs factory methods](testing#content-factory-methods) you may already be familiar with from Eloquent factories. ### `VerbsStateInitialized` -Under the hood, these methods will fire (and immediately commit) a new `VerbsStateInitialized` event, which will fire onto the given state, identified by the id argument (if id is null, we assume it is a singleton) and return a copy of that state. +Under the hood, these methods will fire (and immediately commit) a new `VerbsStateInitialized` event, which will fire +onto the given state, identified by the id argument (if id is null, we assume it is a singleton) and return a copy of +that state. -This is primarily designed for booting up states for testing. If you are migrating non-event-sourced codebases to Verbs, when there is a need to initiate a state for legacy data, it's better to create a custom `MigratedFromLegacy` event. +This is primarily designed for booting up states for testing. If you are migrating non-event-sourced codebases to Verbs, +when there is a need to initiate a state for legacy data, it's better to create a custom `MigratedFromLegacy` event. -You may also change the initial event fired from the StateFactory from `VerbsStateInitialized` to an event class of your choosing by setting an `$intial_event` property on your State Factory. +You may also change the initial event fired from the StateFactory from `VerbsStateInitialized` to an event class of your +choosing by setting an `$intial_event` property on your State Factory. ```php class ExampleStateFactory extends StateFactory @@ -75,11 +84,13 @@ class ExampleStateFactory extends StateFactory } ``` -`VerbsStateInitialized` implements the `CommitsImmediately` interface detailed [above](testing#content-verbscommit), so if you change from this initial event makes sure to extend the interface on your replacement event. +`VerbsStateInitialized` implements the `CommitsImmediately` interface detailed [above](testing#content-verbscommit), so +if you change from this initial event makes sure to extend the interface on your replacement event. ### Factory Methods -Some methods accept Verbs [IDs](/docs/technical/ids), which, written longform, could be any of these types: `Bits|UuidInterface|AbstractUid|int|string`. +Some methods accept Verbs [IDs](/docs/technical/ids), which, written longform, could be any of these types: +`Bits|UuidInterface|AbstractUid|int|string`. For brevity, this will be abbreviated in the following applicable methods as `Id`. @@ -99,14 +110,6 @@ Set the state ID explicitly (cannot be used with `count`). UserState::factory()->id(123)->create(); ``` -#### `singleton()` - -Mark that this is a singleton state (cannot be used with `count`). - -```php -UserState::factory()->singleton()->create(); -``` - #### `state(callable|array $data)` Default data (will be overridden by `create`). @@ -168,7 +171,8 @@ If you'd like to chain behavior after your Factory `create()` executes, do so in #### `configure()` -The configure method in your custom factory allows you to set `afterMaking` and `afterCreating` effects (see [laravel docs](https://laravel.com/docs/11.x/eloquent-factories#factory-callbacks)). +The configure method in your custom factory allows you to set `afterMaking` and `afterCreating` effects ( +see [laravel docs](https://laravel.com/docs/11.x/eloquent-factories#factory-callbacks)). ##### `afterMaking()` & `afterCreating()` diff --git a/examples/Counter/src/Events/IncrementCount.php b/examples/Counter/src/Events/IncrementCount.php index 2f3c8fa5..8c5d1bc1 100644 --- a/examples/Counter/src/Events/IncrementCount.php +++ b/examples/Counter/src/Events/IncrementCount.php @@ -2,21 +2,21 @@ namespace Thunk\Verbs\Examples\Counter\Events; -use Thunk\Verbs\Attributes\Autodiscovery\AppliesToSingletonState; +use Thunk\Verbs\Attributes\Autodiscovery\AppliesToState; use Thunk\Verbs\Event; use Thunk\Verbs\Examples\Counter\States\CountState; /** * In our most basic example of event sourcing, we just use a single event - * to increment a counter. This event uses `AppliesToSingletonState` to tell - * Verbs that it should always be applied to a single `CountState` across the + * to increment a counter. Because CountState is a SingletonState object, + * Verbs will always apply this event to a single `CountState` across the * entire application (as opposed to having different counts for different * situations). * * Because we're using a singleton state, there is no need for the event to * have a `$count_id`. */ -#[AppliesToSingletonState(CountState::class)] +#[AppliesToState(CountState::class)] class IncrementCount extends Event { public function apply(CountState $state) diff --git a/examples/Counter/src/Events/IncrementCountTwice.php b/examples/Counter/src/Events/IncrementCountTwice.php index 8393dec3..c346eee3 100644 --- a/examples/Counter/src/Events/IncrementCountTwice.php +++ b/examples/Counter/src/Events/IncrementCountTwice.php @@ -2,11 +2,11 @@ namespace Thunk\Verbs\Examples\Counter\Events; -use Thunk\Verbs\Attributes\Autodiscovery\AppliesToSingletonState; +use Thunk\Verbs\Attributes\Autodiscovery\AppliesToState; use Thunk\Verbs\Event; use Thunk\Verbs\Examples\Counter\States\CountState; -#[AppliesToSingletonState(CountState::class)] +#[AppliesToState(CountState::class)] class IncrementCountTwice extends Event { public function handle() diff --git a/examples/Counter/src/States/CountState.php b/examples/Counter/src/States/CountState.php index a2a96d27..edd87e3c 100644 --- a/examples/Counter/src/States/CountState.php +++ b/examples/Counter/src/States/CountState.php @@ -2,9 +2,9 @@ namespace Thunk\Verbs\Examples\Counter\States; -use Thunk\Verbs\State; +use Thunk\Verbs\SingletonState; -class CountState extends State +class CountState extends SingletonState { public int $count = 0; } diff --git a/examples/Counter/tests/InitializeStateTest.php b/examples/Counter/tests/InitializeStateTest.php index 11843a70..9746d3e9 100644 --- a/examples/Counter/tests/InitializeStateTest.php +++ b/examples/Counter/tests/InitializeStateTest.php @@ -7,7 +7,7 @@ use Thunk\Verbs\Models\VerbEvent; it('State factory initializes a state', function () { - $count_state = CountState::factory()->singleton()->create([ + $count_state = CountState::factory()->create([ 'count' => 1337, ]); diff --git a/examples/Subscriptions/src/Events/GlobalReportGenerated.php b/examples/Subscriptions/src/Events/GlobalReportGenerated.php index 75e1d0f8..7f35d97e 100644 --- a/examples/Subscriptions/src/Events/GlobalReportGenerated.php +++ b/examples/Subscriptions/src/Events/GlobalReportGenerated.php @@ -2,13 +2,13 @@ namespace Thunk\Verbs\Examples\Subscriptions\Events; -use Thunk\Verbs\Attributes\Autodiscovery\AppliesToSingletonState; +use Thunk\Verbs\Attributes\Autodiscovery\AppliesToState; use Thunk\Verbs\Attributes\Hooks\Once; use Thunk\Verbs\Event; use Thunk\Verbs\Examples\Subscriptions\Models\Report; use Thunk\Verbs\Examples\Subscriptions\States\GlobalReportState; -#[AppliesToSingletonState(GlobalReportState::class)] +#[AppliesToState(GlobalReportState::class)] class GlobalReportGenerated extends Event { #[Once] diff --git a/examples/Subscriptions/src/Events/SubscriptionCancelled.php b/examples/Subscriptions/src/Events/SubscriptionCancelled.php index 9c800a19..e278c775 100644 --- a/examples/Subscriptions/src/Events/SubscriptionCancelled.php +++ b/examples/Subscriptions/src/Events/SubscriptionCancelled.php @@ -3,7 +3,6 @@ namespace Thunk\Verbs\Examples\Subscriptions\Events; use Thunk\Verbs\Attributes\Autodiscovery\AppliesToChildState; -use Thunk\Verbs\Attributes\Autodiscovery\AppliesToSingletonState; use Thunk\Verbs\Attributes\Autodiscovery\AppliesToState; use Thunk\Verbs\Event; use Thunk\Verbs\Examples\Subscriptions\Models\Subscription; @@ -13,7 +12,7 @@ #[AppliesToState(state_type: SubscriptionState::class, id: 'subscription_id', alias: 'subscription')] #[AppliesToChildState(state_type: PlanReportState::class, parent_type: SubscriptionState::class, id: 'plan_id', alias: 'plan')] -#[AppliesToSingletonState(state_type: GlobalReportState::class, alias: 'report')] +#[AppliesToState(state_type: GlobalReportState::class, alias: 'report')] class SubscriptionCancelled extends Event { public int $subscription_id; diff --git a/examples/Subscriptions/src/States/GlobalReportState.php b/examples/Subscriptions/src/States/GlobalReportState.php index 127310f0..a1c3f3a9 100644 --- a/examples/Subscriptions/src/States/GlobalReportState.php +++ b/examples/Subscriptions/src/States/GlobalReportState.php @@ -5,9 +5,9 @@ use Illuminate\Support\Carbon; use Thunk\Verbs\Examples\Subscriptions\Events\SubscriptionCancelled; use Thunk\Verbs\Examples\Subscriptions\Events\SubscriptionStarted; -use Thunk\Verbs\State; +use Thunk\Verbs\SingletonState; -class GlobalReportState extends State +class GlobalReportState extends SingletonState { public int $total_subscriptions = 0; diff --git a/src/Attributes/Autodiscovery/AppliesToSingletonState.php b/src/Attributes/Autodiscovery/AppliesToSingletonState.php deleted file mode 100644 index 3d931074..00000000 --- a/src/Attributes/Autodiscovery/AppliesToSingletonState.php +++ /dev/null @@ -1,27 +0,0 @@ -state_type, State::class, true)) { - throw new InvalidArgumentException('You must pass state class names to the "AppliesToSingletonState" attribute.'); - } - } - - public function discoverState(Event $event, StateManager $manager): State - { - return $manager->singleton($this->state_type); - } -} diff --git a/src/Attributes/Autodiscovery/AppliesToState.php b/src/Attributes/Autodiscovery/AppliesToState.php index 32dd227e..7af9d5f4 100644 --- a/src/Attributes/Autodiscovery/AppliesToState.php +++ b/src/Attributes/Autodiscovery/AppliesToState.php @@ -8,6 +8,7 @@ use InvalidArgumentException; use Thunk\Verbs\Event; use Thunk\Verbs\Lifecycle\StateManager; +use Thunk\Verbs\SingletonState; use Thunk\Verbs\State; #[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] @@ -26,6 +27,10 @@ public function __construct( public function discoverState(Event $event, StateManager $manager): State|array { + if (is_subclass_of($this->state_type, SingletonState::class)) { + return $this->state_type::singleton(); + } + $property = $this->getStateIdProperty($event); $id = $event->{$property}; diff --git a/src/Contracts/StoresEvents.php b/src/Contracts/StoresEvents.php index aa145e18..057356d8 100644 --- a/src/Contracts/StoresEvents.php +++ b/src/Contracts/StoresEvents.php @@ -14,7 +14,6 @@ interface StoresEvents public function read( ?State $state = null, Bits|UuidInterface|AbstractUid|int|string|null $after_id = null, - bool $singleton = false ): LazyCollection; /** @param Event[] $events */ diff --git a/src/Events/VerbsStateInitialized.php b/src/Events/VerbsStateInitialized.php index 920f1974..84cd3cbc 100644 --- a/src/Events/VerbsStateInitialized.php +++ b/src/Events/VerbsStateInitialized.php @@ -4,6 +4,7 @@ use Thunk\Verbs\CommitsImmediately; use Thunk\Verbs\Event; +use Thunk\Verbs\SingletonState; use Thunk\Verbs\Support\StateCollection; /** @template TStateType */ @@ -14,15 +15,18 @@ public function __construct( public int|string $state_id, public string $state_class, public array $state_data, - public bool $singleton = false, ) {} /** @return StateCollection */ public function states(): StateCollection { - return StateCollection::make([ - $this->singleton ? $this->state_class::singleton() : $this->state_class::load($this->state_id), - ]); + $state = is_subclass_of($this->state_class, SingletonState::class) + ? $this->state_class::singleton() + : $this->state_class::load($this->state_id); + + $state->id = $this->state_id; + + return StateCollection::make([$state]); } public function validate() diff --git a/src/Lifecycle/EventStore.php b/src/Lifecycle/EventStore.php index e2a65dbb..40b8fd44 100644 --- a/src/Lifecycle/EventStore.php +++ b/src/Lifecycle/EventStore.php @@ -16,6 +16,7 @@ use Thunk\Verbs\Facades\Id; use Thunk\Verbs\Models\VerbEvent; use Thunk\Verbs\Models\VerbStateEvent; +use Thunk\Verbs\SingletonState; use Thunk\Verbs\State; use Thunk\Verbs\Support\Serializer; @@ -28,9 +29,8 @@ public function __construct( public function read( ?State $state = null, Bits|UuidInterface|AbstractUid|int|string|null $after_id = null, - bool $singleton = false, ): LazyCollection { - return $this->readEvents($state, $after_id, $singleton) + return $this->readEvents($state, $after_id) ->each(fn (VerbEvent $model) => $this->metadata->set($model->event(), $model->metadata())) ->map(fn (VerbEvent $model) => $model->event()); } @@ -50,12 +50,11 @@ public function write(array $events): bool protected function readEvents( ?State $state, Bits|UuidInterface|AbstractUid|int|string|null $after_id, - bool $singleton, ): LazyCollection { if ($state) { return VerbStateEvent::query() ->with('event') - ->unless($singleton, fn (Builder $query) => $query->where('state_id', $state->id)) + ->unless($state instanceof SingletonState, fn (Builder $query) => $query->where('state_id', $state->id)) ->where('state_type', $state::class) ->when($after_id, fn (Builder $query) => $query->whereRelation('event', 'id', '>', Id::from($after_id))) ->lazyById() diff --git a/src/Lifecycle/StateManager.php b/src/Lifecycle/StateManager.php index 26aef786..9ce4553f 100644 --- a/src/Lifecycle/StateManager.php +++ b/src/Lifecycle/StateManager.php @@ -47,7 +47,12 @@ public function load(Bits|UuidInterface|AbstractUid|iterable|int|string $id, str : $this->loadOne($id, $type); } - /** @param class-string $type */ + /** + * @template TStateClass of State + * + * @param class-string $type + * @return TStateClass + */ public function singleton(string $type): State { // FIXME: If the state we're loading has a last_event_id that's ahead of the registry's last_event_id, we need to re-build the state @@ -56,14 +61,14 @@ public function singleton(string $type): State return $state; } - $state = $this->snapshots->loadSingleton($type) ?? $type::make(); + $state = $this->snapshots->loadSingleton($type) ?? new $type; $state->id ??= snowflake_id(); // We'll store a reference to it by the type for future singleton access $this->states->put($type, $state); $this->remember($state); - $this->reconstitute($state, singleton: true); + $this->reconstitute($state); return $state; } @@ -180,13 +185,13 @@ protected function loadMany(iterable $ids, string $type): StateCollection ); } - protected function reconstitute(State $state, bool $singleton = false): static + protected function reconstitute(State $state): static { // When we're replaying, the Broker is in charge of applying the correct events // to the State, so we only need to do it *outside* of replays. if (! $this->is_replaying) { $this->events - ->read(state: $state, after_id: $state->last_event_id, singleton: $singleton) + ->read(state: $state, after_id: $state->last_event_id) ->each(fn (Event $event) => $this->dispatcher->apply($event)); // It's possible for an event to mutate state out of order when reconstituting, diff --git a/src/SingletonState.php b/src/SingletonState.php new file mode 100644 index 00000000..a0167f19 --- /dev/null +++ b/src/SingletonState.php @@ -0,0 +1,51 @@ +singleton(static::class); + } + + public function resolveRouteBinding($value, $field = null) + { + return static::singleton(); + } + + public function resolveChildRouteBinding($childType, $value, $field) + { + throw new RuntimeException('Resolving child state via routing is not supported.'); + } +} diff --git a/src/State.php b/src/State.php index 17b1e57d..8d243e47 100644 --- a/src/State.php +++ b/src/State.php @@ -87,11 +87,6 @@ protected static function normalizeKey(mixed $from) : $from; } - public static function singleton(): static - { - return app(StateManager::class)->singleton(static::class); - } - public function storedEvents() { return app(StoresEvents::class) diff --git a/src/StateFactory.php b/src/StateFactory.php index 6023be7d..e41d5bb5 100644 --- a/src/StateFactory.php +++ b/src/StateFactory.php @@ -42,7 +42,6 @@ public function __construct( protected Collection $transformations = new Collection, protected ?int $count = null, protected int|string|null $id = null, - protected bool $singleton = false, protected ?Generator $faker = null, protected Collection $makeCallbacks = new Collection, protected Collection $createCallbacks = new Collection, @@ -93,11 +92,6 @@ public function id(Bits|UuidInterface|AbstractUid|int|string $id): static return $this->clone(['id' => Id::from($id)]); } - public function singleton(bool $singleton = true): static - { - return $this->clone(['singleton' => $singleton]); - } - /** @return TStateType|StateCollection */ public function create(array $data = [], Bits|UuidInterface|AbstractUid|int|string|null $id = null): State|StateCollection { @@ -121,7 +115,7 @@ public function create(array $data = [], Bits|UuidInterface|AbstractUid|int|stri return StateCollection::make([$this->createState()]); } - if ($this->singleton) { + if (is_subclass_of($this->state_class, SingletonState::class)) { throw new RuntimeException('You cannot create multiple singleton states of the same type.'); } @@ -146,7 +140,6 @@ protected function createState(): State state_id: $this->id ?? Id::make(), state_class: $this->state_class, state_data: $this->getRawData(), - singleton: $this->singleton, ) : $this->initial_event::fire( ...$this->getRawData(), @@ -179,7 +172,6 @@ protected function clone(array $with = []): static transformations: $with['transformations'] ?? $this->transformations, count: $with['count'] ?? $this->count, id: $with['id'] ?? $this->id, - singleton: $with['singleton'] ?? $this->singleton, faker: $with['faker'] ?? $this->faker, makeCallbacks: $with['makeCallbacks'] ?? $this->makeCallbacks, createCallbacks: $with['createCallbacks'] ?? $this->createCallbacks, diff --git a/src/Testing/EventStoreFake.php b/src/Testing/EventStoreFake.php index 508ac711..186b569c 100644 --- a/src/Testing/EventStoreFake.php +++ b/src/Testing/EventStoreFake.php @@ -14,6 +14,7 @@ use Thunk\Verbs\Event; use Thunk\Verbs\Facades\Id; use Thunk\Verbs\Lifecycle\MetadataManager; +use Thunk\Verbs\SingletonState; use Thunk\Verbs\State; class EventStoreFake implements StoresEvents @@ -32,15 +33,14 @@ public function __construct( public function read( ?State $state = null, UuidInterface|string|int|AbstractUid|Bits|null $after_id = null, - bool $singleton = false ): LazyCollection { return LazyCollection::make($this->events) ->flatten() ->when($after_id, function (LazyCollection $events, $after_id) { return $events->filter(fn (Event $event) => $event->id > Id::from($after_id)); }) - ->when($state, function (LazyCollection $events, State $state) use ($singleton) { - return $singleton + ->when($state, function (LazyCollection $events, State $state) { + return $state instanceof SingletonState ? $events->filter(fn (Event $event) => $event->state($state::class) !== null) : $events->filter(fn (Event $event) => $event->state($state::class)?->id === $state->id); }) diff --git a/tests/Unit/ConcurrencyTest.php b/tests/Unit/ConcurrencyTest.php index 00dc7fe9..76b59833 100644 --- a/tests/Unit/ConcurrencyTest.php +++ b/tests/Unit/ConcurrencyTest.php @@ -4,7 +4,7 @@ use Thunk\Verbs\Exceptions\ConcurrencyException; use Thunk\Verbs\Lifecycle\EventStore; use Thunk\Verbs\Models\VerbEvent; -use Thunk\Verbs\State; +use Thunk\Verbs\SingletonState; use Thunk\Verbs\Support\StateCollection; it('does not throw on sequential events', function () { @@ -49,4 +49,4 @@ public function states(): StateCollection } } -class ConcurrencyTestState extends State {} +class ConcurrencyTestState extends SingletonState {} diff --git a/tests/Unit/FactoryTest.php b/tests/Unit/FactoryTest.php index e6897359..c846be11 100644 --- a/tests/Unit/FactoryTest.php +++ b/tests/Unit/FactoryTest.php @@ -2,6 +2,7 @@ use Illuminate\Support\Collection; use Thunk\Verbs\Lifecycle\StateManager; +use Thunk\Verbs\SingletonState; use Thunk\Verbs\State; use Thunk\Verbs\StateFactory; @@ -56,11 +57,11 @@ }); test('it can create a singleton state', function () { - $singleton_state = FactoryTestState::factory()->singleton()->create(); + $singleton_state = FactoryTestSingletonState::factory()->create(); expect($singleton_state->id)->not->toBeNull(); - $retreived_state = app(StateManager::class)->singleton(FactoryTestState::class); + $retreived_state = app(StateManager::class)->singleton(FactoryTestSingletonState::class); expect($retreived_state)->toBe($singleton_state); }); @@ -91,6 +92,11 @@ class FactoryTestState extends State public string $name; } +class FactoryTestSingletonState extends SingletonState +{ + public string $name; +} + class CustomFactoryTestState extends State { public string $name; diff --git a/tests/Unit/UseStatesDirectlyInEventsTest.php b/tests/Unit/UseStatesDirectlyInEventsTest.php index dc127e43..98c848bf 100644 --- a/tests/Unit/UseStatesDirectlyInEventsTest.php +++ b/tests/Unit/UseStatesDirectlyInEventsTest.php @@ -1,6 +1,7 @@ user_request->acknowledged = true; + } +} + class UserRequestsProcessed extends Event { public function __construct(