From 79bc4318ed3f4c14c6e5b137930ced793d659e57 Mon Sep 17 00:00:00 2001 From: Nick Potts Date: Wed, 27 Nov 2024 09:30:58 +0800 Subject: [PATCH 01/15] WIP: Latest attribute to only run handle writes once during replay --- src/Attributes/Hooks/Latest.php | 24 ++++++ src/Lifecycle/Broker.php | 1 + src/Lifecycle/DeferredWriteQueue.php | 28 +++++++ src/Lifecycle/Hook.php | 11 ++- src/Support/DeferredWriteData.php | 13 ++++ src/VerbsServiceProvider.php | 2 + tests/Feature/LatestHandleTest.php | 109 +++++++++++++++++++++++++++ 7 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 src/Attributes/Hooks/Latest.php create mode 100644 src/Lifecycle/DeferredWriteQueue.php create mode 100644 src/Support/DeferredWriteData.php create mode 100644 tests/Feature/LatestHandleTest.php diff --git a/src/Attributes/Hooks/Latest.php b/src/Attributes/Hooks/Latest.php new file mode 100644 index 00000000..3903170e --- /dev/null +++ b/src/Attributes/Hooks/Latest.php @@ -0,0 +1,24 @@ +deferred = new DeferredWriteData($this->type, $this->unique_id); + } +} diff --git a/src/Lifecycle/Broker.php b/src/Lifecycle/Broker.php index 0fdaaa28..ad2f0216 100644 --- a/src/Lifecycle/Broker.php +++ b/src/Lifecycle/Broker.php @@ -105,6 +105,7 @@ public function replay(?callable $beforeEach = null, ?callable $afterEach = null } }); } finally { + app(DeferredWriteQueue::class)->flush(); $this->states->writeSnapshots(); $this->states->prune(); $this->states->setReplaying(false); diff --git a/src/Lifecycle/DeferredWriteQueue.php b/src/Lifecycle/DeferredWriteQueue.php new file mode 100644 index 00000000..792bfc3f --- /dev/null +++ b/src/Lifecycle/DeferredWriteQueue.php @@ -0,0 +1,28 @@ +class_name ?? get_class($event); + $uniqueBy = $deferred->unique_by; + $uniqueByKey = (string)$event->$uniqueBy ?? 'Default'; + + $this->callbacks[$class][$uniqueByKey] = $callback; + } + + public function flush(): void + { + foreach ($this->callbacks as $callbacks) { + foreach ($callbacks as $callback) { + $callback(); + } + } + } +} diff --git a/src/Lifecycle/Hook.php b/src/Lifecycle/Hook.php index 6d79c4f8..2d40a647 100644 --- a/src/Lifecycle/Hook.php +++ b/src/Lifecycle/Hook.php @@ -8,6 +8,9 @@ use RuntimeException; use SplObjectStorage; use Thunk\Verbs\Event; +use Thunk\Verbs\Metadata; +use Thunk\Verbs\State; +use Thunk\Verbs\Support\DeferredWriteData; use Thunk\Verbs\Support\DependencyResolver; use Thunk\Verbs\Support\Reflector; use Thunk\Verbs\Support\Wormhole; @@ -47,6 +50,7 @@ public function __construct( public array $states = [], public SplObjectStorage $phases = new SplObjectStorage, public ?string $name = null, + public ?DeferredWriteData $deferred = null, ) {} public function forcePhases(Phase ...$phases): static @@ -107,7 +111,12 @@ public function handle(Container $container, Event $event): mixed public function replay(Container $container, Event $event): void { if ($this->runsInPhase(Phase::Replay)) { - app(Wormhole::class)->warp($event, fn () => $this->execute($container, $event)); + $callable = fn () => $this->execute($container, $event); + if ($this->deferred) { + app(DeferredWriteQueue::class)->add($event, $callable, $this->deferred); + } else { + app(Wormhole::class)->warp($event, $callable); + } } } diff --git a/src/Support/DeferredWriteData.php b/src/Support/DeferredWriteData.php new file mode 100644 index 00000000..5771653c --- /dev/null +++ b/src/Support/DeferredWriteData.php @@ -0,0 +1,13 @@ +app->scoped(EventQueue::class); $this->app->scoped(EventStateRegistry::class); $this->app->singleton(MetadataManager::class); + $this->app->singleton(DeferredWriteQueue::class); $this->app->scoped(StateManager::class, function (Container $app) { return new StateManager( diff --git a/tests/Feature/LatestHandleTest.php b/tests/Feature/LatestHandleTest.php new file mode 100644 index 00000000..74d206e6 --- /dev/null +++ b/tests/Feature/LatestHandleTest.php @@ -0,0 +1,109 @@ +toBe(4); + + $GLOBALS['handle_count'] = 0; + + config(['app.env' => 'testing']); + $this->artisan(ReplayCommand::class); + + expect($GLOBALS['handle_count'])->toBe(2); +}); + +it('prevents duplicate writes by uniqueBy and type', function () { + $state1_id = Id::make(); + $state2_id = Id::make(); + + // State 1 + LatestHandleTestEvent::fire(add: 2, state_id: $state1_id); // 2 + AnotherLatestHandleTestEvent::fire(add: 2, state_id: $state1_id); // 4 + AnotherLatestHandleTestEvent::fire(add: 2, state_id: $state1_id); // 6 + + // State 2 + LatestHandleTestEvent::fire(add: 5, state_id: $state2_id); // 5 + AnotherLatestHandleTestEvent::fire(add: 2, state_id: $state2_id); // 7 + + Verbs::commit(); + + expect($GLOBALS['handle_count'])->toBe(5); + + $GLOBALS['handle_count'] = 0; + + config(['app.env' => 'testing']); + $this->artisan(ReplayCommand::class); + + expect($GLOBALS['handle_count'])->toBe(2); +}); + +class LatestHandleTestEvent extends Event +{ + public function __construct( + public int $add = 0, + #[StateId(LatestHandleTestState::class)] public ?int $state_id = null, + ) { + } + + public function apply(LatestHandleTestState $state) + { + $state->count += $this->add; + } + + #[Latest(unique_id: 'state_id')] + public function handle(): void + { + $GLOBALS['handle_count']++; + } +} + +class AnotherLatestHandleTestEvent extends Event +{ + public function __construct( + public int $add = 0, + #[StateId(LatestHandleTestState::class)] public ?int $state_id = null, + ) { + } + + public function apply(LatestHandleTestState $state) + { + $state->count += $this->add; + } + + #[Latest(unique_id: 'state_id', type: LatestHandleTestEvent::class)] + public function handle(): void + { + $GLOBALS['handle_count']++; + } +} + +class LatestHandleTestState extends State +{ + public int $count = 0; +} From 9dc021d37c9aad50b6cc48020e1fb6ff88d8fa9a Mon Sep 17 00:00:00 2001 From: Nick Potts Date: Wed, 27 Nov 2024 09:39:34 +0800 Subject: [PATCH 02/15] use wormhole --- src/Lifecycle/DeferredWriteQueue.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Lifecycle/DeferredWriteQueue.php b/src/Lifecycle/DeferredWriteQueue.php index 792bfc3f..b089e2e4 100644 --- a/src/Lifecycle/DeferredWriteQueue.php +++ b/src/Lifecycle/DeferredWriteQueue.php @@ -4,6 +4,7 @@ use Thunk\Verbs\Event; use Thunk\Verbs\Support\DeferredWriteData; +use Thunk\Verbs\Support\Wormhole; class DeferredWriteQueue { @@ -14,14 +15,14 @@ public function add(Event $event, callable $callback, DeferredWriteData $deferre $uniqueBy = $deferred->unique_by; $uniqueByKey = (string)$event->$uniqueBy ?? 'Default'; - $this->callbacks[$class][$uniqueByKey] = $callback; + $this->callbacks[$class][$uniqueByKey] = [$event, $callback]; } public function flush(): void { foreach ($this->callbacks as $callbacks) { foreach ($callbacks as $callback) { - $callback(); + app(Wormhole::class)->warp($callback[0], $callback[1]); } } } From 6929360f4d52abf52a8c766a3b83137bbff5948d Mon Sep 17 00:00:00 2001 From: nick-potts Date: Wed, 27 Nov 2024 01:40:00 +0000 Subject: [PATCH 03/15] Fix styling --- src/Attributes/Hooks/Latest.php | 9 +++------ src/Lifecycle/DeferredWriteQueue.php | 3 ++- src/Lifecycle/Hook.php | 2 -- src/Support/DeferredWriteData.php | 10 ++++------ tests/Feature/LatestHandleTest.php | 7 ++----- 5 files changed, 11 insertions(+), 20 deletions(-) diff --git a/src/Attributes/Hooks/Latest.php b/src/Attributes/Hooks/Latest.php index 3903170e..9df419f3 100644 --- a/src/Attributes/Hooks/Latest.php +++ b/src/Attributes/Hooks/Latest.php @@ -4,18 +4,15 @@ use Attribute; use Thunk\Verbs\Lifecycle\Hook; -use Thunk\Verbs\Lifecycle\Phase; use Thunk\Verbs\Support\DeferredWriteData; #[Attribute(Attribute::TARGET_METHOD)] class Latest implements HookAttribute { public function __construct( - public string|null $unique_id = null, - public ?string $type = null, - ) - { - } + public ?string $unique_id = null, + public ?string $type = null, + ) {} public function applyToHook(Hook $hook): void { diff --git a/src/Lifecycle/DeferredWriteQueue.php b/src/Lifecycle/DeferredWriteQueue.php index b089e2e4..f6fc199a 100644 --- a/src/Lifecycle/DeferredWriteQueue.php +++ b/src/Lifecycle/DeferredWriteQueue.php @@ -9,11 +9,12 @@ class DeferredWriteQueue { private array $callbacks = []; + public function add(Event $event, callable $callback, DeferredWriteData $deferred): void { $class = $deferred->class_name ?? get_class($event); $uniqueBy = $deferred->unique_by; - $uniqueByKey = (string)$event->$uniqueBy ?? 'Default'; + $uniqueByKey = (string) $event->$uniqueBy ?? 'Default'; $this->callbacks[$class][$uniqueByKey] = [$event, $callback]; } diff --git a/src/Lifecycle/Hook.php b/src/Lifecycle/Hook.php index 2d40a647..5904db4e 100644 --- a/src/Lifecycle/Hook.php +++ b/src/Lifecycle/Hook.php @@ -8,8 +8,6 @@ use RuntimeException; use SplObjectStorage; use Thunk\Verbs\Event; -use Thunk\Verbs\Metadata; -use Thunk\Verbs\State; use Thunk\Verbs\Support\DeferredWriteData; use Thunk\Verbs\Support\DependencyResolver; use Thunk\Verbs\Support\Reflector; diff --git a/src/Support/DeferredWriteData.php b/src/Support/DeferredWriteData.php index 5771653c..2d48fd46 100644 --- a/src/Support/DeferredWriteData.php +++ b/src/Support/DeferredWriteData.php @@ -4,10 +4,8 @@ class DeferredWriteData { -public function __construct( - public ?string $class_name, - public string|null $unique_by, -) -{ -} + public function __construct( + public ?string $class_name, + public ?string $unique_by, + ) {} } diff --git a/tests/Feature/LatestHandleTest.php b/tests/Feature/LatestHandleTest.php index 74d206e6..e6a4239b 100644 --- a/tests/Feature/LatestHandleTest.php +++ b/tests/Feature/LatestHandleTest.php @@ -6,7 +6,6 @@ use Thunk\Verbs\Event; use Thunk\Verbs\Facades\Id; use Thunk\Verbs\Facades\Verbs; -use Thunk\Verbs\Lifecycle\StateManager; use Thunk\Verbs\State; beforeEach(function () { @@ -68,8 +67,7 @@ class LatestHandleTestEvent extends Event public function __construct( public int $add = 0, #[StateId(LatestHandleTestState::class)] public ?int $state_id = null, - ) { - } + ) {} public function apply(LatestHandleTestState $state) { @@ -88,8 +86,7 @@ class AnotherLatestHandleTestEvent extends Event public function __construct( public int $add = 0, #[StateId(LatestHandleTestState::class)] public ?int $state_id = null, - ) { - } + ) {} public function apply(LatestHandleTestState $state) { From 1fb01e1267f868a7cb5af16c2105eed4a2704b4f Mon Sep 17 00:00:00 2001 From: Nick Potts Date: Wed, 27 Nov 2024 09:41:25 +0800 Subject: [PATCH 04/15] Clear queue --- src/Lifecycle/DeferredWriteQueue.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Lifecycle/DeferredWriteQueue.php b/src/Lifecycle/DeferredWriteQueue.php index f6fc199a..5e6b263a 100644 --- a/src/Lifecycle/DeferredWriteQueue.php +++ b/src/Lifecycle/DeferredWriteQueue.php @@ -26,5 +26,6 @@ public function flush(): void app(Wormhole::class)->warp($callback[0], $callback[1]); } } + $this->callbacks = []; } } From c9916e259f99a1b99ec3d53f6e92a985df000b5a Mon Sep 17 00:00:00 2001 From: Nick Potts Date: Tue, 3 Dec 2024 10:25:18 +0800 Subject: [PATCH 05/15] Rename to UniqueBy, add Verbs::whenUnique() --- .../Autodiscovery/StateDiscoveryAttribute.php | 5 + .../Hooks/{Latest.php => UniqueBy.php} | 12 +- src/Facades/Verbs.php | 2 +- src/Lifecycle/Broker.php | 1 + src/Lifecycle/BrokerConvenienceMethods.php | 7 ++ src/Lifecycle/DeferredWriteQueue.php | 54 ++++++-- src/Lifecycle/Hook.php | 15 ++- src/Support/DeferredWriteData.php | 11 -- src/Support/EventStateRegistry.php | 12 ++ tests/Feature/LatestHandleTest.php | 119 ++++++++++++++---- 10 files changed, 183 insertions(+), 55 deletions(-) rename src/Attributes/Hooks/{Latest.php => UniqueBy.php} (50%) delete mode 100644 src/Support/DeferredWriteData.php diff --git a/src/Attributes/Autodiscovery/StateDiscoveryAttribute.php b/src/Attributes/Autodiscovery/StateDiscoveryAttribute.php index 385d97ba..5334395a 100644 --- a/src/Attributes/Autodiscovery/StateDiscoveryAttribute.php +++ b/src/Attributes/Autodiscovery/StateDiscoveryAttribute.php @@ -55,4 +55,9 @@ protected function inferAliasFromVariableName(string $name) ? Str::beforeLast($name, '_id') : Str::beforeLast($name, 'Id'); } + + public function propertyName(): string + { + return $this->property->getName(); + } } diff --git a/src/Attributes/Hooks/Latest.php b/src/Attributes/Hooks/UniqueBy.php similarity index 50% rename from src/Attributes/Hooks/Latest.php rename to src/Attributes/Hooks/UniqueBy.php index 9df419f3..6ab1e874 100644 --- a/src/Attributes/Hooks/Latest.php +++ b/src/Attributes/Hooks/UniqueBy.php @@ -4,18 +4,20 @@ use Attribute; use Thunk\Verbs\Lifecycle\Hook; -use Thunk\Verbs\Support\DeferredWriteData; #[Attribute(Attribute::TARGET_METHOD)] -class Latest implements HookAttribute +class UniqueBy implements HookAttribute { + /** + * @param string|string[] $property + */ public function __construct( - public ?string $unique_id = null, - public ?string $type = null, + public string|array|null $property, + public ?string $name = null, ) {} public function applyToHook(Hook $hook): void { - $hook->deferred = new DeferredWriteData($this->type, $this->unique_id); + $hook->deferred_attribute = $this; } } diff --git a/src/Facades/Verbs.php b/src/Facades/Verbs.php index dce514c4..35a0d1d1 100644 --- a/src/Facades/Verbs.php +++ b/src/Facades/Verbs.php @@ -15,9 +15,9 @@ * @method static bool commit() * @method static bool isReplaying() * @method static void unlessReplaying(callable $callback) + * @method static void whenUnique(null|int|iterable $state, callable $callback, string $name = 'Default') * @method static Event fire(Event $event) * @method static void createMetadataUsing(callable $callback) - * @method static void commitImmediately(bool $commit_immediately = true) * @method static EventStoreFake assertCommitted(string|Closure $event, Closure|int|null $callback = null) * @method static EventStoreFake assertNotCommitted(string|Closure $event, ?Closure $callback = null) * @method static EventStoreFake assertNothingCommitted() diff --git a/src/Lifecycle/Broker.php b/src/Lifecycle/Broker.php index ad2f0216..afa5848c 100644 --- a/src/Lifecycle/Broker.php +++ b/src/Lifecycle/Broker.php @@ -71,6 +71,7 @@ public function commit(): bool foreach ($events as $event) { $this->metadata->setLastResults($event, $this->dispatcher->handle($event)); } + app(DeferredWriteQueue::class)->flush(); return $this->commit(); } diff --git a/src/Lifecycle/BrokerConvenienceMethods.php b/src/Lifecycle/BrokerConvenienceMethods.php index aab881b6..c5172e4e 100644 --- a/src/Lifecycle/BrokerConvenienceMethods.php +++ b/src/Lifecycle/BrokerConvenienceMethods.php @@ -11,6 +11,7 @@ use Thunk\Verbs\Exceptions\EventNotAuthorized; use Thunk\Verbs\Exceptions\EventNotValid; use Thunk\Verbs\Facades\Id; +use Thunk\Verbs\State; use Thunk\Verbs\Support\IdManager; use Thunk\Verbs\Support\Wormhole; @@ -74,6 +75,12 @@ public function unlessReplaying(callable $callback) } } + public function whenUnique(State|iterable|null $state, callable $callback, string $name = 'default'): void + { + $states = is_iterable($state) ? $state : [$state]; + app(DeferredWriteQueue::class)->addCallback($states, $callback, $name); + } + public function realNow(): CarbonInterface { return app(Wormhole::class)->realNow(); diff --git a/src/Lifecycle/DeferredWriteQueue.php b/src/Lifecycle/DeferredWriteQueue.php index 5e6b263a..ca68ca48 100644 --- a/src/Lifecycle/DeferredWriteQueue.php +++ b/src/Lifecycle/DeferredWriteQueue.php @@ -2,28 +2,68 @@ namespace Thunk\Verbs\Lifecycle; +use Thunk\Verbs\Attributes\Hooks\UniqueBy; use Thunk\Verbs\Event; -use Thunk\Verbs\Support\DeferredWriteData; +use Thunk\Verbs\State; +use Thunk\Verbs\Support\EventStateRegistry; +use Thunk\Verbs\Support\StateCollection; use Thunk\Verbs\Support\Wormhole; class DeferredWriteQueue { private array $callbacks = []; - public function add(Event $event, callable $callback, DeferredWriteData $deferred): void + public function addHook(Event $event, UniqueBy $deferred, callable $callback): void { - $class = $deferred->class_name ?? get_class($event); - $uniqueBy = $deferred->unique_by; - $uniqueByKey = (string) $event->$uniqueBy ?? 'Default'; + /** @var string[] $propertyNames */ + $propertyNames = is_array($deferred->property) ? $deferred->property : [$deferred->property]; - $this->callbacks[$class][$uniqueByKey] = [$event, $callback]; + $uniqueByKey = ''; + $states = new StateCollection; + foreach ($propertyNames as $property) { + if ($property === null) { + $uniqueByKey .= 'null'; + + continue; + } + + $states = $states->merge(app(EventStateRegistry::class)->statesForProperty($event, $property)); + } + + $uniqueByKey .= $states->map(fn (State $state) => $state->id)->implode('|'); + + $name = $deferred->name ?? 'DeferredWriteQueue'; + + $this->callbacks[$name][$uniqueByKey] = [$event, $callback, true]; + } + + /** + * @param iterable $states + */ + public function addCallback(iterable $states, callable $callback, string $name): void + { + $id = ''; + foreach ($states as $state) { + if ($state === null) { + $id .= 'null'; + + continue; + } + $id .= $state->id; + } + + $this->callbacks[$name][$id] = [null, $callback, false]; } public function flush(): void { foreach ($this->callbacks as $callbacks) { foreach ($callbacks as $callback) { - app(Wormhole::class)->warp($callback[0], $callback[1]); + if ($callback[2]) { + app(Wormhole::class)->warp($callback[0], $callback[1]); + } else { + $callback[1](); + } } } $this->callbacks = []; diff --git a/src/Lifecycle/Hook.php b/src/Lifecycle/Hook.php index 5904db4e..b6bf72f1 100644 --- a/src/Lifecycle/Hook.php +++ b/src/Lifecycle/Hook.php @@ -7,8 +7,8 @@ use ReflectionMethod; use RuntimeException; use SplObjectStorage; +use Thunk\Verbs\Attributes\Hooks\UniqueBy; use Thunk\Verbs\Event; -use Thunk\Verbs\Support\DeferredWriteData; use Thunk\Verbs\Support\DependencyResolver; use Thunk\Verbs\Support\Reflector; use Thunk\Verbs\Support\Wormhole; @@ -48,7 +48,7 @@ public function __construct( public array $states = [], public SplObjectStorage $phases = new SplObjectStorage, public ?string $name = null, - public ?DeferredWriteData $deferred = null, + public ?UniqueBy $deferred_attribute = null, ) {} public function forcePhases(Phase ...$phases): static @@ -100,7 +100,12 @@ public function fired(Container $container, Event $event): void public function handle(Container $container, Event $event): mixed { if ($this->runsInPhase(Phase::Handle)) { - return $this->execute($container, $event); + $callable = fn () => $this->execute($container, $event); + if ($this->deferred_attribute) { + app(DeferredWriteQueue::class)->addHook($event, $this->deferred_attribute, $callable); + } else { + $this->execute($container, $event); + } } return null; @@ -110,8 +115,8 @@ public function replay(Container $container, Event $event): void { if ($this->runsInPhase(Phase::Replay)) { $callable = fn () => $this->execute($container, $event); - if ($this->deferred) { - app(DeferredWriteQueue::class)->add($event, $callable, $this->deferred); + if ($this->deferred_attribute) { + app(DeferredWriteQueue::class)->addHook($event, $this->deferred_attribute, $callable); } else { app(Wormhole::class)->warp($event, $callable); } diff --git a/src/Support/DeferredWriteData.php b/src/Support/DeferredWriteData.php deleted file mode 100644 index 2d48fd46..00000000 --- a/src/Support/DeferredWriteData.php +++ /dev/null @@ -1,11 +0,0 @@ -getAttributes($event); + $property = $attributes + ->firstOrFail(fn (StateDiscoveryAttribute $attribute) => $attribute->propertyName() === $property); + + $states = new StateCollection; + $this->discoverAndPushState($property, $event, $states); + + return $states; + } + /** @return Collection */ protected function discoverAndPushState(StateDiscoveryAttribute $attribute, Event $target, StateCollection $discovered): Collection { diff --git a/tests/Feature/LatestHandleTest.php b/tests/Feature/LatestHandleTest.php index e6a4239b..546cdf41 100644 --- a/tests/Feature/LatestHandleTest.php +++ b/tests/Feature/LatestHandleTest.php @@ -1,7 +1,7 @@ toBe(4); + expect($GLOBALS['handle_count'])->toBe(2); $GLOBALS['handle_count'] = 0; @@ -37,22 +37,22 @@ expect($GLOBALS['handle_count'])->toBe(2); }); -it('prevents duplicate writes by uniqueBy and type', function () { +it('prevents duplicate writes automatically using the StateId attribute', function () { $state1_id = Id::make(); $state2_id = Id::make(); // State 1 - LatestHandleTestEvent::fire(add: 2, state_id: $state1_id); // 2 - AnotherLatestHandleTestEvent::fire(add: 2, state_id: $state1_id); // 4 - AnotherLatestHandleTestEvent::fire(add: 2, state_id: $state1_id); // 6 + LatestHandleTestEvent::fire(state_id: $state1_id); + AnotherLatestHandleTestEvent::fire(state_id: $state1_id); + AnotherLatestHandleTestEvent::fire(state_id: $state1_id); // State 2 - LatestHandleTestEvent::fire(add: 5, state_id: $state2_id); // 5 - AnotherLatestHandleTestEvent::fire(add: 2, state_id: $state2_id); // 7 + LatestHandleTestEvent::fire(state_id: $state2_id); + AnotherLatestHandleTestEvent::fire(state_id: $state2_id); Verbs::commit(); - expect($GLOBALS['handle_count'])->toBe(5); + expect($GLOBALS['handle_count'])->toBe(2); $GLOBALS['handle_count'] = 0; @@ -62,19 +62,71 @@ expect($GLOBALS['handle_count'])->toBe(2); }); +it('prevents duplicate writes automatically using a specific name', function () { + $state1_id = Id::make(); + $state2_id = Id::make(); + + NamedHandleTestEvent::fire(state_id: $state2_id); // 5 + AnotherNamedHandleTestEvent::fire(state_id: $state2_id); // 7 + + Verbs::commit(); + + expect($GLOBALS['handle_count'])->toBe(1); + + $GLOBALS['handle_count'] = 0; + + config(['app.env' => 'testing']); + $this->artisan(ReplayCommand::class); + + expect($GLOBALS['handle_count'])->toBe(1); +}); + +it('only runs callbacks once', function () { + NamedHandleTestEvent::fire(); + + Verbs::whenUnique(null, function () { + $GLOBALS['handle_count']++; + }); + + Verbs::whenUnique(null, function () { + $GLOBALS['handle_count']++; + }); + + Verbs::whenUnique(null, function () { + $GLOBALS['handle_count']++; + }, 'another'); + + Verbs::whenUnique(null, function () { + $GLOBALS['handle_count']++; + }, 'another'); + + $state = LatestHandleTestState::load(snowflake_id()); + $state2 = LatestHandleTestState::load(snowflake_id()); + + Verbs::whenUnique($state, function () { + $GLOBALS['handle_count']++; + }, 'another'); + + Verbs::whenUnique($state, function () { + $GLOBALS['handle_count']++; + }, 'another'); + + Verbs::whenUnique([$state, $state2], function () { + $GLOBALS['handle_count']++; + }, 'another'); + + Verbs::commit(); + + expect($GLOBALS['handle_count'])->toBe(5); +}); + class LatestHandleTestEvent extends Event { public function __construct( - public int $add = 0, #[StateId(LatestHandleTestState::class)] public ?int $state_id = null, ) {} - public function apply(LatestHandleTestState $state) - { - $state->count += $this->add; - } - - #[Latest(unique_id: 'state_id')] + #[UniqueBy('state_id')] public function handle(): void { $GLOBALS['handle_count']++; @@ -84,23 +136,38 @@ public function handle(): void class AnotherLatestHandleTestEvent extends Event { public function __construct( - public int $add = 0, #[StateId(LatestHandleTestState::class)] public ?int $state_id = null, ) {} - public function apply(LatestHandleTestState $state) + #[UniqueBy('state_id')] + public function handle(): void { - $state->count += $this->add; + $GLOBALS['handle_count']++; } +} - #[Latest(unique_id: 'state_id', type: LatestHandleTestEvent::class)] +class NamedHandleTestEvent extends Event +{ + public function __construct( + ) {} + + #[UniqueBy(null, name: 'named')] public function handle(): void { $GLOBALS['handle_count']++; } } -class LatestHandleTestState extends State +class AnotherNamedHandleTestEvent extends Event { - public int $count = 0; + public function __construct( + ) {} + + #[UniqueBy(null, name: 'named')] + public function handle(): void + { + $GLOBALS['handle_count']++; + } } + +class LatestHandleTestState extends State {} From 07466c0c50f17929ff18028f1e816f7e11d7e14f Mon Sep 17 00:00:00 2001 From: Nick Potts Date: Tue, 3 Dec 2024 10:27:15 +0800 Subject: [PATCH 06/15] rename tests --- tests/Feature/{LatestHandleTest.php => UniqueWritesTest.php} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/Feature/{LatestHandleTest.php => UniqueWritesTest.php} (100%) diff --git a/tests/Feature/LatestHandleTest.php b/tests/Feature/UniqueWritesTest.php similarity index 100% rename from tests/Feature/LatestHandleTest.php rename to tests/Feature/UniqueWritesTest.php From 88150799d21ae9f8e59860e1ed802afee9e5269e Mon Sep 17 00:00:00 2001 From: Nick Potts Date: Tue, 3 Dec 2024 10:34:58 +0800 Subject: [PATCH 07/15] add replay only option, so you can still commit and return data --- src/Attributes/Hooks/UniqueBy.php | 1 + src/Lifecycle/Hook.php | 4 ++-- tests/Feature/UniqueWritesTest.php | 27 +++++++++++++++++++++++---- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/Attributes/Hooks/UniqueBy.php b/src/Attributes/Hooks/UniqueBy.php index 6ab1e874..1529860d 100644 --- a/src/Attributes/Hooks/UniqueBy.php +++ b/src/Attributes/Hooks/UniqueBy.php @@ -14,6 +14,7 @@ class UniqueBy implements HookAttribute public function __construct( public string|array|null $property, public ?string $name = null, + public bool $replay_only = false, ) {} public function applyToHook(Hook $hook): void diff --git a/src/Lifecycle/Hook.php b/src/Lifecycle/Hook.php index b6bf72f1..63bfbbfd 100644 --- a/src/Lifecycle/Hook.php +++ b/src/Lifecycle/Hook.php @@ -101,10 +101,10 @@ public function handle(Container $container, Event $event): mixed { if ($this->runsInPhase(Phase::Handle)) { $callable = fn () => $this->execute($container, $event); - if ($this->deferred_attribute) { + if ($this->deferred_attribute && !$this->deferred_attribute->replay_only) { app(DeferredWriteQueue::class)->addHook($event, $this->deferred_attribute, $callable); } else { - $this->execute($container, $event); + return $this->execute($container, $event); } } diff --git a/tests/Feature/UniqueWritesTest.php b/tests/Feature/UniqueWritesTest.php index 546cdf41..f7d98f73 100644 --- a/tests/Feature/UniqueWritesTest.php +++ b/tests/Feature/UniqueWritesTest.php @@ -9,7 +9,6 @@ use Thunk\Verbs\State; beforeEach(function () { - $GLOBALS['replay_test_counts'] = []; $GLOBALS['handle_count'] = 0; }); @@ -31,7 +30,6 @@ $GLOBALS['handle_count'] = 0; - config(['app.env' => 'testing']); $this->artisan(ReplayCommand::class); expect($GLOBALS['handle_count'])->toBe(2); @@ -56,7 +54,6 @@ $GLOBALS['handle_count'] = 0; - config(['app.env' => 'testing']); $this->artisan(ReplayCommand::class); expect($GLOBALS['handle_count'])->toBe(2); @@ -75,7 +72,19 @@ $GLOBALS['handle_count'] = 0; - config(['app.env' => 'testing']); + $this->artisan(ReplayCommand::class); + + expect($GLOBALS['handle_count'])->toBe(1); +}); + +it('can receive handle data when replay_only is set', function () { + $this->assertTrue(CommitOnlyTestEvent::commit()); + $this->assertTrue(CommitOnlyTestEvent::commit()); + + expect($GLOBALS['handle_count'])->toBe(2); + + $GLOBALS['handle_count'] = 0; + $this->artisan(ReplayCommand::class); expect($GLOBALS['handle_count'])->toBe(1); @@ -170,4 +179,14 @@ public function handle(): void } } +class CommitOnlyTestEvent extends Event +{ + #[UniqueBy(null, replay_only: true)] + public function handle(): bool + { + $GLOBALS['handle_count']++; + return true; + } +} + class LatestHandleTestState extends State {} From 231ef3a14f47d5bbb8933f60e62df93234281fed Mon Sep 17 00:00:00 2001 From: nick-potts Date: Tue, 3 Dec 2024 02:35:25 +0000 Subject: [PATCH 08/15] Fix styling --- src/Lifecycle/Hook.php | 2 +- tests/Feature/UniqueWritesTest.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Lifecycle/Hook.php b/src/Lifecycle/Hook.php index 63bfbbfd..26a52c4e 100644 --- a/src/Lifecycle/Hook.php +++ b/src/Lifecycle/Hook.php @@ -101,7 +101,7 @@ public function handle(Container $container, Event $event): mixed { if ($this->runsInPhase(Phase::Handle)) { $callable = fn () => $this->execute($container, $event); - if ($this->deferred_attribute && !$this->deferred_attribute->replay_only) { + if ($this->deferred_attribute && ! $this->deferred_attribute->replay_only) { app(DeferredWriteQueue::class)->addHook($event, $this->deferred_attribute, $callable); } else { return $this->execute($container, $event); diff --git a/tests/Feature/UniqueWritesTest.php b/tests/Feature/UniqueWritesTest.php index f7d98f73..939ca4aa 100644 --- a/tests/Feature/UniqueWritesTest.php +++ b/tests/Feature/UniqueWritesTest.php @@ -185,6 +185,7 @@ class CommitOnlyTestEvent extends Event public function handle(): bool { $GLOBALS['handle_count']++; + return true; } } From 513b0cc7ea11f1ca2c81daa8fc6e79417c3b7bbe Mon Sep 17 00:00:00 2001 From: Nick Potts Date: Tue, 3 Dec 2024 10:57:39 +0800 Subject: [PATCH 09/15] fix AppliesToState --- .../Autodiscovery/AppliesToChildState.php | 5 ++++ .../Autodiscovery/AppliesToSingletonState.php | 5 ++++ .../Autodiscovery/AppliesToState.php | 6 +++++ .../Autodiscovery/StateDiscoveryAttribute.php | 5 +--- src/Attributes/Autodiscovery/StateId.php | 5 ++++ src/Lifecycle/DeferredWriteQueue.php | 2 +- tests/Feature/UniqueWritesTest.php | 24 ++++++++----------- 7 files changed, 33 insertions(+), 19 deletions(-) diff --git a/src/Attributes/Autodiscovery/AppliesToChildState.php b/src/Attributes/Autodiscovery/AppliesToChildState.php index 7333ec07..8a0a15c0 100644 --- a/src/Attributes/Autodiscovery/AppliesToChildState.php +++ b/src/Attributes/Autodiscovery/AppliesToChildState.php @@ -37,4 +37,9 @@ public function discoverState(Event $event, StateManager $manager): State return $manager->load($parent->{$this->id}, $this->state_type); } + + public function propertyName(): ?string + { + return $this->id; + } } diff --git a/src/Attributes/Autodiscovery/AppliesToSingletonState.php b/src/Attributes/Autodiscovery/AppliesToSingletonState.php index 3d931074..cfa43154 100644 --- a/src/Attributes/Autodiscovery/AppliesToSingletonState.php +++ b/src/Attributes/Autodiscovery/AppliesToSingletonState.php @@ -24,4 +24,9 @@ public function discoverState(Event $event, StateManager $manager): State { return $manager->singleton($this->state_type); } + + public function propertyName(): ?string + { + return null; + } } diff --git a/src/Attributes/Autodiscovery/AppliesToState.php b/src/Attributes/Autodiscovery/AppliesToState.php index 32dd227e..052b52c0 100644 --- a/src/Attributes/Autodiscovery/AppliesToState.php +++ b/src/Attributes/Autodiscovery/AppliesToState.php @@ -66,4 +66,10 @@ protected function getStateIdProperty(Event $event): string throw new InvalidArgumentException("No ID property provided AppliesToState for {$this->state_type}"); } + + + public function propertyName(): string + { + return $this->id; + } } diff --git a/src/Attributes/Autodiscovery/StateDiscoveryAttribute.php b/src/Attributes/Autodiscovery/StateDiscoveryAttribute.php index 5334395a..b9a017f8 100644 --- a/src/Attributes/Autodiscovery/StateDiscoveryAttribute.php +++ b/src/Attributes/Autodiscovery/StateDiscoveryAttribute.php @@ -56,8 +56,5 @@ protected function inferAliasFromVariableName(string $name) : Str::beforeLast($name, 'Id'); } - public function propertyName(): string - { - return $this->property->getName(); - } + abstract public function propertyName(): ?string; } diff --git a/src/Attributes/Autodiscovery/StateId.php b/src/Attributes/Autodiscovery/StateId.php index 6366241d..2296d185 100644 --- a/src/Attributes/Autodiscovery/StateId.php +++ b/src/Attributes/Autodiscovery/StateId.php @@ -53,4 +53,9 @@ public function discoverState(Event $event, StateManager $manager): State|array return array_map(fn ($id) => $manager->load($id, $this->state_type), Arr::wrap($id)); } + + public function propertyName(): string + { + return $this->property->getName(); + } } diff --git a/src/Lifecycle/DeferredWriteQueue.php b/src/Lifecycle/DeferredWriteQueue.php index ca68ca48..196262fe 100644 --- a/src/Lifecycle/DeferredWriteQueue.php +++ b/src/Lifecycle/DeferredWriteQueue.php @@ -32,7 +32,7 @@ public function addHook(Event $event, UniqueBy $deferred, callable $callback): v $uniqueByKey .= $states->map(fn (State $state) => $state->id)->implode('|'); - $name = $deferred->name ?? 'DeferredWriteQueue'; + $name = $deferred->name ?? get_class($event); $this->callbacks[$name][$uniqueByKey] = [$event, $callback, true]; } diff --git a/tests/Feature/UniqueWritesTest.php b/tests/Feature/UniqueWritesTest.php index f7d98f73..81dd6738 100644 --- a/tests/Feature/UniqueWritesTest.php +++ b/tests/Feature/UniqueWritesTest.php @@ -1,5 +1,6 @@ toBe(2); + expect($GLOBALS['handle_count'])->toBe(4); $GLOBALS['handle_count'] = 0; $this->artisan(ReplayCommand::class); - expect($GLOBALS['handle_count'])->toBe(2); + expect($GLOBALS['handle_count'])->toBe(4); }); it('prevents duplicate writes automatically using a specific name', function () { @@ -131,9 +135,8 @@ class LatestHandleTestEvent extends Event { - public function __construct( - #[StateId(LatestHandleTestState::class)] public ?int $state_id = null, - ) {} + #[StateId(LatestHandleTestState::class)] + public int $state_id; #[UniqueBy('state_id')] public function handle(): void @@ -142,11 +145,10 @@ public function handle(): void } } +#[AppliesToState(LatestHandleTestState::class, 'state_id')] class AnotherLatestHandleTestEvent extends Event { - public function __construct( - #[StateId(LatestHandleTestState::class)] public ?int $state_id = null, - ) {} + public int $state_id; #[UniqueBy('state_id')] public function handle(): void @@ -157,9 +159,6 @@ public function handle(): void class NamedHandleTestEvent extends Event { - public function __construct( - ) {} - #[UniqueBy(null, name: 'named')] public function handle(): void { @@ -169,9 +168,6 @@ public function handle(): void class AnotherNamedHandleTestEvent extends Event { - public function __construct( - ) {} - #[UniqueBy(null, name: 'named')] public function handle(): void { From 07f78468fe763c693b76fee53e3ff0c93e447f4e Mon Sep 17 00:00:00 2001 From: nick-potts Date: Tue, 3 Dec 2024 03:00:29 +0000 Subject: [PATCH 10/15] Fix styling --- src/Attributes/Autodiscovery/AppliesToState.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Attributes/Autodiscovery/AppliesToState.php b/src/Attributes/Autodiscovery/AppliesToState.php index 052b52c0..3d8d926a 100644 --- a/src/Attributes/Autodiscovery/AppliesToState.php +++ b/src/Attributes/Autodiscovery/AppliesToState.php @@ -67,7 +67,6 @@ protected function getStateIdProperty(Event $event): string throw new InvalidArgumentException("No ID property provided AppliesToState for {$this->state_type}"); } - public function propertyName(): string { return $this->id; From 2af9cdfa9244710a851e1039dc85a1a998d9fa0e Mon Sep 17 00:00:00 2001 From: Nick Potts Date: Tue, 3 Dec 2024 12:59:48 +0800 Subject: [PATCH 11/15] rename to defer --- src/Attributes/Hooks/DeferFor.php | 34 +++++++++++++++++++ src/Attributes/Hooks/UniqueBy.php | 24 ------------- src/Facades/Verbs.php | 21 +++++++++++- src/Lifecycle/BrokerConvenienceMethods.php | 4 +-- src/Lifecycle/DeferredWriteQueue.php | 21 ++++++++---- src/Lifecycle/Hook.php | 12 +++---- ...eWritesTest.php => DeferredWritesTest.php} | 26 +++++++------- 7 files changed, 89 insertions(+), 53 deletions(-) create mode 100644 src/Attributes/Hooks/DeferFor.php delete mode 100644 src/Attributes/Hooks/UniqueBy.php rename tests/Feature/{UniqueWritesTest.php => DeferredWritesTest.php} (88%) diff --git a/src/Attributes/Hooks/DeferFor.php b/src/Attributes/Hooks/DeferFor.php new file mode 100644 index 00000000..aeb0c8c7 --- /dev/null +++ b/src/Attributes/Hooks/DeferFor.php @@ -0,0 +1,34 @@ +deferred_attribute = $this; + } +} diff --git a/src/Attributes/Hooks/UniqueBy.php b/src/Attributes/Hooks/UniqueBy.php deleted file mode 100644 index 1529860d..00000000 --- a/src/Attributes/Hooks/UniqueBy.php +++ /dev/null @@ -1,24 +0,0 @@ -deferred_attribute = $this; - } -} diff --git a/src/Facades/Verbs.php b/src/Facades/Verbs.php index 35a0d1d1..33858728 100644 --- a/src/Facades/Verbs.php +++ b/src/Facades/Verbs.php @@ -8,16 +8,35 @@ use Thunk\Verbs\Contracts\BrokersEvents; use Thunk\Verbs\Event; use Thunk\Verbs\Lifecycle\Phase; +use Thunk\Verbs\State; use Thunk\Verbs\Testing\BrokerFake; use Thunk\Verbs\Testing\EventStoreFake; /** + * Commits all outstanding events * @method static bool commit() + * + * Determines if verbs is currently replaying events. * @method static bool isReplaying() + * + * Executes the given callback only if not replaying events. * @method static void unlessReplaying(callable $callback) - * @method static void whenUnique(null|int|iterable $state, callable $callback, string $name = 'Default') + * + * Defers the execution of a callback. It will only get called once per unique constraint. + * @method static void defer(State|string|iterable|null $unique_by, callable $callback, string $name = 'Default') + * @param State|string|int|iterable|null $unique_by The uniqueness constraint for the deferred callback. It can be a State, string or array combination of both + * @param callable $callback The callback to be executed + * @param string $name Optional name identifier for the deferred callback, defaults to 'Default'. It's a secondary constraint + * + * Fires an event through the event store. * @method static Event fire(Event $event) + * @param Event $event The event object to be fired + * @return Event The fired event instance + * + * Sets a callback to create metadata for events. * @method static void createMetadataUsing(callable $callback) + * @param callable $callback The callback function that generates metadata + * * @method static EventStoreFake assertCommitted(string|Closure $event, Closure|int|null $callback = null) * @method static EventStoreFake assertNotCommitted(string|Closure $event, ?Closure $callback = null) * @method static EventStoreFake assertNothingCommitted() diff --git a/src/Lifecycle/BrokerConvenienceMethods.php b/src/Lifecycle/BrokerConvenienceMethods.php index c5172e4e..16729860 100644 --- a/src/Lifecycle/BrokerConvenienceMethods.php +++ b/src/Lifecycle/BrokerConvenienceMethods.php @@ -75,9 +75,9 @@ public function unlessReplaying(callable $callback) } } - public function whenUnique(State|iterable|null $state, callable $callback, string $name = 'default'): void + public function defer(State|string|int|iterable|null $unique_by, callable $callback, string $name = 'default'): void { - $states = is_iterable($state) ? $state : [$state]; + $states = is_iterable($unique_by) ? $unique_by : [$unique_by]; app(DeferredWriteQueue::class)->addCallback($states, $callback, $name); } diff --git a/src/Lifecycle/DeferredWriteQueue.php b/src/Lifecycle/DeferredWriteQueue.php index 196262fe..5d9e30ac 100644 --- a/src/Lifecycle/DeferredWriteQueue.php +++ b/src/Lifecycle/DeferredWriteQueue.php @@ -2,7 +2,7 @@ namespace Thunk\Verbs\Lifecycle; -use Thunk\Verbs\Attributes\Hooks\UniqueBy; +use Thunk\Verbs\Attributes\Hooks\DeferFor; use Thunk\Verbs\Event; use Thunk\Verbs\State; use Thunk\Verbs\Support\EventStateRegistry; @@ -13,10 +13,10 @@ class DeferredWriteQueue { private array $callbacks = []; - public function addHook(Event $event, UniqueBy $deferred, callable $callback): void + public function addHook(Event $event, DeferFor $deferred, callable $callback): void { /** @var string[] $propertyNames */ - $propertyNames = is_array($deferred->property) ? $deferred->property : [$deferred->property]; + $propertyNames = is_array($deferred->property_name) ? $deferred->property_name : [$deferred->property_name]; $uniqueByKey = ''; $states = new StateCollection; @@ -32,13 +32,13 @@ public function addHook(Event $event, UniqueBy $deferred, callable $callback): v $uniqueByKey .= $states->map(fn (State $state) => $state->id)->implode('|'); - $name = $deferred->name ?? get_class($event); + $name = $deferred->name === DeferFor::EVENT_CLASS ? get_class($event) : $deferred->name; $this->callbacks[$name][$uniqueByKey] = [$event, $callback, true]; } /** - * @param iterable $states + * @param iterable $states */ public function addCallback(iterable $states, callable $callback, string $name): void { @@ -46,10 +46,17 @@ public function addCallback(iterable $states, callable $callback, string $name): foreach ($states as $state) { if ($state === null) { $id .= 'null'; - continue; } - $id .= $state->id; + if (is_string($state) || is_int($state)) { + $id .= $state; + continue; + } + if ($state instanceof State) { + $id .= $state->id; + continue; + } + throw new \InvalidArgumentException('Invalid state type'); } $this->callbacks[$name][$id] = [null, $callback, false]; diff --git a/src/Lifecycle/Hook.php b/src/Lifecycle/Hook.php index 26a52c4e..93ab2885 100644 --- a/src/Lifecycle/Hook.php +++ b/src/Lifecycle/Hook.php @@ -7,7 +7,7 @@ use ReflectionMethod; use RuntimeException; use SplObjectStorage; -use Thunk\Verbs\Attributes\Hooks\UniqueBy; +use Thunk\Verbs\Attributes\Hooks\DeferFor; use Thunk\Verbs\Event; use Thunk\Verbs\Support\DependencyResolver; use Thunk\Verbs\Support\Reflector; @@ -43,12 +43,12 @@ public static function fromClosure(Closure $callback): static } public function __construct( - public Closure $callback, - public array $events = [], - public array $states = [], + public Closure $callback, + public array $events = [], + public array $states = [], public SplObjectStorage $phases = new SplObjectStorage, - public ?string $name = null, - public ?UniqueBy $deferred_attribute = null, + public ?string $name = null, + public ?DeferFor $deferred_attribute = null, ) {} public function forcePhases(Phase ...$phases): static diff --git a/tests/Feature/UniqueWritesTest.php b/tests/Feature/DeferredWritesTest.php similarity index 88% rename from tests/Feature/UniqueWritesTest.php rename to tests/Feature/DeferredWritesTest.php index 1509dbee..1a0785b6 100644 --- a/tests/Feature/UniqueWritesTest.php +++ b/tests/Feature/DeferredWritesTest.php @@ -2,7 +2,7 @@ use Thunk\Verbs\Attributes\Autodiscovery\AppliesToState; use Thunk\Verbs\Attributes\Autodiscovery\StateId; -use Thunk\Verbs\Attributes\Hooks\UniqueBy; +use Thunk\Verbs\Attributes\Hooks\DeferFor; use Thunk\Verbs\Commands\ReplayCommand; use Thunk\Verbs\Event; use Thunk\Verbs\Facades\Id; @@ -97,34 +97,34 @@ it('only runs callbacks once', function () { NamedHandleTestEvent::fire(); - Verbs::whenUnique(null, function () { + Verbs::defer(null, function () { $GLOBALS['handle_count']++; }); - Verbs::whenUnique(null, function () { + Verbs::defer(null, function () { $GLOBALS['handle_count']++; }); - Verbs::whenUnique(null, function () { + Verbs::defer(null, function () { $GLOBALS['handle_count']++; }, 'another'); - Verbs::whenUnique(null, function () { + Verbs::defer(null, function () { $GLOBALS['handle_count']++; }, 'another'); $state = LatestHandleTestState::load(snowflake_id()); $state2 = LatestHandleTestState::load(snowflake_id()); - Verbs::whenUnique($state, function () { + Verbs::defer($state, function () { $GLOBALS['handle_count']++; }, 'another'); - Verbs::whenUnique($state, function () { + Verbs::defer($state, function () { $GLOBALS['handle_count']++; }, 'another'); - Verbs::whenUnique([$state, $state2], function () { + Verbs::defer([$state, $state2], function () { $GLOBALS['handle_count']++; }, 'another'); @@ -138,7 +138,7 @@ class LatestHandleTestEvent extends Event #[StateId(LatestHandleTestState::class)] public int $state_id; - #[UniqueBy('state_id')] + #[DeferFor('state_id')] public function handle(): void { $GLOBALS['handle_count']++; @@ -150,7 +150,7 @@ class AnotherLatestHandleTestEvent extends Event { public int $state_id; - #[UniqueBy('state_id')] + #[DeferFor('state_id')] public function handle(): void { $GLOBALS['handle_count']++; @@ -159,7 +159,7 @@ public function handle(): void class NamedHandleTestEvent extends Event { - #[UniqueBy(null, name: 'named')] + #[DeferFor(null, name: 'named')] public function handle(): void { $GLOBALS['handle_count']++; @@ -168,7 +168,7 @@ public function handle(): void class AnotherNamedHandleTestEvent extends Event { - #[UniqueBy(null, name: 'named')] + #[DeferFor(null, name: 'named')] public function handle(): void { $GLOBALS['handle_count']++; @@ -177,7 +177,7 @@ public function handle(): void class CommitOnlyTestEvent extends Event { - #[UniqueBy(null, replay_only: true)] + #[DeferFor(null, replay_only: true)] public function handle(): bool { $GLOBALS['handle_count']++; From f2710b01b371de417feef42ed4e64e90efb4aa16 Mon Sep 17 00:00:00 2001 From: nick-potts Date: Tue, 3 Dec 2024 05:00:20 +0000 Subject: [PATCH 12/15] Fix styling --- src/Attributes/Hooks/DeferFor.php | 11 +++++------ src/Facades/Verbs.php | 18 ++++++++++++------ src/Lifecycle/DeferredWriteQueue.php | 3 +++ src/Lifecycle/Hook.php | 10 +++++----- 4 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/Attributes/Hooks/DeferFor.php b/src/Attributes/Hooks/DeferFor.php index aeb0c8c7..750d07dd 100644 --- a/src/Attributes/Hooks/DeferFor.php +++ b/src/Attributes/Hooks/DeferFor.php @@ -3,7 +3,6 @@ namespace Thunk\Verbs\Attributes\Hooks; use Attribute; -use Thunk\Verbs\Facades\Verbs; use Thunk\Verbs\Lifecycle\Hook; #[Attribute(Attribute::TARGET_METHOD)] @@ -17,14 +16,14 @@ class DeferFor implements HookAttribute * to prevent duplicate writes by ensuring that the hook is only * handled once for a given set of state properties. * - * @param string|string[] $property_name The state property name(s) to be unique by - * @param string $name Defaults to the event's class name - * @param bool $replay_only Only defer for replayed events + * @param string|string[] $property_name The state property name(s) to be unique by + * @param string $name Defaults to the event's class name + * @param bool $replay_only Only defer for replayed events */ public function __construct( public string|array|null $property_name, - public string $name = self::EVENT_CLASS, - public bool $replay_only = false, + public string $name = self::EVENT_CLASS, + public bool $replay_only = false, ) {} public function applyToHook(Hook $hook): void diff --git a/src/Facades/Verbs.php b/src/Facades/Verbs.php index 33858728..79205833 100644 --- a/src/Facades/Verbs.php +++ b/src/Facades/Verbs.php @@ -14,6 +14,7 @@ /** * Commits all outstanding events + * * @method static bool commit() * * Determines if verbs is currently replaying events. @@ -24,18 +25,23 @@ * * Defers the execution of a callback. It will only get called once per unique constraint. * @method static void defer(State|string|iterable|null $unique_by, callable $callback, string $name = 'Default') - * @param State|string|int|iterable|null $unique_by The uniqueness constraint for the deferred callback. It can be a State, string or array combination of both - * @param callable $callback The callback to be executed - * @param string $name Optional name identifier for the deferred callback, defaults to 'Default'. It's a secondary constraint + * + * @param State|string|int|iterable|null $unique_by The uniqueness constraint for the deferred callback. It can be a State, string or array combination of both + * @param callable $callback The callback to be executed + * @param string $name Optional name identifier for the deferred callback, defaults to 'Default'. It's a secondary constraint * * Fires an event through the event store. + * * @method static Event fire(Event $event) - * @param Event $event The event object to be fired + * + * @param Event $event The event object to be fired + * + * @method static void createMetadataUsing(callable $callback) + * + * @param callable $callback The callback function that generates metadata * @return Event The fired event instance * * Sets a callback to create metadata for events. - * @method static void createMetadataUsing(callable $callback) - * @param callable $callback The callback function that generates metadata * * @method static EventStoreFake assertCommitted(string|Closure $event, Closure|int|null $callback = null) * @method static EventStoreFake assertNotCommitted(string|Closure $event, ?Closure $callback = null) diff --git a/src/Lifecycle/DeferredWriteQueue.php b/src/Lifecycle/DeferredWriteQueue.php index 5d9e30ac..9c0aa100 100644 --- a/src/Lifecycle/DeferredWriteQueue.php +++ b/src/Lifecycle/DeferredWriteQueue.php @@ -46,14 +46,17 @@ public function addCallback(iterable $states, callable $callback, string $name): foreach ($states as $state) { if ($state === null) { $id .= 'null'; + continue; } if (is_string($state) || is_int($state)) { $id .= $state; + continue; } if ($state instanceof State) { $id .= $state->id; + continue; } throw new \InvalidArgumentException('Invalid state type'); diff --git a/src/Lifecycle/Hook.php b/src/Lifecycle/Hook.php index 93ab2885..8768cfad 100644 --- a/src/Lifecycle/Hook.php +++ b/src/Lifecycle/Hook.php @@ -43,12 +43,12 @@ public static function fromClosure(Closure $callback): static } public function __construct( - public Closure $callback, - public array $events = [], - public array $states = [], + public Closure $callback, + public array $events = [], + public array $states = [], public SplObjectStorage $phases = new SplObjectStorage, - public ?string $name = null, - public ?DeferFor $deferred_attribute = null, + public ?string $name = null, + public ?DeferFor $deferred_attribute = null, ) {} public function forcePhases(Phase ...$phases): static From fc7f85fb6028627865d4e91f8b6344bb59c4b3f3 Mon Sep 17 00:00:00 2001 From: Nick Potts Date: Tue, 3 Dec 2024 13:01:48 +0800 Subject: [PATCH 13/15] Formatting --- src/Lifecycle/BrokerConvenienceMethods.php | 2 +- src/Lifecycle/DeferredWriteQueue.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Lifecycle/BrokerConvenienceMethods.php b/src/Lifecycle/BrokerConvenienceMethods.php index 16729860..d7cb1b9a 100644 --- a/src/Lifecycle/BrokerConvenienceMethods.php +++ b/src/Lifecycle/BrokerConvenienceMethods.php @@ -75,7 +75,7 @@ public function unlessReplaying(callable $callback) } } - public function defer(State|string|int|iterable|null $unique_by, callable $callback, string $name = 'default'): void + public function defer(State|string|iterable|null $unique_by, callable $callback, string $name = 'default'): void { $states = is_iterable($unique_by) ? $unique_by : [$unique_by]; app(DeferredWriteQueue::class)->addCallback($states, $callback, $name); diff --git a/src/Lifecycle/DeferredWriteQueue.php b/src/Lifecycle/DeferredWriteQueue.php index 9c0aa100..7a9dc544 100644 --- a/src/Lifecycle/DeferredWriteQueue.php +++ b/src/Lifecycle/DeferredWriteQueue.php @@ -38,7 +38,7 @@ public function addHook(Event $event, DeferFor $deferred, callable $callback): v } /** - * @param iterable $states + * @param iterable $states */ public function addCallback(iterable $states, callable $callback, string $name): void { @@ -49,7 +49,7 @@ public function addCallback(iterable $states, callable $callback, string $name): continue; } - if (is_string($state) || is_int($state)) { + if (is_string($state)) { $id .= $state; continue; From 84080052056854a9407319164c52b924f711a471 Mon Sep 17 00:00:00 2001 From: Nick Potts Date: Tue, 3 Dec 2024 13:33:50 +0800 Subject: [PATCH 14/15] Ensure ordering --- src/Lifecycle/DeferredWriteQueue.php | 21 ++++++++++++++------- tests/Feature/DeferredWritesTest.php | 6 +++++- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/Lifecycle/DeferredWriteQueue.php b/src/Lifecycle/DeferredWriteQueue.php index 7a9dc544..d8cdf260 100644 --- a/src/Lifecycle/DeferredWriteQueue.php +++ b/src/Lifecycle/DeferredWriteQueue.php @@ -12,6 +12,7 @@ class DeferredWriteQueue { private array $callbacks = []; + private int $count = 0; public function addHook(Event $event, DeferFor $deferred, callable $callback): void { @@ -34,7 +35,7 @@ public function addHook(Event $event, DeferFor $deferred, callable $callback): v $name = $deferred->name === DeferFor::EVENT_CLASS ? get_class($event) : $deferred->name; - $this->callbacks[$name][$uniqueByKey] = [$event, $callback, true]; + $this->callbacks[$name][$uniqueByKey][$this->count++] = [$event, $callback, true]; } /** @@ -62,20 +63,26 @@ public function addCallback(iterable $states, callable $callback, string $name): throw new \InvalidArgumentException('Invalid state type'); } - $this->callbacks[$name][$id] = [null, $callback, false]; + unset($this->callbacks[$name][$id]); + $this->callbacks[$name][$id][$this->count++] = [null, $callback, false]; } public function flush(): void { - foreach ($this->callbacks as $callbacks) { - foreach ($callbacks as $callback) { - if ($callback[2]) { - app(Wormhole::class)->warp($callback[0], $callback[1]); + foreach ($this->callbacks as $namedCallbacks) { + foreach ($namedCallbacks as $stateGroup) { + $lastCallback = end($stateGroup); + [$event, $callback, $isEventCallback] = $lastCallback; + + if ($isEventCallback) { + app(Wormhole::class)->warp($event, $callback); } else { - $callback[1](); + $callback(); } } } + $this->callbacks = []; + $this->count = 0; } } diff --git a/tests/Feature/DeferredWritesTest.php b/tests/Feature/DeferredWritesTest.php index 1a0785b6..231426f1 100644 --- a/tests/Feature/DeferredWritesTest.php +++ b/tests/Feature/DeferredWritesTest.php @@ -186,4 +186,8 @@ public function handle(): bool } } -class LatestHandleTestState extends State {} + + +class LatestHandleTestState extends State { + public $test = 'test'; +} From 52faec665aa8f0e070470d480f3da77fd50aa99c Mon Sep 17 00:00:00 2001 From: nick-potts Date: Tue, 3 Dec 2024 05:35:02 +0000 Subject: [PATCH 15/15] Fix styling --- src/Lifecycle/DeferredWriteQueue.php | 1 + tests/Feature/DeferredWritesTest.php | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Lifecycle/DeferredWriteQueue.php b/src/Lifecycle/DeferredWriteQueue.php index d8cdf260..bedacbd6 100644 --- a/src/Lifecycle/DeferredWriteQueue.php +++ b/src/Lifecycle/DeferredWriteQueue.php @@ -12,6 +12,7 @@ class DeferredWriteQueue { private array $callbacks = []; + private int $count = 0; public function addHook(Event $event, DeferFor $deferred, callable $callback): void diff --git a/tests/Feature/DeferredWritesTest.php b/tests/Feature/DeferredWritesTest.php index 231426f1..80e7109a 100644 --- a/tests/Feature/DeferredWritesTest.php +++ b/tests/Feature/DeferredWritesTest.php @@ -186,8 +186,7 @@ public function handle(): bool } } - - -class LatestHandleTestState extends State { +class LatestHandleTestState extends State +{ public $test = 'test'; }