From af1e343dc14d1972ae947dd16510e61b026dd9e8 Mon Sep 17 00:00:00 2001 From: David Badura Date: Fri, 18 Oct 2024 12:40:42 +0200 Subject: [PATCH 01/12] add pipeline again --- docs/mkdocs.yml | 1 + docs/pages/message.md | 163 +------ docs/pages/pipeline.md | 409 ++++++++++++++++++ src/Message/Message.php | 9 + src/Message/Translator/ChainTranslator.php | 3 + .../Translator/ExcludeEventTranslator.php | 3 + .../ExcludeEventWithHeaderTranslator.php | 3 + .../Translator/FilterEventTranslator.php | 3 + .../Translator/IncludeEventTranslator.php | 3 + .../IncludeEventWithHeaderTranslator.php | 3 + .../RecalculatePlayheadTranslator.php | 3 + .../Translator/ReplaceEventTranslator.php | 6 +- src/Message/Translator/Translator.php | 3 + .../Translator/UntilEventTranslator.php | 3 + .../AggregateToStreamHeaderMiddleware.php | 35 ++ src/Pipeline/Middleware/ChainMiddleware.php | 46 ++ .../Middleware/ExcludeEventMiddleware.php | 28 ++ .../ExcludeEventWithHeaderMiddleware.php | 26 ++ .../Middleware/FilterEventMiddleware.php | 31 ++ .../Middleware/IncludeEventMiddleware.php | 28 ++ .../IncludeEventWithHeaderMiddleware.php | 26 ++ src/Pipeline/Middleware/Middleware.php | 13 + .../RecalculatePlayheadMiddleware.php | 76 ++++ .../Middleware/ReplaceEventMiddleware.php | 40 ++ .../Middleware/UntilEventMiddleware.php | 41 ++ src/Pipeline/Pipeline.php | 60 +++ src/Pipeline/Source/InMemorySource.php | 22 + src/Pipeline/Source/Source.php | 13 + src/Pipeline/Source/StoreSource.php | 21 + src/Pipeline/Target/InMemoryTarget.php | 31 ++ src/Pipeline/Target/StoreTarget.php | 21 + src/Pipeline/Target/Target.php | 12 + src/Store/DoctrineDbalStore.php | 5 + src/Store/StreamDoctrineDbalStore.php | 5 + ...igrateAggregateToStreamStoreSubscriber.php | 91 ++++ .../Subscription/SubscriptionTest.php | 118 +++++ 36 files changed, 1241 insertions(+), 163 deletions(-) create mode 100644 docs/pages/pipeline.md create mode 100644 src/Pipeline/Middleware/AggregateToStreamHeaderMiddleware.php create mode 100644 src/Pipeline/Middleware/ChainMiddleware.php create mode 100644 src/Pipeline/Middleware/ExcludeEventMiddleware.php create mode 100644 src/Pipeline/Middleware/ExcludeEventWithHeaderMiddleware.php create mode 100644 src/Pipeline/Middleware/FilterEventMiddleware.php create mode 100644 src/Pipeline/Middleware/IncludeEventMiddleware.php create mode 100644 src/Pipeline/Middleware/IncludeEventWithHeaderMiddleware.php create mode 100644 src/Pipeline/Middleware/Middleware.php create mode 100644 src/Pipeline/Middleware/RecalculatePlayheadMiddleware.php create mode 100644 src/Pipeline/Middleware/ReplaceEventMiddleware.php create mode 100644 src/Pipeline/Middleware/UntilEventMiddleware.php create mode 100644 src/Pipeline/Pipeline.php create mode 100644 src/Pipeline/Source/InMemorySource.php create mode 100644 src/Pipeline/Source/Source.php create mode 100644 src/Pipeline/Source/StoreSource.php create mode 100644 src/Pipeline/Target/InMemoryTarget.php create mode 100644 src/Pipeline/Target/StoreTarget.php create mode 100644 src/Pipeline/Target/Target.php create mode 100644 tests/Integration/Subscription/Subscriber/MigrateAggregateToStreamStoreSubscriber.php diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 790f9a724..4850d4dac 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -106,6 +106,7 @@ nav: - Upcasting: upcasting.md - Message Decorator: message_decorator.md - Split Stream: split_stream.md + - Pipeline / ACL: pipeline.md - Time / Clock: clock.md - Testing: testing.md - CLI: cli.md diff --git a/docs/pages/message.md b/docs/pages/message.md index 398ddb170..bf0e4faff 100644 --- a/docs/pages/message.md +++ b/docs/pages/message.md @@ -97,169 +97,7 @@ use Patchlevel\EventSourcing\Message\Message; /** @var Message $message */ $message->header(ApplicationHeader::class); ``` -## Translator -Translator can be used to manipulate, filter or expand messages or events. -This can be used for anti-corruption layers, data migration, or to fix errors in the event stream. - -### Exclude - -With this translator you can exclude certain events. - -```php -use Patchlevel\EventSourcing\Message\Translator\ExcludeEventTranslator; - -$translator = new ExcludeEventTranslator([EmailChanged::class]); -``` -### Include - -With this translator you can only allow certain events. - -```php -use Patchlevel\EventSourcing\Message\Translator\IncludeEventTranslator; - -$translator = new IncludeEventTranslator([ProfileCreated::class]); -``` -### Filter - -If the translator `ExcludeEventTranslator` and `IncludeEventTranslator` are not sufficient, -you can also write your own filter. -This translator expects a callback that returns either true to allow events or false to not allow them. - -```php -use Patchlevel\EventSourcing\Message\Translator\FilterEventTranslator; - -$translator = new FilterEventTranslator(static function (object $event) { - if (!$event instanceof ProfileCreated) { - return true; - } - - return $event->allowNewsletter(); -}); -``` -### Exclude Events with Header - -With this translator you can exclude event with specific header. - -```php -use Patchlevel\EventSourcing\Message\Translator\ExcludeEventWithHeaderTranslator; -use Patchlevel\EventSourcing\Store\ArchivedHeader; - -$translator = new ExcludeEventWithHeaderTranslator(ArchivedHeader::class); -``` -### Only Events with Header - -With this translator you can only allow events with a specific header. - -```php -use Patchlevel\EventSourcing\Message\Translator\IncludeEventWithHeaderTranslator; - -$translator = new IncludeEventWithHeaderTranslator(ArchivedHeader::class); -``` -### Replace - -If you want to replace an event, you can use the `ReplaceEventTranslator`. -The first parameter you have to define is the event class that you want to replace. -And as a second parameter a callback, that the old event awaits and a new event returns. - -```php -use Patchlevel\EventSourcing\Message\Translator\ReplaceEventTranslator; - -$translator = new ReplaceEventTranslator(OldVisited::class, static function (OldVisited $oldVisited) { - return new NewVisited($oldVisited->profileId()); -}); -``` -### Until - -A use case could also be that you want to look at the projection from a previous point in time. -You can use the `UntilEventTranslator` to only allow events that were `recorded` before this point in time. - -```php -use Patchlevel\EventSourcing\Message\Translator\UntilEventTranslator; - -$translator = new UntilEventTranslator(new DateTimeImmutable('2020-01-01 12:00:00')); -``` -### Recalculate playhead - -This translator can be used to recalculate the playhead. -The playhead must always be in ascending order so that the data is valid. -Some translator can break this order and the translator `RecalculatePlayheadTranslator` can fix this problem. - -```php -use Patchlevel\EventSourcing\Message\Translator\RecalculatePlayheadTranslator; - -$translator = new RecalculatePlayheadTranslator(); -``` -!!! tip - - If you migrate your event stream, you can use the `RecalculatePlayheadTranslator` to fix the playhead. - -### Chain - -If you want to group your translator, you can use one or more `ChainTranslator`. - -```php -use Patchlevel\EventSourcing\Message\Translator\ChainTranslator; -use Patchlevel\EventSourcing\Message\Translator\ExcludeEventTranslator; -use Patchlevel\EventSourcing\Message\Translator\RecalculatePlayheadTranslator; - -$translator = new ChainTranslator([ - new ExcludeEventTranslator([EmailChanged::class]), - new RecalculatePlayheadTranslator(), -]); -``` -### Custom Translator - -You can also write a custom translator. The translator gets a message and can return `n` messages. -There are the following possibilities: - -* Return only the message to an array to leave it unchanged. -* Put another message in the array to swap the message. -* Return an empty array to remove the message. -* Or return multiple messages to enrich the stream. - -In our case, the domain has changed a bit. -In the beginning we had a `ProfileCreated` event that just created a profile. -Now we have a `ProfileRegistered` and a `ProfileActivated` event, -which should replace the `ProfileCreated` event. - -```php -use Patchlevel\EventSourcing\Message\Message; -use Patchlevel\EventSourcing\Message\Translator\Translator; - -final class SplitProfileCreatedTranslator implements Translator -{ - public function __invoke(Message $message): array - { - $event = $message->event(); - - if (!$event instanceof ProfileCreated) { - return [$message]; - } - - $profileRegisteredMessage = Message::createWithHeaders( - new ProfileRegistered($event->id(), $event->name()), - $message->headers(), - ); - - $profileActivatedMessage = Message::createWithHeaders( - new ProfileActivated($event->id()), - $message->headers(), - ); - - return [$profileRegisteredMessage, $profileActivatedMessage]; - } -} -``` -!!! warning - - Since we changed the number of messages, we have to recalculate the playhead. - -!!! tip - - You don't have to migrate the store directly for every change, - but you can also use the [upcasting](upcasting.md) feature. - ## Learn more * [How to decorate messages](message_decorator.md) @@ -267,3 +105,4 @@ final class SplitProfileCreatedTranslator implements Translator * [How to store messages](store.md) * [How to use subscriptions](subscription.md) * [How to use the event bus](event_bus.md) +* [How to migrate messages](pipeline.md) diff --git a/docs/pages/pipeline.md b/docs/pages/pipeline.md new file mode 100644 index 000000000..8fe975d67 --- /dev/null +++ b/docs/pages/pipeline.md @@ -0,0 +1,409 @@ +# Pipeline / Anti Corruption Layer + +A store is immutable, i.e. it cannot be changed afterwards. +This includes both manipulating events and deleting them. + +Instead, you can duplicate the store and manipulate the events in the process. +Thus the old store remains untouched and you can test the new store beforehand, +whether the migration worked. + +In this example the event `PrivacyAdded` is removed and the event `OldVisited` is replaced by `NewVisited`: + +```php +use Patchlevel\EventSourcing\Pipeline\Middleware\ExcludeEventMiddleware; +use Patchlevel\EventSourcing\Pipeline\Middleware\RecalculatePlayheadMiddleware; +use Patchlevel\EventSourcing\Pipeline\Middleware\ReplaceEventMiddleware; +use Patchlevel\EventSourcing\Pipeline\Pipeline; +use Patchlevel\EventSourcing\Pipeline\Source\StoreSource; +use Patchlevel\EventSourcing\Pipeline\Target\StoreTarget; + +$pipeline = new Pipeline( + new StoreSource($oldStore), + new StoreTarget($newStore), + [ + new ExcludeEventMiddleware([PrivacyAdded::class]), + new ReplaceEventMiddleware(OldVisited::class, static function (OldVisited $oldVisited) { + return new NewVisited($oldVisited->profileId()); + }), + new RecalculatePlayheadMiddleware(), + ] +); +``` + +!!! danger + + Under no circumstances may the same store be used that is used for the source. + Otherwise the store will be broken afterwards! + +The pipeline can also be used to create or rebuild a projection: + +```php +use Patchlevel\EventSourcing\Pipeline\Pipeline; +use Patchlevel\EventSourcing\Pipeline\Source\StoreSource; +use Patchlevel\EventSourcing\Pipeline\Target\ProjectionTarget; + +$pipeline = new Pipeline( + new StoreSource($store), + new ProjectionTarget($projection) +); +``` + +The principle remains the same. +There is a source where the data comes from. +A target where the data should flow. +And any number of middlewares to do something with the data beforehand. + +## Source + +The first thing you need is a source of where the data should come from. + +### Store + +The `StoreSource` is the standard source to load all events from the database. + +```php +use Patchlevel\EventSourcing\Pipeline\Source\StoreSource; + +$source = new StoreSource($store); +``` + +### In Memory + +There is an `InMemorySource` that receives the messages in an array. This source can be used to write pipeline tests. + +```php +use Patchlevel\EventSourcing\EventBus\Message; +use Patchlevel\EventSourcing\Pipeline\Source\InMemorySource; + +$source = new InMemorySource([ + new Message( + Profile::class, + '1', + 1, + new ProfileCreated(Email::fromString('david.badura@patchlevel.de')), + ), + // ... +]); +``` + +### Custom Source + +You can also create your own source class. It has to inherit from `Source`. +Here you can, for example, create a migration from another event sourcing system or similar system. + +```php +use Patchlevel\EventSourcing\EventBus\Message; +use Patchlevel\EventSourcing\Pipeline\Source\Source; + +$source = new class implements Source { + /** + * @return Generator + */ + public function load(): Generator + { + yield new Message( + Profile::class, + '1', + 0, + new ProfileCreated('1', ['name' => 'David']) + ); + } + + public function count(): int + { + reutrn 1; + } +} +``` + +## Target + +After you have a source, you still need the destination of the pipeline. + +### Store + +You can use a store to save the final result. + +```php +use Patchlevel\EventSourcing\Pipeline\Target\StoreTarget; + +$target = new StoreTarget($store); +``` + +!!! danger + + Under no circumstances may the same store be used that is used for the source. + Otherwise the store will be broken afterwards! + +!!! note + + It does not matter whether the previous store was a SingleTable or a MultiTable. + You can switch back and forth between both store types using the pipeline. + +### Projection + +A projection can also be used as a target. +For example, to set up a new projection or to build a new projection. + +```php +use Patchlevel\EventSourcing\Pipeline\Target\ProjectionTarget; + +$target = new ProjectionTarget($projection); +``` + +### Projection Handler + +If you want to build or create all projections from scratch, +then you can also use the ProjectionRepositoryTarget. +In this, the individual projections are iterated and the events are then passed on. + +```php +use Patchlevel\EventSourcing\Pipeline\Target\ProjectionHandlerTarget; + +$target = new ProjectionHandlerTarget($projectionHandler); +``` + +### In Memory + +There is also an in-memory variant for the target. This target can also be used for tests. +With the `messages` method you get all `Messages` that have reached the target. + +```php +use Patchlevel\EventSourcing\Pipeline\Target\InMemoryTarget; + +$target = new InMemoryTarget(); + +// run pipeline + +$messages = $target->messages(); +``` + +### Custom Target + +You can also define your own target. To do this, you need to implement the `Target` interface. + +```php +use Patchlevel\EventSourcing\EventBus\Message; + +final class OtherStoreTarget implements Target +{ + private OtherStore $store; + + public function __construct(OtherStore $store) + { + $this->store = $store; + } + + public function save(Message $message): void + { + $this->store->save($message); + } +} +``` + +## Middlewares + +Middelwares can be used to manipulate, delete or expand messages or events during the process. + +!!! warning + + It is important to know that some middlewares require recalculation from the playhead, + if the target is a store. This is a numbering of the events that must be in ascending order. + A corresponding note is supplied with every middleware. + +### Exclude + +With this middleware you can exclude certain events. + +```php +use Patchlevel\EventSourcing\Pipeline\Middleware\ExcludeEventMiddleware; + +$middleware = new ExcludeEventMiddleware([EmailChanged::class]); +``` + +!!! warning + + After this middleware, the playhead must be recalculated! + +### Include + + +With this middleware you can only allow certain events. + +```php +use Patchlevel\EventSourcing\Pipeline\Middleware\IncludeEventMiddleware; + +$middleware = new IncludeEventMiddleware([ProfileCreated::class]); +``` + +!!! warning + + After this middleware, the playhead must be recalculated! + +### Filter + +If the middlewares `ExcludeEventMiddleware` and `IncludeEventMiddleware` are not sufficient, +you can also write your own filter. +This middleware expects a callback that returns either true to allow events or false to not allow them. + +```php +use Patchlevel\EventSourcing\Aggregate\AggregateChanged; +use Patchlevel\EventSourcing\Pipeline\Middleware\FilterEventMiddleware; + +$middleware = new FilterEventMiddleware(function (AggregateChanged $event) { + if (!$event instanceof ProfileCreated) { + return true; + } + + return $event->allowNewsletter(); +}); +``` + +!!! warning + + After this middleware, the playhead must be recalculated! + +### Exclude Archived Events + +With this middleware you can exclude archived events. + +```php +use Patchlevel\EventSourcing\Pipeline\Middleware\ExcludeArchivedEventMiddleware; + +$middleware = new ExcludeArchivedEventMiddleware(); +``` + +!!! warning + + After this middleware, the playhead must be recalculated! + +### Only Archived Events + + +With this middleware you can only allow archived events. + +```php +use Patchlevel\EventSourcing\Pipeline\Middleware\OnlyArchivedEventMiddleware; + +$middleware = new OnlyArchivedEventMiddleware(); +``` + +!!! warning + + After this middleware, the playhead must be recalculated! + +### Replace + +If you want to replace an event, you can use the `ReplaceEventMiddleware`. +The first parameter you have to define is the event class that you want to replace. +And as a second parameter a callback, that the old event awaits and a new event returns. + +```php +use Patchlevel\EventSourcing\Pipeline\Middleware\ReplaceEventMiddleware; + +$middleware = new ReplaceEventMiddleware(OldVisited::class, static function (OldVisited $oldVisited) { + return new NewVisited($oldVisited->profileId()); +}); +``` + +!!! note + + The middleware takes over the playhead and recordedAt information. + +### Until + +A use case could also be that you want to look at the projection from a previous point in time. +You can use the `UntilEventMiddleware` to only allow events that were `recorded` before this point in time. + +```php +use Patchlevel\EventSourcing\Pipeline\Middleware\ClassRenameMiddleware; + +$middleware = new UntilEventMiddleware(new DateTimeImmutable('2020-01-01 12:00:00')); +``` + +!!! warning + + After this middleware, the playhead must be recalculated! + +### Recalculate playhead + +This middleware can be used to recalculate the playhead. +The playhead must always be in ascending order so that the data is valid. +Some middleware can break this order and the middleware `RecalculatePlayheadMiddleware` can fix this problem. + +```php +use Patchlevel\EventSourcing\Pipeline\Middleware\RecalculatePlayheadMiddleware; + +$middleware = new RecalculatePlayheadMiddleware(); +``` + +!!! note + + You only need to add this middleware once at the end of the pipeline. + +### Chain + +If you want to group your middleware, you can use one or more `ChainMiddleware`. + +```php +use Patchlevel\EventSourcing\Pipeline\Middleware\ChainMiddleware; +use Patchlevel\EventSourcing\Pipeline\Middleware\ExcludeEventMiddleware; +use Patchlevel\EventSourcing\Pipeline\Middleware\RecalculatePlayheadMiddleware; + +$middleware = new ChainMiddleware([ + new ExcludeEventMiddleware([EmailChanged::class]), + new RecalculatePlayheadMiddleware() +]); +``` + +### Custom middleware + +You can also write a custom middleware. The middleware gets a message and can return `N` messages. +There are the following possibilities: + +* Return only the message to an array to leave it unchanged. +* Put another message in the array to swap the message. +* Return an empty array to remove the message. +* Or return multiple messages to enrich the stream. + +In our case, the domain has changed a bit. +In the beginning we had a `ProfileCreated` event that just created a profile. +Now we have a `ProfileRegistered` and a `ProfileActivated` event, +which should replace the `ProfileCreated` event. + +```php +use Patchlevel\EventSourcing\EventBus\Message; +use Patchlevel\EventSourcing\Pipeline\Middleware\Middleware; + +final class SplitProfileCreatedMiddleware implements Middleware +{ + public function __invoke(Message $message): array + { + $event = $message->event(); + + if (!$event instanceof ProfileCreated) { + return [$message]; + } + + $profileRegisteredMessage = Message::createWithHeaders( + new ProfileRegistered($event->id(), $event->name()), + $message->headers() + ); + + $profileActivatedMessage = Message::createWithHeaders( + new ProfileActivated($event->id()), + $message->headers() + ); + + return [$profileRegisteredMessage, $profileActivatedMessage]; + } +} +``` + +!!! warning + + Since we changed the number of messages, we have to recalculate the playhead. + +!!! note + + You can find more about messages [here](event_bus.md). \ No newline at end of file diff --git a/src/Message/Message.php b/src/Message/Message.php index 40635c267..0085354c4 100644 --- a/src/Message/Message.php +++ b/src/Message/Message.php @@ -84,6 +84,15 @@ public function withHeader(object $header): self return $message; } + /** @param class-string $name */ + public function removeHeader(string $name): self + { + $message = clone $this; + unset($message->headers[$name]); + + return $message; + } + /** @return list */ public function headers(): array { diff --git a/src/Message/Translator/ChainTranslator.php b/src/Message/Translator/ChainTranslator.php index 708bdb3b4..03931c81b 100644 --- a/src/Message/Translator/ChainTranslator.php +++ b/src/Message/Translator/ChainTranslator.php @@ -8,6 +8,9 @@ use function array_values; +/** + * @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\ChainMiddleware instead + */ final class ChainTranslator implements Translator { /** @param iterable $translators */ diff --git a/src/Message/Translator/ExcludeEventTranslator.php b/src/Message/Translator/ExcludeEventTranslator.php index f4a26f632..aeaaadc9d 100644 --- a/src/Message/Translator/ExcludeEventTranslator.php +++ b/src/Message/Translator/ExcludeEventTranslator.php @@ -6,6 +6,9 @@ use Patchlevel\EventSourcing\Message\Message; +/** + * @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\ExcludeEventMiddleware instead + */ final class ExcludeEventTranslator implements Translator { /** @param list $classes */ diff --git a/src/Message/Translator/ExcludeEventWithHeaderTranslator.php b/src/Message/Translator/ExcludeEventWithHeaderTranslator.php index 4fa4b7854..fcd928956 100644 --- a/src/Message/Translator/ExcludeEventWithHeaderTranslator.php +++ b/src/Message/Translator/ExcludeEventWithHeaderTranslator.php @@ -6,6 +6,9 @@ use Patchlevel\EventSourcing\Message\Message; +/** + * @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\ExcludeEventWithHeaderMiddleware instead + */ final class ExcludeEventWithHeaderTranslator implements Translator { /** @param class-string $header */ diff --git a/src/Message/Translator/FilterEventTranslator.php b/src/Message/Translator/FilterEventTranslator.php index b8f3c5b78..b93261cb9 100644 --- a/src/Message/Translator/FilterEventTranslator.php +++ b/src/Message/Translator/FilterEventTranslator.php @@ -6,6 +6,9 @@ use Patchlevel\EventSourcing\Message\Message; +/** + * @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\FilterEventMiddleware instead + */ final class FilterEventTranslator implements Translator { /** @var callable(object $event):bool */ diff --git a/src/Message/Translator/IncludeEventTranslator.php b/src/Message/Translator/IncludeEventTranslator.php index ae2a412f5..4bd1dc230 100644 --- a/src/Message/Translator/IncludeEventTranslator.php +++ b/src/Message/Translator/IncludeEventTranslator.php @@ -6,6 +6,9 @@ use Patchlevel\EventSourcing\Message\Message; +/** + * @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\IncludeEventWithHeaderMiddleware instead + */ final class IncludeEventTranslator implements Translator { /** @param list $classes */ diff --git a/src/Message/Translator/IncludeEventWithHeaderTranslator.php b/src/Message/Translator/IncludeEventWithHeaderTranslator.php index 8c88514ae..6dcd58b5b 100644 --- a/src/Message/Translator/IncludeEventWithHeaderTranslator.php +++ b/src/Message/Translator/IncludeEventWithHeaderTranslator.php @@ -6,6 +6,9 @@ use Patchlevel\EventSourcing\Message\Message; +/** + * @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\IncludeEventWithHeaderMiddleware instead + */ final class IncludeEventWithHeaderTranslator implements Translator { /** @param class-string $header */ diff --git a/src/Message/Translator/RecalculatePlayheadTranslator.php b/src/Message/Translator/RecalculatePlayheadTranslator.php index 27185baaa..de3011650 100644 --- a/src/Message/Translator/RecalculatePlayheadTranslator.php +++ b/src/Message/Translator/RecalculatePlayheadTranslator.php @@ -11,6 +11,9 @@ use function array_key_exists; +/** + * @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\RecalculatePlayheadMiddleware instead + */ final class RecalculatePlayheadTranslator implements Translator { /** @var array */ diff --git a/src/Message/Translator/ReplaceEventTranslator.php b/src/Message/Translator/ReplaceEventTranslator.php index 3e13fa416..d31076175 100644 --- a/src/Message/Translator/ReplaceEventTranslator.php +++ b/src/Message/Translator/ReplaceEventTranslator.php @@ -6,7 +6,11 @@ use Patchlevel\EventSourcing\Message\Message; -/** @template T of object */ +/** + * @template T of object + * + * @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\ReplaceEventMiddleware instead + */ final class ReplaceEventTranslator implements Translator { /** @var callable(T $event):object */ diff --git a/src/Message/Translator/Translator.php b/src/Message/Translator/Translator.php index 66fe20057..7450c944f 100644 --- a/src/Message/Translator/Translator.php +++ b/src/Message/Translator/Translator.php @@ -6,6 +6,9 @@ use Patchlevel\EventSourcing\Message\Message; +/** + * @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\Middleware instead + */ interface Translator { /** @return list */ diff --git a/src/Message/Translator/UntilEventTranslator.php b/src/Message/Translator/UntilEventTranslator.php index 3cdf739ad..1d14ca6a3 100644 --- a/src/Message/Translator/UntilEventTranslator.php +++ b/src/Message/Translator/UntilEventTranslator.php @@ -10,6 +10,9 @@ use Patchlevel\EventSourcing\Message\Message; use Patchlevel\EventSourcing\Store\StreamHeader; +/** + * @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\UntilEventMiddleware instead + */ final class UntilEventTranslator implements Translator { public function __construct( diff --git a/src/Pipeline/Middleware/AggregateToStreamHeaderMiddleware.php b/src/Pipeline/Middleware/AggregateToStreamHeaderMiddleware.php new file mode 100644 index 000000000..9620a041f --- /dev/null +++ b/src/Pipeline/Middleware/AggregateToStreamHeaderMiddleware.php @@ -0,0 +1,35 @@ + */ + public function __invoke(Message $message): array + { + if (!$message->hasHeader(AggregateHeader::class)) { + return [$message]; + } + + $aggregateHeader = $message->header(AggregateHeader::class); + + return [ + $message + ->removeHeader(AggregateHeader::class) + ->withHeader(new StreamHeader( + $aggregateHeader->streamName(), + $aggregateHeader->playhead, + $aggregateHeader->recordedOn, + )) + ]; + } +} diff --git a/src/Pipeline/Middleware/ChainMiddleware.php b/src/Pipeline/Middleware/ChainMiddleware.php new file mode 100644 index 000000000..50a06d515 --- /dev/null +++ b/src/Pipeline/Middleware/ChainMiddleware.php @@ -0,0 +1,46 @@ + $translators */ + public function __construct( + private readonly iterable $translators, + ) { + } + + /** @return list */ + public function __invoke(Message $message): array + { + $messages = [$message]; + + foreach ($this->translators as $middleware) { + $messages = $this->process($middleware, $messages); + } + + return $messages; + } + + /** + * @param list $messages + * + * @return list + */ + private function process(Middleware $translator, array $messages): array + { + $result = []; + + foreach ($messages as $message) { + $result += $translator($message); + } + + return array_values($result); + } +} diff --git a/src/Pipeline/Middleware/ExcludeEventMiddleware.php b/src/Pipeline/Middleware/ExcludeEventMiddleware.php new file mode 100644 index 000000000..0fcb96376 --- /dev/null +++ b/src/Pipeline/Middleware/ExcludeEventMiddleware.php @@ -0,0 +1,28 @@ + $classes */ + public function __construct( + private readonly array $classes, + ) { + } + + /** @return list */ + public function __invoke(Message $message): array + { + foreach ($this->classes as $class) { + if ($message->event() instanceof $class) { + return []; + } + } + + return [$message]; + } +} diff --git a/src/Pipeline/Middleware/ExcludeEventWithHeaderMiddleware.php b/src/Pipeline/Middleware/ExcludeEventWithHeaderMiddleware.php new file mode 100644 index 000000000..608612285 --- /dev/null +++ b/src/Pipeline/Middleware/ExcludeEventWithHeaderMiddleware.php @@ -0,0 +1,26 @@ + */ + public function __invoke(Message $message): array + { + if ($message->hasHeader($this->header)) { + return []; + } + + return [$message]; + } +} diff --git a/src/Pipeline/Middleware/FilterEventMiddleware.php b/src/Pipeline/Middleware/FilterEventMiddleware.php new file mode 100644 index 000000000..9aeaefb55 --- /dev/null +++ b/src/Pipeline/Middleware/FilterEventMiddleware.php @@ -0,0 +1,31 @@ +callable = $callable; + } + + /** @return list */ + public function __invoke(Message $message): array + { + $result = ($this->callable)($message->event()); + + if ($result) { + return [$message]; + } + + return []; + } +} diff --git a/src/Pipeline/Middleware/IncludeEventMiddleware.php b/src/Pipeline/Middleware/IncludeEventMiddleware.php new file mode 100644 index 000000000..7b3262423 --- /dev/null +++ b/src/Pipeline/Middleware/IncludeEventMiddleware.php @@ -0,0 +1,28 @@ + $classes */ + public function __construct( + private readonly array $classes, + ) { + } + + /** @return list */ + public function __invoke(Message $message): array + { + foreach ($this->classes as $class) { + if ($message->event() instanceof $class) { + return [$message]; + } + } + + return []; + } +} diff --git a/src/Pipeline/Middleware/IncludeEventWithHeaderMiddleware.php b/src/Pipeline/Middleware/IncludeEventWithHeaderMiddleware.php new file mode 100644 index 000000000..3ad46446b --- /dev/null +++ b/src/Pipeline/Middleware/IncludeEventWithHeaderMiddleware.php @@ -0,0 +1,26 @@ + */ + public function __invoke(Message $message): array + { + if ($message->hasHeader($this->header)) { + return [$message]; + } + + return []; + } +} diff --git a/src/Pipeline/Middleware/Middleware.php b/src/Pipeline/Middleware/Middleware.php new file mode 100644 index 000000000..490f19514 --- /dev/null +++ b/src/Pipeline/Middleware/Middleware.php @@ -0,0 +1,13 @@ + */ + public function __invoke(Message $message): array; +} diff --git a/src/Pipeline/Middleware/RecalculatePlayheadMiddleware.php b/src/Pipeline/Middleware/RecalculatePlayheadMiddleware.php new file mode 100644 index 000000000..cabb99f93 --- /dev/null +++ b/src/Pipeline/Middleware/RecalculatePlayheadMiddleware.php @@ -0,0 +1,76 @@ + */ + private array $index = []; + + /** @return list */ + public function __invoke(Message $message): array + { + try { + $header = $message->header(AggregateHeader::class); + } catch (HeaderNotFound) { + try { + $header = $message->header(StreamHeader::class); + } catch (HeaderNotFound) { + return [$message]; + } + } + + $stream = $header instanceof StreamHeader ? $header->streamName : $header->streamName(); + + $playhead = $this->nextPlayhead($stream); + + if ($header->playhead === $playhead) { + return [$message]; + } + + if ($header instanceof StreamHeader) { + return [ + $message->withHeader(new StreamHeader( + $header->streamName, + $playhead, + $header->recordedOn, + )), + ]; + } + + return [ + $message->withHeader(new AggregateHeader( + $header->aggregateName, + $header->aggregateId, + $playhead, + $header->recordedOn, + )), + ]; + } + + public function reset(): void + { + $this->index = []; + } + + /** @return positive-int */ + private function nextPlayhead(string $stream): int + { + if (!array_key_exists($stream, $this->index)) { + $this->index[$stream] = 1; + } else { + $this->index[$stream]++; + } + + return $this->index[$stream]; + } +} diff --git a/src/Pipeline/Middleware/ReplaceEventMiddleware.php b/src/Pipeline/Middleware/ReplaceEventMiddleware.php new file mode 100644 index 000000000..9dc92fdb5 --- /dev/null +++ b/src/Pipeline/Middleware/ReplaceEventMiddleware.php @@ -0,0 +1,40 @@ + $class + * @param callable(T $event):object $callable + */ + public function __construct( + private readonly string $class, + callable $callable, + ) { + $this->callable = $callable; + } + + /** @return list */ + public function __invoke(Message $message): array + { + $event = $message->event(); + + if (!$event instanceof $this->class) { + return [$message]; + } + + $callable = $this->callable; + $newEvent = $callable($event); + + return [Message::createWithHeaders($newEvent, $message->headers())]; + } +} diff --git a/src/Pipeline/Middleware/UntilEventMiddleware.php b/src/Pipeline/Middleware/UntilEventMiddleware.php new file mode 100644 index 000000000..44cc201ba --- /dev/null +++ b/src/Pipeline/Middleware/UntilEventMiddleware.php @@ -0,0 +1,41 @@ + */ + public function __invoke(Message $message): array + { + try { + $header = $message->header(AggregateHeader::class); + } catch (HeaderNotFound) { + try { + $header = $message->header(StreamHeader::class); + } catch (HeaderNotFound) { + return [$message]; + } + } + + $recordedOn = $header->recordedOn; + + if ($recordedOn < $this->until) { + return [$message]; + } + + return []; + } +} diff --git a/src/Pipeline/Pipeline.php b/src/Pipeline/Pipeline.php new file mode 100644 index 000000000..bc0cefc18 --- /dev/null +++ b/src/Pipeline/Pipeline.php @@ -0,0 +1,60 @@ +|Middleware $middlewares */ + public function __construct( + private readonly Source $source, + private readonly Target $target, + array|Middleware $middlewares = [], + private readonly int $bufferSize = 1_000, + ) { + if (is_array($middlewares)) { + $this->middleware = new ChainMiddleware($middlewares); + } else { + $this->middleware = $middlewares; + } + } + + public function run(): void + { + $buffer = []; + + foreach ($this->source->load() as $message) { + $result = ($this->middleware)($message); + + array_push($buffer, ...$result); + + if (count($buffer) >= $this->bufferSize) { + $this->target->save(...$result); + $buffer = []; + } + } + + if (count($buffer) > 0) { + $this->target->save(...$buffer); + } + } + + public static function execute( + Source $source, + Target $target, + array|Middleware $middlewares = [], + $bufferSize = 1_000, + ): void + { + $pipeline = new self($source, $target, $middlewares, $bufferSize); + $pipeline->run(); + } +} \ No newline at end of file diff --git a/src/Pipeline/Source/InMemorySource.php b/src/Pipeline/Source/InMemorySource.php new file mode 100644 index 000000000..720050140 --- /dev/null +++ b/src/Pipeline/Source/InMemorySource.php @@ -0,0 +1,22 @@ + $messages */ + public function __construct( + private readonly array $messages + ) { + } + + /** @return iterable */ + public function load(): iterable + { + return $this->messages; + } +} \ No newline at end of file diff --git a/src/Pipeline/Source/Source.php b/src/Pipeline/Source/Source.php new file mode 100644 index 000000000..1870b8d78 --- /dev/null +++ b/src/Pipeline/Source/Source.php @@ -0,0 +1,13 @@ + */ + public function load(): iterable; +} \ No newline at end of file diff --git a/src/Pipeline/Source/StoreSource.php b/src/Pipeline/Source/StoreSource.php new file mode 100644 index 000000000..a21825795 --- /dev/null +++ b/src/Pipeline/Source/StoreSource.php @@ -0,0 +1,21 @@ +store->load(); + } +} \ No newline at end of file diff --git a/src/Pipeline/Target/InMemoryTarget.php b/src/Pipeline/Target/InMemoryTarget.php new file mode 100644 index 000000000..ad7a102ac --- /dev/null +++ b/src/Pipeline/Target/InMemoryTarget.php @@ -0,0 +1,31 @@ + */ + private array $messages = []; + + public function save(Message ...$message): void + { + foreach ($message as $m) { + $this->messages[] = $m; + } + } + + /** @return list */ + public function messages(): array + { + return $this->messages; + } + + public function clear(): void + { + $this->messages = []; + } +} \ No newline at end of file diff --git a/src/Pipeline/Target/StoreTarget.php b/src/Pipeline/Target/StoreTarget.php new file mode 100644 index 000000000..ef9e60a9b --- /dev/null +++ b/src/Pipeline/Target/StoreTarget.php @@ -0,0 +1,21 @@ +store->save(...$message); + } +} \ No newline at end of file diff --git a/src/Pipeline/Target/Target.php b/src/Pipeline/Target/Target.php new file mode 100644 index 000000000..2a9be09df --- /dev/null +++ b/src/Pipeline/Target/Target.php @@ -0,0 +1,12 @@ +connection; + } + private function createTriggerFunctionName(): string { $tableConfig = explode('.', $this->config['table_name']); diff --git a/src/Store/StreamDoctrineDbalStore.php b/src/Store/StreamDoctrineDbalStore.php index d9c1cd166..703c561fb 100644 --- a/src/Store/StreamDoctrineDbalStore.php +++ b/src/Store/StreamDoctrineDbalStore.php @@ -435,6 +435,11 @@ public function setupSubscription(): void )); } + public function connection(): Connection + { + return $this->connection; + } + private function createTriggerFunctionName(): string { $tableConfig = explode('.', $this->config['table_name']); diff --git a/tests/Integration/Subscription/Subscriber/MigrateAggregateToStreamStoreSubscriber.php b/tests/Integration/Subscription/Subscriber/MigrateAggregateToStreamStoreSubscriber.php new file mode 100644 index 000000000..20d2b41bd --- /dev/null +++ b/tests/Integration/Subscription/Subscriber/MigrateAggregateToStreamStoreSubscriber.php @@ -0,0 +1,91 @@ + + */ + private array $messages = []; + + public function __construct( + private readonly StreamDoctrineDbalStore $targetStore, + ) { + $this->schemaDirector = new DoctrineSchemaDirector( + $targetStore->connection(), + new ChainDoctrineSchemaConfigurator([ + $targetStore, + ]), + ); + } + + #[Subscribe('*')] + public function handle(Message $message): void + { + $this->messages[] = $message; + } + + public function beginBatch(): void + { + $this->messages = []; + } + + public function commitBatch(): void + { + $messages = $this->messages; + $this->messages = []; + + Pipeline::execute( + new InMemorySource($messages), + new StoreTarget($this->targetStore), + new AggregateToStreamHeaderMiddleware(), + self::BATCH_SIZE * 10, // make sure we have only one batch + ); + } + + public function rollbackBatch(): void + { + $this->messages = []; + } + + public function forceCommit(): bool + { + return count($this->messages) >= self::BATCH_SIZE; + } + + #[Setup] + public function setup(): void + { + $this->schemaDirector->create(); + } + + #[Teardown] + public function teardown(): void + { + $this->schemaDirector->drop(); + } +} diff --git a/tests/Integration/Subscription/SubscriptionTest.php b/tests/Integration/Subscription/SubscriptionTest.php index 08af8133e..fe5a45676 100644 --- a/tests/Integration/Subscription/SubscriptionTest.php +++ b/tests/Integration/Subscription/SubscriptionTest.php @@ -22,6 +22,7 @@ use Patchlevel\EventSourcing\Schema\DoctrineSchemaDirector; use Patchlevel\EventSourcing\Serializer\DefaultEventSerializer; use Patchlevel\EventSourcing\Store\DoctrineDbalStore; +use Patchlevel\EventSourcing\Store\StreamDoctrineDbalStore; use Patchlevel\EventSourcing\Subscription\Engine\CatchUpSubscriptionEngine; use Patchlevel\EventSourcing\Subscription\Engine\DefaultSubscriptionEngine; use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngineCriteria; @@ -33,6 +34,7 @@ use Patchlevel\EventSourcing\Subscription\Subscription; use Patchlevel\EventSourcing\Tests\DbalManager; use Patchlevel\EventSourcing\Tests\Integration\Subscription\Subscriber\ErrorProducerSubscriber; +use Patchlevel\EventSourcing\Tests\Integration\Subscription\Subscriber\MigrateAggregateToStreamStoreSubscriber; use Patchlevel\EventSourcing\Tests\Integration\Subscription\Subscriber\ProfileNewProjection; use Patchlevel\EventSourcing\Tests\Integration\Subscription\Subscriber\ProfileProcessor; use Patchlevel\EventSourcing\Tests\Integration\Subscription\Subscriber\ProfileProjection; @@ -927,6 +929,122 @@ public function testBlueGreenDeploymentRollback(): void ); } + public function testPipeline(): void + { + $clock = new FrozenClock(new DateTimeImmutable('2021-01-01T00:00:00')); + + $store = new DoctrineDbalStore( + $this->connection, + DefaultEventSerializer::createFromPaths([__DIR__ . '/Events']), + ); + + $targetStore = new StreamDoctrineDbalStore( + $this->projectionConnection, + DefaultEventSerializer::createFromPaths([__DIR__ . '/Events']), + config: ['table_name' => 'new_eventstore'], + ); + + $subscriptionStore = new DoctrineSubscriptionStore( + $this->connection, + $clock, + ); + + $manager = new DefaultRepositoryManager( + new AggregateRootRegistry(['profile' => Profile::class]), + $store, + ); + + $repository = $manager->get(Profile::class); + + $schemaDirector = new DoctrineSchemaDirector( + $this->connection, + new ChainDoctrineSchemaConfigurator([ + $store, + $subscriptionStore, + ]), + ); + + $schemaDirector->create(); + + $engine = new DefaultSubscriptionEngine( + $store, + $subscriptionStore, + new MetadataSubscriberAccessorRepository([new MigrateAggregateToStreamStoreSubscriber($targetStore)]), + ); + + self::assertEquals( + [ + new Subscription( + 'migrate', + 'default', + RunMode::Once, + lastSavedAt: new DateTimeImmutable('2021-01-01T00:00:00'), + ), + ], + $engine->subscriptions(), + ); + + $result = $engine->setup(); + + self::assertEquals([], $result->errors); + + self::assertTrue( + $this->projectionConnection->createSchemaManager()->tableExists('new_eventstore'), + ); + + $profileId = ProfileId::generate(); + $profile = Profile::create($profileId, 'John'); + + for ($i = 1; $i < 1_000; $i++) { + $profile->changeName(sprintf('John %d', $i)); + } + + $repository->save($profile); + + $result = $engine->boot(); + + self::assertEquals(1_000, $result->processedMessages); + + self::assertEquals([], $result->errors); + + self::assertEquals( + [ + new Subscription( + 'migrate', + 'default', + RunMode::Once, + Status::Finished, + 1_000, + lastSavedAt: new DateTimeImmutable('2021-01-01T00:00:00'), + ), + ], + $engine->subscriptions(), + ); + + // target store check + + + $result = $engine->remove(); + self::assertEquals([], $result->errors); + + self::assertEquals( + [ + new Subscription( + 'migrate', + 'default', + RunMode::Once, + Status::New, + lastSavedAt: new DateTimeImmutable('2021-01-01T00:00:00'), + ), + ], + $engine->subscriptions(), + ); + + self::assertFalse( + $this->projectionConnection->createSchemaManager()->tableExists('new_eventstore'), + ); + } + /** @param list $subscriptions */ private static function findSubscription(array $subscriptions, string $id): Subscription { From db8d9560219c02604dc5e844436d15732c8e1def Mon Sep 17 00:00:00 2001 From: David Badura Date: Fri, 18 Oct 2024 13:56:20 +0200 Subject: [PATCH 02/12] rewrite pipeline api --- src/Message/Translator/ChainTranslator.php | 4 +- .../Translator/ExcludeEventTranslator.php | 4 +- .../ExcludeEventWithHeaderTranslator.php | 4 +- .../Translator/FilterEventTranslator.php | 4 +- .../Translator/IncludeEventTranslator.php | 4 +- .../IncludeEventWithHeaderTranslator.php | 4 +- .../RecalculatePlayheadTranslator.php | 4 +- .../Translator/ReplaceEventTranslator.php | 4 +- src/Message/Translator/Translator.php | 4 +- .../Translator/UntilEventTranslator.php | 4 +- .../AggregateToStreamHeaderMiddleware.php | 6 +-- src/Pipeline/Pipeline.php | 39 +++++++++---------- src/Pipeline/Source/InMemorySource.php | 22 ----------- src/Pipeline/Source/Source.php | 13 ------- src/Pipeline/Source/StoreSource.php | 21 ---------- src/Pipeline/Target/EventBusTarget.php | 21 ++++++++++ src/Pipeline/Target/InMemoryTarget.php | 4 +- src/Pipeline/Target/StoreTarget.php | 4 +- src/Pipeline/Target/Target.php | 2 +- ...igrateAggregateToStreamStoreSubscriber.php | 35 ++++++++--------- .../Subscription/SubscriptionTest.php | 2 +- 21 files changed, 75 insertions(+), 134 deletions(-) delete mode 100644 src/Pipeline/Source/InMemorySource.php delete mode 100644 src/Pipeline/Source/Source.php delete mode 100644 src/Pipeline/Source/StoreSource.php create mode 100644 src/Pipeline/Target/EventBusTarget.php diff --git a/src/Message/Translator/ChainTranslator.php b/src/Message/Translator/ChainTranslator.php index 03931c81b..733838f52 100644 --- a/src/Message/Translator/ChainTranslator.php +++ b/src/Message/Translator/ChainTranslator.php @@ -8,9 +8,7 @@ use function array_values; -/** - * @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\ChainMiddleware instead - */ +/** @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\ChainMiddleware instead */ final class ChainTranslator implements Translator { /** @param iterable $translators */ diff --git a/src/Message/Translator/ExcludeEventTranslator.php b/src/Message/Translator/ExcludeEventTranslator.php index aeaaadc9d..9c249d373 100644 --- a/src/Message/Translator/ExcludeEventTranslator.php +++ b/src/Message/Translator/ExcludeEventTranslator.php @@ -6,9 +6,7 @@ use Patchlevel\EventSourcing\Message\Message; -/** - * @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\ExcludeEventMiddleware instead - */ +/** @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\ExcludeEventMiddleware instead */ final class ExcludeEventTranslator implements Translator { /** @param list $classes */ diff --git a/src/Message/Translator/ExcludeEventWithHeaderTranslator.php b/src/Message/Translator/ExcludeEventWithHeaderTranslator.php index fcd928956..2776b8fc8 100644 --- a/src/Message/Translator/ExcludeEventWithHeaderTranslator.php +++ b/src/Message/Translator/ExcludeEventWithHeaderTranslator.php @@ -6,9 +6,7 @@ use Patchlevel\EventSourcing\Message\Message; -/** - * @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\ExcludeEventWithHeaderMiddleware instead - */ +/** @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\ExcludeEventWithHeaderMiddleware instead */ final class ExcludeEventWithHeaderTranslator implements Translator { /** @param class-string $header */ diff --git a/src/Message/Translator/FilterEventTranslator.php b/src/Message/Translator/FilterEventTranslator.php index b93261cb9..aad97330a 100644 --- a/src/Message/Translator/FilterEventTranslator.php +++ b/src/Message/Translator/FilterEventTranslator.php @@ -6,9 +6,7 @@ use Patchlevel\EventSourcing\Message\Message; -/** - * @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\FilterEventMiddleware instead - */ +/** @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\FilterEventMiddleware instead */ final class FilterEventTranslator implements Translator { /** @var callable(object $event):bool */ diff --git a/src/Message/Translator/IncludeEventTranslator.php b/src/Message/Translator/IncludeEventTranslator.php index 4bd1dc230..152b2dfb5 100644 --- a/src/Message/Translator/IncludeEventTranslator.php +++ b/src/Message/Translator/IncludeEventTranslator.php @@ -6,9 +6,7 @@ use Patchlevel\EventSourcing\Message\Message; -/** - * @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\IncludeEventWithHeaderMiddleware instead - */ +/** @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\IncludeEventWithHeaderMiddleware instead */ final class IncludeEventTranslator implements Translator { /** @param list $classes */ diff --git a/src/Message/Translator/IncludeEventWithHeaderTranslator.php b/src/Message/Translator/IncludeEventWithHeaderTranslator.php index 6dcd58b5b..bb47c55ad 100644 --- a/src/Message/Translator/IncludeEventWithHeaderTranslator.php +++ b/src/Message/Translator/IncludeEventWithHeaderTranslator.php @@ -6,9 +6,7 @@ use Patchlevel\EventSourcing\Message\Message; -/** - * @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\IncludeEventWithHeaderMiddleware instead - */ +/** @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\IncludeEventWithHeaderMiddleware instead */ final class IncludeEventWithHeaderTranslator implements Translator { /** @param class-string $header */ diff --git a/src/Message/Translator/RecalculatePlayheadTranslator.php b/src/Message/Translator/RecalculatePlayheadTranslator.php index de3011650..23c5bbd34 100644 --- a/src/Message/Translator/RecalculatePlayheadTranslator.php +++ b/src/Message/Translator/RecalculatePlayheadTranslator.php @@ -11,9 +11,7 @@ use function array_key_exists; -/** - * @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\RecalculatePlayheadMiddleware instead - */ +/** @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\RecalculatePlayheadMiddleware instead */ final class RecalculatePlayheadTranslator implements Translator { /** @var array */ diff --git a/src/Message/Translator/ReplaceEventTranslator.php b/src/Message/Translator/ReplaceEventTranslator.php index d31076175..c6c343fa7 100644 --- a/src/Message/Translator/ReplaceEventTranslator.php +++ b/src/Message/Translator/ReplaceEventTranslator.php @@ -7,9 +7,9 @@ use Patchlevel\EventSourcing\Message\Message; /** - * @template T of object - * * @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\ReplaceEventMiddleware instead + * + * @template T of object */ final class ReplaceEventTranslator implements Translator { diff --git a/src/Message/Translator/Translator.php b/src/Message/Translator/Translator.php index 7450c944f..592c599d7 100644 --- a/src/Message/Translator/Translator.php +++ b/src/Message/Translator/Translator.php @@ -6,9 +6,7 @@ use Patchlevel\EventSourcing\Message\Message; -/** - * @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\Middleware instead - */ +/** @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\Middleware instead */ interface Translator { /** @return list */ diff --git a/src/Message/Translator/UntilEventTranslator.php b/src/Message/Translator/UntilEventTranslator.php index 1d14ca6a3..8779f138b 100644 --- a/src/Message/Translator/UntilEventTranslator.php +++ b/src/Message/Translator/UntilEventTranslator.php @@ -10,9 +10,7 @@ use Patchlevel\EventSourcing\Message\Message; use Patchlevel\EventSourcing\Store\StreamHeader; -/** - * @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\UntilEventMiddleware instead - */ +/** @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\UntilEventMiddleware instead */ final class UntilEventTranslator implements Translator { public function __construct( diff --git a/src/Pipeline/Middleware/AggregateToStreamHeaderMiddleware.php b/src/Pipeline/Middleware/AggregateToStreamHeaderMiddleware.php index 9620a041f..4c136bd92 100644 --- a/src/Pipeline/Middleware/AggregateToStreamHeaderMiddleware.php +++ b/src/Pipeline/Middleware/AggregateToStreamHeaderMiddleware.php @@ -8,9 +8,7 @@ use Patchlevel\EventSourcing\Message\Message; use Patchlevel\EventSourcing\Store\StreamHeader; -/** - * @experimental - */ +/** @experimental */ final class AggregateToStreamHeaderMiddleware implements Middleware { /** @return list */ @@ -29,7 +27,7 @@ public function __invoke(Message $message): array $aggregateHeader->streamName(), $aggregateHeader->playhead, $aggregateHeader->recordedOn, - )) + )), ]; } } diff --git a/src/Pipeline/Pipeline.php b/src/Pipeline/Pipeline.php index bc0cefc18..8fb072c8b 100644 --- a/src/Pipeline/Pipeline.php +++ b/src/Pipeline/Pipeline.php @@ -4,21 +4,24 @@ namespace Patchlevel\EventSourcing\Pipeline; +use Patchlevel\EventSourcing\Message\Message; use Patchlevel\EventSourcing\Pipeline\Middleware\ChainMiddleware; use Patchlevel\EventSourcing\Pipeline\Middleware\Middleware; -use Patchlevel\EventSourcing\Pipeline\Source\Source; use Patchlevel\EventSourcing\Pipeline\Target\Target; +use function array_push; +use function count; +use function is_array; + final class Pipeline { private readonly Middleware $middleware; /** @param list|Middleware $middlewares */ public function __construct( - private readonly Source $source, private readonly Target $target, array|Middleware $middlewares = [], - private readonly int $bufferSize = 1_000, + private readonly float|int $bufferSize = 0, ) { if (is_array($middlewares)) { $this->middleware = new ChainMiddleware($middlewares); @@ -27,34 +30,28 @@ public function __construct( } } - public function run(): void + /** @param iterable $messages */ + public function run(iterable $messages): void { $buffer = []; - foreach ($this->source->load() as $message) { + foreach ($messages as $message) { $result = ($this->middleware)($message); array_push($buffer, ...$result); - if (count($buffer) >= $this->bufferSize) { - $this->target->save(...$result); - $buffer = []; + if (count($buffer) < $this->bufferSize) { + continue; } - } - if (count($buffer) > 0) { $this->target->save(...$buffer); + $buffer = []; } - } - public static function execute( - Source $source, - Target $target, - array|Middleware $middlewares = [], - $bufferSize = 1_000, - ): void - { - $pipeline = new self($source, $target, $middlewares, $bufferSize); - $pipeline->run(); + if ($buffer === []) { + return; + } + + $this->target->save(...$buffer); } -} \ No newline at end of file +} diff --git a/src/Pipeline/Source/InMemorySource.php b/src/Pipeline/Source/InMemorySource.php deleted file mode 100644 index 720050140..000000000 --- a/src/Pipeline/Source/InMemorySource.php +++ /dev/null @@ -1,22 +0,0 @@ - $messages */ - public function __construct( - private readonly array $messages - ) { - } - - /** @return iterable */ - public function load(): iterable - { - return $this->messages; - } -} \ No newline at end of file diff --git a/src/Pipeline/Source/Source.php b/src/Pipeline/Source/Source.php deleted file mode 100644 index 1870b8d78..000000000 --- a/src/Pipeline/Source/Source.php +++ /dev/null @@ -1,13 +0,0 @@ - */ - public function load(): iterable; -} \ No newline at end of file diff --git a/src/Pipeline/Source/StoreSource.php b/src/Pipeline/Source/StoreSource.php deleted file mode 100644 index a21825795..000000000 --- a/src/Pipeline/Source/StoreSource.php +++ /dev/null @@ -1,21 +0,0 @@ -store->load(); - } -} \ No newline at end of file diff --git a/src/Pipeline/Target/EventBusTarget.php b/src/Pipeline/Target/EventBusTarget.php new file mode 100644 index 000000000..7d07f5905 --- /dev/null +++ b/src/Pipeline/Target/EventBusTarget.php @@ -0,0 +1,21 @@ +eventBus->dispatch(...$message); + } +} diff --git a/src/Pipeline/Target/InMemoryTarget.php b/src/Pipeline/Target/InMemoryTarget.php index ad7a102ac..a012176b8 100644 --- a/src/Pipeline/Target/InMemoryTarget.php +++ b/src/Pipeline/Target/InMemoryTarget.php @@ -14,7 +14,7 @@ final class InMemoryTarget implements Target public function save(Message ...$message): void { foreach ($message as $m) { - $this->messages[] = $m; + $this->messages[] = $m; } } @@ -28,4 +28,4 @@ public function clear(): void { $this->messages = []; } -} \ No newline at end of file +} diff --git a/src/Pipeline/Target/StoreTarget.php b/src/Pipeline/Target/StoreTarget.php index ef9e60a9b..1283da453 100644 --- a/src/Pipeline/Target/StoreTarget.php +++ b/src/Pipeline/Target/StoreTarget.php @@ -10,7 +10,7 @@ final class StoreTarget implements Target { public function __construct( - private readonly Store $store + private readonly Store $store, ) { } @@ -18,4 +18,4 @@ public function save(Message ...$message): void { $this->store->save(...$message); } -} \ No newline at end of file +} diff --git a/src/Pipeline/Target/Target.php b/src/Pipeline/Target/Target.php index 2a9be09df..0302a7d53 100644 --- a/src/Pipeline/Target/Target.php +++ b/src/Pipeline/Target/Target.php @@ -9,4 +9,4 @@ interface Target { public function save(Message ...$message): void; -} \ No newline at end of file +} diff --git a/tests/Integration/Subscription/Subscriber/MigrateAggregateToStreamStoreSubscriber.php b/tests/Integration/Subscription/Subscriber/MigrateAggregateToStreamStoreSubscriber.php index 20d2b41bd..bd9a177ac 100644 --- a/tests/Integration/Subscription/Subscriber/MigrateAggregateToStreamStoreSubscriber.php +++ b/tests/Integration/Subscription/Subscriber/MigrateAggregateToStreamStoreSubscriber.php @@ -11,7 +11,6 @@ use Patchlevel\EventSourcing\Message\Message; use Patchlevel\EventSourcing\Pipeline\Middleware\AggregateToStreamHeaderMiddleware; use Patchlevel\EventSourcing\Pipeline\Pipeline; -use Patchlevel\EventSourcing\Pipeline\Source\InMemorySource; use Patchlevel\EventSourcing\Pipeline\Target\StoreTarget; use Patchlevel\EventSourcing\Schema\ChainDoctrineSchemaConfigurator; use Patchlevel\EventSourcing\Schema\DoctrineSchemaDirector; @@ -20,26 +19,32 @@ use Patchlevel\EventSourcing\Subscription\RunMode; use Patchlevel\EventSourcing\Subscription\Subscriber\BatchableSubscriber; +use function count; + +use const INF; + #[Subscriber('migrate', RunMode::Once)] final class MigrateAggregateToStreamStoreSubscriber implements BatchableSubscriber { - private const BATCH_SIZE = 10_000; - private readonly SchemaDirector $schemaDirector; - /** - * @var list - */ + /** @var list */ private array $messages = []; + private readonly Pipeline $pipeline; + public function __construct( private readonly StreamDoctrineDbalStore $targetStore, ) { $this->schemaDirector = new DoctrineSchemaDirector( $targetStore->connection(), - new ChainDoctrineSchemaConfigurator([ - $targetStore, - ]), + new ChainDoctrineSchemaConfigurator([$targetStore]), + ); + + $this->pipeline = new Pipeline( + new StoreTarget($this->targetStore), + new AggregateToStreamHeaderMiddleware(), + INF, ); } @@ -56,15 +61,9 @@ public function beginBatch(): void public function commitBatch(): void { - $messages = $this->messages; - $this->messages = []; + $this->pipeline->run($this->messages); - Pipeline::execute( - new InMemorySource($messages), - new StoreTarget($this->targetStore), - new AggregateToStreamHeaderMiddleware(), - self::BATCH_SIZE * 10, // make sure we have only one batch - ); + $this->messages = []; } public function rollbackBatch(): void @@ -74,7 +73,7 @@ public function rollbackBatch(): void public function forceCommit(): bool { - return count($this->messages) >= self::BATCH_SIZE; + return count($this->messages) >= 10_000; } #[Setup] diff --git a/tests/Integration/Subscription/SubscriptionTest.php b/tests/Integration/Subscription/SubscriptionTest.php index fe5a45676..0557edec0 100644 --- a/tests/Integration/Subscription/SubscriptionTest.php +++ b/tests/Integration/Subscription/SubscriptionTest.php @@ -43,6 +43,7 @@ use function gc_collect_cycles; use function iterator_to_array; +use function sprintf; /** @coversNothing */ final class SubscriptionTest extends TestCase @@ -1023,7 +1024,6 @@ public function testPipeline(): void // target store check - $result = $engine->remove(); self::assertEquals([], $result->errors); From f126e2249363a1c6ac55aa6d92f51f2c35a4eff0 Mon Sep 17 00:00:00 2001 From: David Badura Date: Sat, 19 Oct 2024 14:01:23 +0200 Subject: [PATCH 03/12] add tests --- baseline.xml | 123 +++++++++++ docs/pages/pipeline.md | 67 +----- src/Pipeline/Middleware/ChainMiddleware.php | 10 +- src/Pipeline/Middleware/ClosureMiddleware.php | 23 ++ src/Pipeline/Pipeline.php | 2 +- .../AggregateToStreamHeaderMiddlewareTest.php | 72 ++++++ .../Middleware/ChainMiddlewareTest.php | 43 ++++ .../Middleware/ClosureMiddlewareTest.php | 34 +++ .../Middleware/ExcludeEventMiddlewareTest.php | 48 ++++ .../ExcludeEventWithHeaderMiddlewareTest.php | 49 +++++ .../Middleware/FilterEventMiddlewareTest.php | 52 +++++ .../Middleware/IncludeEventMiddlewareTest.php | 48 ++++ .../IncludeEventWithHeaderMiddlewareTest.php | 49 +++++ .../RecalculatePlayheadMiddlewareTest.php | 145 ++++++++++++ .../Middleware/ReplaceEventMiddlewareTest.php | 73 ++++++ .../Middleware/UntilEventMiddlewareTest.php | 72 ++++++ tests/Unit/Pipeline/PipelineTest.php | 207 ++++++++++++++++++ .../Pipeline/Target/EventBusTargetTest.php | 34 +++ .../Pipeline/Target/InMemoryTargetTest.php | 46 ++++ .../Unit/Pipeline/Target/StoreTargetTest.php | 34 +++ 20 files changed, 1160 insertions(+), 71 deletions(-) create mode 100644 src/Pipeline/Middleware/ClosureMiddleware.php create mode 100644 tests/Unit/Pipeline/Middleware/AggregateToStreamHeaderMiddlewareTest.php create mode 100644 tests/Unit/Pipeline/Middleware/ChainMiddlewareTest.php create mode 100644 tests/Unit/Pipeline/Middleware/ClosureMiddlewareTest.php create mode 100644 tests/Unit/Pipeline/Middleware/ExcludeEventMiddlewareTest.php create mode 100644 tests/Unit/Pipeline/Middleware/ExcludeEventWithHeaderMiddlewareTest.php create mode 100644 tests/Unit/Pipeline/Middleware/FilterEventMiddlewareTest.php create mode 100644 tests/Unit/Pipeline/Middleware/IncludeEventMiddlewareTest.php create mode 100644 tests/Unit/Pipeline/Middleware/IncludeEventWithHeaderMiddlewareTest.php create mode 100644 tests/Unit/Pipeline/Middleware/RecalculatePlayheadMiddlewareTest.php create mode 100644 tests/Unit/Pipeline/Middleware/ReplaceEventMiddlewareTest.php create mode 100644 tests/Unit/Pipeline/Middleware/UntilEventMiddlewareTest.php create mode 100644 tests/Unit/Pipeline/PipelineTest.php create mode 100644 tests/Unit/Pipeline/Target/EventBusTargetTest.php create mode 100644 tests/Unit/Pipeline/Target/InMemoryTargetTest.php create mode 100644 tests/Unit/Pipeline/Target/StoreTargetTest.php diff --git a/baseline.xml b/baseline.xml index 8956382f8..ef6bd6cc0 100644 --- a/baseline.xml +++ b/baseline.xml @@ -37,6 +37,53 @@ + + + + + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -213,6 +260,82 @@ + + + reveal(), + $child2->reveal(), + ])]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + profileId, + ); + }, + )]]> + profileId, + ); + }, + )]]> + + + + + + + + diff --git a/docs/pages/pipeline.md b/docs/pages/pipeline.md index 8fe975d67..18ba2a0bc 100644 --- a/docs/pages/pipeline.md +++ b/docs/pages/pipeline.md @@ -14,11 +14,9 @@ use Patchlevel\EventSourcing\Pipeline\Middleware\ExcludeEventMiddleware; use Patchlevel\EventSourcing\Pipeline\Middleware\RecalculatePlayheadMiddleware; use Patchlevel\EventSourcing\Pipeline\Middleware\ReplaceEventMiddleware; use Patchlevel\EventSourcing\Pipeline\Pipeline; -use Patchlevel\EventSourcing\Pipeline\Source\StoreSource; use Patchlevel\EventSourcing\Pipeline\Target\StoreTarget; $pipeline = new Pipeline( - new StoreSource($oldStore), new StoreTarget($newStore), [ new ExcludeEventMiddleware([PrivacyAdded::class]), @@ -28,6 +26,8 @@ $pipeline = new Pipeline( new RecalculatePlayheadMiddleware(), ] ); + +$pipeline->run($oldStore->load()); ``` !!! danger @@ -53,69 +53,6 @@ There is a source where the data comes from. A target where the data should flow. And any number of middlewares to do something with the data beforehand. -## Source - -The first thing you need is a source of where the data should come from. - -### Store - -The `StoreSource` is the standard source to load all events from the database. - -```php -use Patchlevel\EventSourcing\Pipeline\Source\StoreSource; - -$source = new StoreSource($store); -``` - -### In Memory - -There is an `InMemorySource` that receives the messages in an array. This source can be used to write pipeline tests. - -```php -use Patchlevel\EventSourcing\EventBus\Message; -use Patchlevel\EventSourcing\Pipeline\Source\InMemorySource; - -$source = new InMemorySource([ - new Message( - Profile::class, - '1', - 1, - new ProfileCreated(Email::fromString('david.badura@patchlevel.de')), - ), - // ... -]); -``` - -### Custom Source - -You can also create your own source class. It has to inherit from `Source`. -Here you can, for example, create a migration from another event sourcing system or similar system. - -```php -use Patchlevel\EventSourcing\EventBus\Message; -use Patchlevel\EventSourcing\Pipeline\Source\Source; - -$source = new class implements Source { - /** - * @return Generator - */ - public function load(): Generator - { - yield new Message( - Profile::class, - '1', - 0, - new ProfileCreated('1', ['name' => 'David']) - ); - } - - public function count(): int - { - reutrn 1; - } -} -``` - ## Target After you have a source, you still need the destination of the pipeline. diff --git a/src/Pipeline/Middleware/ChainMiddleware.php b/src/Pipeline/Middleware/ChainMiddleware.php index 50a06d515..bf67db990 100644 --- a/src/Pipeline/Middleware/ChainMiddleware.php +++ b/src/Pipeline/Middleware/ChainMiddleware.php @@ -10,9 +10,9 @@ final class ChainMiddleware implements Middleware { - /** @param iterable $translators */ + /** @param iterable $middlewares */ public function __construct( - private readonly iterable $translators, + private readonly iterable $middlewares, ) { } @@ -21,7 +21,7 @@ public function __invoke(Message $message): array { $messages = [$message]; - foreach ($this->translators as $middleware) { + foreach ($this->middlewares as $middleware) { $messages = $this->process($middleware, $messages); } @@ -33,12 +33,12 @@ public function __invoke(Message $message): array * * @return list */ - private function process(Middleware $translator, array $messages): array + private function process(Middleware $middleware, array $messages): array { $result = []; foreach ($messages as $message) { - $result += $translator($message); + $result += $middleware($message); } return array_values($result); diff --git a/src/Pipeline/Middleware/ClosureMiddleware.php b/src/Pipeline/Middleware/ClosureMiddleware.php new file mode 100644 index 000000000..339561251 --- /dev/null +++ b/src/Pipeline/Middleware/ClosureMiddleware.php @@ -0,0 +1,23 @@ + $callable */ + public function __construct( + private readonly Closure $callable, + ) { + } + + /** @return list */ + public function __invoke(Message $message): array + { + return ($this->callable)($message); + } +} diff --git a/src/Pipeline/Pipeline.php b/src/Pipeline/Pipeline.php index 8fb072c8b..af78d8523 100644 --- a/src/Pipeline/Pipeline.php +++ b/src/Pipeline/Pipeline.php @@ -21,7 +21,7 @@ final class Pipeline public function __construct( private readonly Target $target, array|Middleware $middlewares = [], - private readonly float|int $bufferSize = 0, + private readonly float|int $bufferSize = 1_000, ) { if (is_array($middlewares)) { $this->middleware = new ChainMiddleware($middlewares); diff --git a/tests/Unit/Pipeline/Middleware/AggregateToStreamHeaderMiddlewareTest.php b/tests/Unit/Pipeline/Middleware/AggregateToStreamHeaderMiddlewareTest.php new file mode 100644 index 000000000..22ff18310 --- /dev/null +++ b/tests/Unit/Pipeline/Middleware/AggregateToStreamHeaderMiddlewareTest.php @@ -0,0 +1,72 @@ +withHeader($aggregateHeader); + + $middleware = new AggregateToStreamHeaderMiddleware(); + + $result = $middleware($message); + + self::assertCount(1, $result); + + $message = $result[0]; + + self::assertFalse($message->hasHeader(AggregateHeader::class)); + self::assertTrue($message->hasHeader(StreamHeader::class)); + + $streamHeader = $message->header(StreamHeader::class); + + self::assertEquals($aggregateHeader->recordedOn, $streamHeader->recordedOn); + self::assertEquals('profile-1', $streamHeader->streamName); + self::assertEquals(1, $streamHeader->playhead); + } +} diff --git a/tests/Unit/Pipeline/Middleware/ChainMiddlewareTest.php b/tests/Unit/Pipeline/Middleware/ChainMiddlewareTest.php new file mode 100644 index 000000000..8f6b0ea36 --- /dev/null +++ b/tests/Unit/Pipeline/Middleware/ChainMiddlewareTest.php @@ -0,0 +1,43 @@ +prophesize(Middleware::class); + $child1->__invoke($message)->willReturn([$message])->shouldBeCalled(); + + $child2 = $this->prophesize(Middleware::class); + $child2->__invoke($message)->willReturn([$message])->shouldBeCalled(); + + $middleware = new ChainMiddleware([ + $child1->reveal(), + $child2->reveal(), + ]); + + $middleware($message); + } +} diff --git a/tests/Unit/Pipeline/Middleware/ClosureMiddlewareTest.php b/tests/Unit/Pipeline/Middleware/ClosureMiddlewareTest.php new file mode 100644 index 000000000..c81494a78 --- /dev/null +++ b/tests/Unit/Pipeline/Middleware/ClosureMiddlewareTest.php @@ -0,0 +1,34 @@ +withHeader(new ArchivedHeader()); + + $result = $middleware($message); + + self::assertSame([], $result); + } + + public function testIncludeEvent(): void + { + $middleware = new ExcludeEventWithHeaderMiddleware(ArchivedHeader::class); + + $message = Message::create( + new ProfileCreated( + ProfileId::fromString('1'), + Email::fromString('hallo@patchlevel.de'), + ), + ); + + $result = $middleware($message); + + self::assertSame([$message], $result); + } +} diff --git a/tests/Unit/Pipeline/Middleware/FilterEventMiddlewareTest.php b/tests/Unit/Pipeline/Middleware/FilterEventMiddlewareTest.php new file mode 100644 index 000000000..5ddc0f7e1 --- /dev/null +++ b/tests/Unit/Pipeline/Middleware/FilterEventMiddlewareTest.php @@ -0,0 +1,52 @@ +withHeader(new ArchivedHeader()); + + $result = $middleware($message); + + self::assertSame([$message], $result); + } +} diff --git a/tests/Unit/Pipeline/Middleware/RecalculatePlayheadMiddlewareTest.php b/tests/Unit/Pipeline/Middleware/RecalculatePlayheadMiddlewareTest.php new file mode 100644 index 000000000..46d7c272b --- /dev/null +++ b/tests/Unit/Pipeline/Middleware/RecalculatePlayheadMiddlewareTest.php @@ -0,0 +1,145 @@ +withHeader(new AggregateHeader('profile', '1', 5, new DateTimeImmutable())); + + $result = $middleware($message); + + self::assertCount(1, $result); + self::assertSame('profile', $result[0]->header(AggregateHeader::class)->aggregateName); + self::assertSame(1, $result[0]->header(AggregateHeader::class)->playhead); + } + + public function testRecalculatePlayheadWithSamePlayhead(): void + { + $middleware = new RecalculatePlayheadMiddleware(); + + $event = new ProfileCreated( + ProfileId::fromString('1'), + Email::fromString('hallo@patchlevel.de'), + ); + + $message = Message::create($event) + ->withHeader(new AggregateHeader('profile', '1', 1, new DateTimeImmutable())); + + $result = $middleware($message); + + self::assertSame([$message], $result); + } + + public function testRecalculateMultipleMessages(): void + { + $middleware = new RecalculatePlayheadMiddleware(); + + $event = new ProfileCreated( + ProfileId::fromString('1'), + Email::fromString('hallo@patchlevel.de'), + ); + + $message = Message::create($event) + ->withHeader(new AggregateHeader('profile', '1', 5, new DateTimeImmutable())); + $result = $middleware($message); + + self::assertCount(1, $result); + self::assertSame('profile', $result[0]->header(AggregateHeader::class)->aggregateName); + self::assertSame(1, $result[0]->header(AggregateHeader::class)->playhead); + + $message = Message::create($event) + ->withHeader(new AggregateHeader('profile', '1', 8, new DateTimeImmutable())); + + $result = $middleware($message); + + self::assertCount(1, $result); + self::assertSame('profile', $result[0]->header(AggregateHeader::class)->aggregateName); + self::assertSame(2, $result[0]->header(AggregateHeader::class)->playhead); + } + + public function testReset(): void + { + $middleware = new RecalculatePlayheadMiddleware(); + + $event = new ProfileCreated( + ProfileId::fromString('1'), + Email::fromString('hallo@patchlevel.de'), + ); + + $message = Message::create($event) + ->withHeader(new AggregateHeader('profile', '1', 5, new DateTimeImmutable())); + $result = $middleware($message); + + self::assertCount(1, $result); + self::assertSame('profile', $result[0]->header(AggregateHeader::class)->aggregateName); + self::assertSame(1, $result[0]->header(AggregateHeader::class)->playhead); + + $message = Message::create($event) + ->withHeader(new AggregateHeader('profile', '1', 8, new DateTimeImmutable())); + + $middleware->reset(); + $result = $middleware($message); + + self::assertCount(1, $result); + self::assertSame('profile', $result[0]->header(AggregateHeader::class)->aggregateName); + self::assertSame(1, $result[0]->header(AggregateHeader::class)->playhead); + } + + public function testRecalculatePlayheadWithStreamHeader(): void + { + $middleware = new RecalculatePlayheadMiddleware(); + + $event = new ProfileCreated( + ProfileId::fromString('1'), + Email::fromString('hallo@patchlevel.de'), + ); + + $message = Message::create($event) + ->withHeader(new StreamHeader('profile-1', 5, new DateTimeImmutable())); + + $result = $middleware($message); + + self::assertCount(1, $result); + self::assertSame('profile-1', $result[0]->header(StreamHeader::class)->streamName); + self::assertSame(1, $result[0]->header(StreamHeader::class)->playhead); + } + + public function testRecalculateWithoutHeader(): void + { + $middleware = new RecalculatePlayheadMiddleware(); + + $event = new ProfileCreated( + ProfileId::fromString('1'), + Email::fromString('hallo@patchlevel.de'), + ); + + $message = Message::create($event); + + $result = $middleware($message); + + self::assertSame([$message], $result); + } +} diff --git a/tests/Unit/Pipeline/Middleware/ReplaceEventMiddlewareTest.php b/tests/Unit/Pipeline/Middleware/ReplaceEventMiddlewareTest.php new file mode 100644 index 000000000..d7098a9f0 --- /dev/null +++ b/tests/Unit/Pipeline/Middleware/ReplaceEventMiddlewareTest.php @@ -0,0 +1,73 @@ +profileId, + ); + }, + ); + + $message = new Message( + new ProfileCreated( + ProfileId::fromString('1'), + Email::fromString('hallo@patchlevel.de'), + ), + ); + + $result = $middleware($message); + + self::assertCount(1, $result); + + $event = $result[0]->event(); + + self::assertInstanceOf(ProfileVisited::class, $event); + } + + public function testReplaceInvalidClass(): void + { + /** @psalm-suppress InvalidArgument */ + $middleware = new ReplaceEventMiddleware( + MessagePublished::class, + static function (ProfileCreated $event) { + return new ProfileVisited( + $event->profileId, + ); + }, + ); + + $message = new Message( + new ProfileCreated( + ProfileId::fromString('1'), + Email::fromString('hallo@patchlevel.de'), + ), + ); + + $result = $middleware($message); + + self::assertCount(1, $result); + + $event = $result[0]->event(); + + self::assertInstanceOf(ProfileCreated::class, $event); + } +} diff --git a/tests/Unit/Pipeline/Middleware/UntilEventMiddlewareTest.php b/tests/Unit/Pipeline/Middleware/UntilEventMiddlewareTest.php new file mode 100644 index 000000000..3d6f087d3 --- /dev/null +++ b/tests/Unit/Pipeline/Middleware/UntilEventMiddlewareTest.php @@ -0,0 +1,72 @@ +withHeader(new AggregateHeader('pofile', '1', 1, new DateTimeImmutable('2020-02-01 00:00:00'))); + + $result = $middleware($message); + + self::assertSame([$message], $result); + } + + public function testNegative(): void + { + $until = new DateTimeImmutable('2020-01-01 00:00:00'); + + $middleware = new UntilEventMiddleware($until); + + $message = Message::create( + new ProfileCreated( + ProfileId::fromString('1'), + Email::fromString('info@patchlevel.de'), + ), + )->withHeader(new AggregateHeader('pofile', '1', 1, new DateTimeImmutable('2020-02-01 00:00:00'))); + + $result = $middleware($message); + + self::assertSame([], $result); + } + + public function testWithoutHeader(): void + { + $until = new DateTimeImmutable('2020-01-01 00:00:00'); + + $middleware = new UntilEventMiddleware($until); + + $event = new ProfileCreated( + ProfileId::fromString('1'), + Email::fromString('hallo@patchlevel.de'), + ); + + $message = Message::create($event); + + $result = $middleware($message); + + self::assertSame([$message], $result); + } +} diff --git a/tests/Unit/Pipeline/PipelineTest.php b/tests/Unit/Pipeline/PipelineTest.php new file mode 100644 index 000000000..8adab8256 --- /dev/null +++ b/tests/Unit/Pipeline/PipelineTest.php @@ -0,0 +1,207 @@ +messages(); + + $target = new InMemoryTarget(); + $pipeline = new Pipeline($target); + + $pipeline->run($messages); + + self::assertSame($messages, $target->messages()); + } + + public function testPipelineWithOneMiddleware(): void + { + $messages = $this->messages(); + + $target = new InMemoryTarget(); + $pipeline = new Pipeline( + $target, + new ExcludeEventMiddleware([ProfileCreated::class]), + ); + + $pipeline->run($messages); + + $resultMessages = $target->messages(); + + self::assertCount(3, $resultMessages); + + self::assertInstanceOf(ProfileVisited::class, $resultMessages[0]->event()); + self::assertSame('1', $resultMessages[0]->header(AggregateHeader::class)->aggregateId); + + self::assertInstanceOf(ProfileVisited::class, $resultMessages[1]->event()); + self::assertSame('1', $resultMessages[1]->header(AggregateHeader::class)->aggregateId); + + self::assertInstanceOf(ProfileVisited::class, $resultMessages[2]->event()); + self::assertSame('2', $resultMessages[2]->header(AggregateHeader::class)->aggregateId); + } + + public function testPipelineWithMiddleware(): void + { + $messages = $this->messages(); + + $target = new InMemoryTarget(); + $pipeline = new Pipeline( + $target, + [ + new ExcludeEventMiddleware([ProfileCreated::class]), + new RecalculatePlayheadMiddleware(), + ], + ); + + $pipeline->run($messages); + + $resultMessages = $target->messages(); + + self::assertCount(3, $resultMessages); + + self::assertInstanceOf(ProfileVisited::class, $resultMessages[0]->event()); + self::assertSame('1', $resultMessages[0]->header(AggregateHeader::class)->aggregateId); + self::assertSame(1, $resultMessages[0]->header(AggregateHeader::class)->playhead); + + self::assertInstanceOf(ProfileVisited::class, $resultMessages[1]->event()); + self::assertSame('1', $resultMessages[1]->header(AggregateHeader::class)->aggregateId); + self::assertSame(2, $resultMessages[1]->header(AggregateHeader::class)->playhead); + + self::assertInstanceOf(ProfileVisited::class, $resultMessages[2]->event()); + self::assertSame('2', $resultMessages[2]->header(AggregateHeader::class)->aggregateId); + self::assertSame(1, $resultMessages[2]->header(AggregateHeader::class)->playhead); + } + + public function testBatching(): void + { + $messages = $this->messages(); + + $target = $this->prophesize(Target::class); + + $target->save($messages[0], $messages[1], $messages[2])->shouldBeCalled(); + $target->save($messages[3], $messages[4])->shouldBeCalled(); + + $pipeline = new Pipeline($target->reveal(), bufferSize: 3); + + $pipeline->run($messages); + } + + public function testBatchingInf(): void + { + $messages = $this->messages(); + + $target = $this->prophesize(Target::class); + $target->save(...$messages)->shouldBeCalled(); + + $pipeline = new Pipeline($target->reveal(), bufferSize: INF); + + $pipeline->run($messages); + } + + public function testBatchingNothing(): void + { + $messages = $this->messages(); + + $target = $this->prophesize(Target::class); + + $target->save($messages[0])->shouldBeCalled(); + $target->save($messages[1])->shouldBeCalled(); + $target->save($messages[2])->shouldBeCalled(); + $target->save($messages[3])->shouldBeCalled(); + $target->save($messages[4])->shouldBeCalled(); + + $pipeline = new Pipeline($target->reveal(), bufferSize: 0); + + $pipeline->run($messages); + } + + /** @return list */ + private function messages(): array + { + return [ + Message::create( + new ProfileCreated( + ProfileId::fromString('1'), + Email::fromString('hallo@patchlevel.de'), + ), + ) + ->withHeader(new AggregateHeader( + 'profile', + '1', + 1, + new DateTimeImmutable(), + )), + Message::create( + new ProfileVisited( + ProfileId::fromString('1'), + ), + ) + ->withHeader(new AggregateHeader( + 'profile', + '1', + 2, + new DateTimeImmutable(), + )), + Message::create( + new ProfileVisited( + ProfileId::fromString('1'), + ), + ) + ->withHeader(new AggregateHeader( + 'profile', + '1', + 3, + new DateTimeImmutable(), + )), + + Message::create( + new ProfileCreated( + ProfileId::fromString('2'), + Email::fromString('hallo@patchlevel.de'), + ), + ) + ->withHeader(new AggregateHeader( + 'profile', + '2', + 1, + new DateTimeImmutable(), + )), + + Message::create( + new ProfileVisited( + ProfileId::fromString('2'), + ), + ) + ->withHeader(new AggregateHeader( + 'profile', + '2', + 2, + new DateTimeImmutable(), + )), + ]; + } +} diff --git a/tests/Unit/Pipeline/Target/EventBusTargetTest.php b/tests/Unit/Pipeline/Target/EventBusTargetTest.php new file mode 100644 index 000000000..c9f7fd63c --- /dev/null +++ b/tests/Unit/Pipeline/Target/EventBusTargetTest.php @@ -0,0 +1,34 @@ +prophesize(EventBus::class); + $pipelineStore->dispatch($message)->shouldBeCalled(); + + $storeTarget = new EventBusTarget($pipelineStore->reveal()); + + $storeTarget->save($message); + } +} diff --git a/tests/Unit/Pipeline/Target/InMemoryTargetTest.php b/tests/Unit/Pipeline/Target/InMemoryTargetTest.php new file mode 100644 index 000000000..e5aadb306 --- /dev/null +++ b/tests/Unit/Pipeline/Target/InMemoryTargetTest.php @@ -0,0 +1,46 @@ +save($message); + + $messages = $inMemoryTarget->messages(); + + self::assertSame([$message], $messages); + } + + public function testClear(): void + { + $inMemoryTarget = new InMemoryTarget(); + + $message = new Message( + new ProfileCreated(ProfileId::fromString('1'), Email::fromString('foo@test.com')), + ); + + $inMemoryTarget->save($message); + $inMemoryTarget->clear(); + + $messages = $inMemoryTarget->messages(); + + self::assertSame([], $messages); + } +} diff --git a/tests/Unit/Pipeline/Target/StoreTargetTest.php b/tests/Unit/Pipeline/Target/StoreTargetTest.php new file mode 100644 index 000000000..187ac642a --- /dev/null +++ b/tests/Unit/Pipeline/Target/StoreTargetTest.php @@ -0,0 +1,34 @@ +prophesize(Store::class); + $pipelineStore->save($message)->shouldBeCalled(); + + $storeTarget = new StoreTarget($pipelineStore->reveal()); + + $storeTarget->save($message); + } +} From 2ce9348a061254d73904ec46452ae6cd69d11bd7 Mon Sep 17 00:00:00 2001 From: David Badura Date: Sat, 19 Oct 2024 16:57:04 +0200 Subject: [PATCH 04/12] add pipe class for inline processing --- docs/pages/pipeline.md | 28 ++--- src/Pipeline/Pipe.php | 67 ++++++++++ src/Pipeline/Pipeline.php | 22 ++-- tests/Unit/Pipeline/PipeTest.php | 205 +++++++++++++++++++++++++++++++ 4 files changed, 296 insertions(+), 26 deletions(-) create mode 100644 src/Pipeline/Pipe.php create mode 100644 tests/Unit/Pipeline/PipeTest.php diff --git a/docs/pages/pipeline.md b/docs/pages/pipeline.md index 18ba2a0bc..1aaaefd27 100644 --- a/docs/pages/pipeline.md +++ b/docs/pages/pipeline.md @@ -1,30 +1,28 @@ # Pipeline / Anti Corruption Layer -A store is immutable, i.e. it cannot be changed afterwards. -This includes both manipulating events and deleting them. +Sobald man den Fall hat, dass man eine Menge an Events von A nach B fliesen lassen möchte, +und evtl sogar dabei noch einfluss auf die Events nehmen möchte in Form von Filtern oder Manipulationen, +dann kommt die Pipeline ins Spiel. -Instead, you can duplicate the store and manipulate the events in the process. -Thus the old store remains untouched and you can test the new store beforehand, -whether the migration worked. +Es gibt mehrere Situationen, in denen eine Pipeline sinnvoll ist: -In this example the event `PrivacyAdded` is removed and the event `OldVisited` is replaced by `NewVisited`: +* Migration vom event store und deren events. +* Als Anti Corruption Layer beim Publizieren von Events an andere Systeme +* Oder als Anti Corruption Layer beim Importieren von Events aus anderen Systemen + +In diesem Beispiel wird eine Pipeline verwendet, +um einen neuen Event Store zu erstellen und dabei alte Events durch neue zu ersetzen. ```php -use Patchlevel\EventSourcing\Pipeline\Middleware\ExcludeEventMiddleware; -use Patchlevel\EventSourcing\Pipeline\Middleware\RecalculatePlayheadMiddleware; use Patchlevel\EventSourcing\Pipeline\Middleware\ReplaceEventMiddleware; use Patchlevel\EventSourcing\Pipeline\Pipeline; use Patchlevel\EventSourcing\Pipeline\Target\StoreTarget; $pipeline = new Pipeline( new StoreTarget($newStore), - [ - new ExcludeEventMiddleware([PrivacyAdded::class]), - new ReplaceEventMiddleware(OldVisited::class, static function (OldVisited $oldVisited) { + new ReplaceEventMiddleware(OldVisited::class, static function (OldVisited $oldVisited) { return new NewVisited($oldVisited->profileId()); }), - new RecalculatePlayheadMiddleware(), - ] ); $pipeline->run($oldStore->load()); @@ -32,8 +30,8 @@ $pipeline->run($oldStore->load()); !!! danger - Under no circumstances may the same store be used that is used for the source. - Otherwise the store will be broken afterwards! + Unter keinen Umständen darf der selbe Store als Target verwendet werden + wie der, der als Source verwendet wird. Ansonsten wird der Store danach kaputt sein! The pipeline can also be used to create or rebuild a projection: diff --git a/src/Pipeline/Pipe.php b/src/Pipeline/Pipe.php new file mode 100644 index 000000000..688b9e628 --- /dev/null +++ b/src/Pipeline/Pipe.php @@ -0,0 +1,67 @@ + */ +final class Pipe implements IteratorAggregate +{ + /** + * @param iterable $messages + * @param list $middlewares + */ + public function __construct( + private readonly iterable $messages, + private readonly array $middlewares = [], + ) { + } + + public function appendMiddleware(Middleware $middleware): self + { + return new self( + $this->messages, + [...$this->middlewares, $middleware], + ); + } + + public function prependMiddleware(Middleware $middleware): self + { + return new self( + $this->messages, + [$middleware, ...$this->middlewares], + ); + } + + /** @return Traversable */ + public function getIterator(): Traversable + { + return $this->createGenerator( + $this->messages, + new ChainMiddleware($this->middlewares), + ); + } + + /** + * @param iterable $messages + * + * @return Generator + */ + private function createGenerator(iterable $messages, Middleware $middleware): Generator + { + foreach ($messages as $message) { + $result = $middleware($message); + + foreach ($result as $m) { + yield $m; + } + } + } +} diff --git a/src/Pipeline/Pipeline.php b/src/Pipeline/Pipeline.php index af78d8523..bb8b528e6 100644 --- a/src/Pipeline/Pipeline.php +++ b/src/Pipeline/Pipeline.php @@ -5,17 +5,14 @@ namespace Patchlevel\EventSourcing\Pipeline; use Patchlevel\EventSourcing\Message\Message; -use Patchlevel\EventSourcing\Pipeline\Middleware\ChainMiddleware; use Patchlevel\EventSourcing\Pipeline\Middleware\Middleware; use Patchlevel\EventSourcing\Pipeline\Target\Target; -use function array_push; use function count; -use function is_array; final class Pipeline { - private readonly Middleware $middleware; + private readonly array $middlewares; /** @param list|Middleware $middlewares */ public function __construct( @@ -23,22 +20,25 @@ public function __construct( array|Middleware $middlewares = [], private readonly float|int $bufferSize = 1_000, ) { - if (is_array($middlewares)) { - $this->middleware = new ChainMiddleware($middlewares); + if ($middlewares instanceof Middleware) { + $this->middlewares = [$middlewares]; } else { - $this->middleware = $middlewares; + $this->middlewares = $middlewares; } } /** @param iterable $messages */ public function run(iterable $messages): void { - $buffer = []; + $stream = new Pipe( + $messages, + $this->middlewares, + ); - foreach ($messages as $message) { - $result = ($this->middleware)($message); + $buffer = []; - array_push($buffer, ...$result); + foreach ($stream as $message) { + $buffer[] = $message; if (count($buffer) < $this->bufferSize) { continue; diff --git a/tests/Unit/Pipeline/PipeTest.php b/tests/Unit/Pipeline/PipeTest.php new file mode 100644 index 000000000..89906eb97 --- /dev/null +++ b/tests/Unit/Pipeline/PipeTest.php @@ -0,0 +1,205 @@ +messages(); + + $stream = new Pipe($messages); + + $resultMessages = iterator_to_array($stream); + + self::assertSame($messages, $resultMessages); + } + + public function testWithMiddlewares(): void + { + $messages = $this->messages(); + + $stream = new Pipe( + $messages, + [ + new ExcludeEventMiddleware([ProfileCreated::class]), + new RecalculatePlayheadMiddleware(), + ], + ); + + $resultMessages = iterator_to_array($stream); + + self::assertCount(3, $resultMessages); + + self::assertInstanceOf(ProfileVisited::class, $resultMessages[0]->event()); + self::assertSame('1', $resultMessages[0]->header(AggregateHeader::class)->aggregateId); + self::assertSame(1, $resultMessages[0]->header(AggregateHeader::class)->playhead); + + self::assertInstanceOf(ProfileVisited::class, $resultMessages[1]->event()); + self::assertSame('1', $resultMessages[1]->header(AggregateHeader::class)->aggregateId); + self::assertSame(2, $resultMessages[1]->header(AggregateHeader::class)->playhead); + + self::assertInstanceOf(ProfileVisited::class, $resultMessages[2]->event()); + self::assertSame('2', $resultMessages[2]->header(AggregateHeader::class)->aggregateId); + self::assertSame(1, $resultMessages[2]->header(AggregateHeader::class)->playhead); + } + + public function testAppendMiddleware(): void + { + $messages = $this->messages(); + + $stream = new Pipe( + $messages, + [ + new ExcludeEventMiddleware([ProfileCreated::class]), + ], + ); + + $stream = $stream->appendMiddleware( + new RecalculatePlayheadMiddleware(), + ); + + $resultMessages = iterator_to_array($stream); + + self::assertCount(3, $resultMessages); + + self::assertInstanceOf(ProfileVisited::class, $resultMessages[0]->event()); + self::assertSame('1', $resultMessages[0]->header(AggregateHeader::class)->aggregateId); + self::assertSame(1, $resultMessages[0]->header(AggregateHeader::class)->playhead); + + self::assertInstanceOf(ProfileVisited::class, $resultMessages[1]->event()); + self::assertSame('1', $resultMessages[1]->header(AggregateHeader::class)->aggregateId); + self::assertSame(2, $resultMessages[1]->header(AggregateHeader::class)->playhead); + + self::assertInstanceOf(ProfileVisited::class, $resultMessages[2]->event()); + self::assertSame('2', $resultMessages[2]->header(AggregateHeader::class)->aggregateId); + self::assertSame(1, $resultMessages[2]->header(AggregateHeader::class)->playhead); + } + + public function testPrependMiddleware(): void + { + $messages = $this->messages(); + + $stream = new Pipe( + $messages, + [ + new RecalculatePlayheadMiddleware(), + ], + ); + + $stream = $stream->prependMiddleware( + new ExcludeEventMiddleware([ProfileCreated::class]), + ); + + $resultMessages = iterator_to_array($stream); + + self::assertCount(3, $resultMessages); + + self::assertInstanceOf(ProfileVisited::class, $resultMessages[0]->event()); + self::assertSame('1', $resultMessages[0]->header(AggregateHeader::class)->aggregateId); + self::assertSame(1, $resultMessages[0]->header(AggregateHeader::class)->playhead); + + self::assertInstanceOf(ProfileVisited::class, $resultMessages[1]->event()); + self::assertSame('1', $resultMessages[1]->header(AggregateHeader::class)->aggregateId); + self::assertSame(2, $resultMessages[1]->header(AggregateHeader::class)->playhead); + + self::assertInstanceOf(ProfileVisited::class, $resultMessages[2]->event()); + self::assertSame('2', $resultMessages[2]->header(AggregateHeader::class)->aggregateId); + self::assertSame(1, $resultMessages[2]->header(AggregateHeader::class)->playhead); + } + + /** @return list */ + private function messages(): array + { + return [ + Message::create( + new ProfileCreated( + ProfileId::fromString('1'), + Email::fromString('hallo@patchlevel.de'), + ), + ) + ->withHeader(new AggregateHeader( + 'profile', + '1', + 1, + new DateTimeImmutable(), + )), + Message::create( + new ProfileVisited( + ProfileId::fromString('1'), + ), + ) + ->withHeader(new AggregateHeader( + 'profile', + '1', + 2, + new DateTimeImmutable(), + )), + Message::create( + new ProfileVisited( + ProfileId::fromString('1'), + ), + ) + ->withHeader(new AggregateHeader( + 'profile', + '1', + 3, + new DateTimeImmutable(), + )), + + Message::create( + new ProfileCreated( + ProfileId::fromString('2'), + Email::fromString('hallo@patchlevel.de'), + ), + ) + ->withHeader(new AggregateHeader( + 'profile', + '2', + 1, + new DateTimeImmutable(), + )), + + Message::create( + new ProfileVisited( + ProfileId::fromString('2'), + ), + ) + ->withHeader(new AggregateHeader( + 'profile', + '2', + 2, + new DateTimeImmutable(), + )), + ]; + } +} From d2128becfe94a49b28fe1a12b1057174ff79db8a Mon Sep 17 00:00:00 2001 From: David Badura Date: Sun, 20 Oct 2024 13:22:40 +0200 Subject: [PATCH 05/12] add state processor --- docs/pages/pipeline.md | 86 ++++++++------- src/Pipeline/StateProcessor.php | 178 ++++++++++++++++++++++++++++++++ 2 files changed, 220 insertions(+), 44 deletions(-) create mode 100644 src/Pipeline/StateProcessor.php diff --git a/docs/pages/pipeline.md b/docs/pages/pipeline.md index 1aaaefd27..69ad8ce6a 100644 --- a/docs/pages/pipeline.md +++ b/docs/pages/pipeline.md @@ -1,59 +1,69 @@ # Pipeline / Anti Corruption Layer -Sobald man den Fall hat, dass man eine Menge an Events von A nach B fliesen lassen möchte, -und evtl sogar dabei noch einfluss auf die Events nehmen möchte in Form von Filtern oder Manipulationen, -dann kommt die Pipeline ins Spiel. +As soon as you have the case where you want to flow a lot of events from A to B, +and maybe even influence the events in the form of filters or manipulations, +then the pipeline comes into play. -Es gibt mehrere Situationen, in denen eine Pipeline sinnvoll ist: +There are several situations in which a pipeline makes sense: -* Migration vom event store und deren events. -* Als Anti Corruption Layer beim Publizieren von Events an andere Systeme -* Oder als Anti Corruption Layer beim Importieren von Events aus anderen Systemen +* Migration of the event store and its events. +* As an anti-corruption layer when publishing events to other systems. +* Or as an anti-corruption layer when importing events from other systems. -In diesem Beispiel wird eine Pipeline verwendet, -um einen neuen Event Store zu erstellen und dabei alte Events durch neue zu ersetzen. +## Pipe ```php use Patchlevel\EventSourcing\Pipeline\Middleware\ReplaceEventMiddleware; use Patchlevel\EventSourcing\Pipeline\Pipeline; use Patchlevel\EventSourcing\Pipeline\Target\StoreTarget; +use Patchlevel\EventSourcing\Tests\Unit\Fixture\Profile -$pipeline = new Pipeline( - new StoreTarget($newStore), - new ReplaceEventMiddleware(OldVisited::class, static function (OldVisited $oldVisited) { - return new NewVisited($oldVisited->profileId()); - }), +$pipe = new Pipe( + $oldStore->load(), + new ReplaceEventMiddleware(OldVisited::class, static function (OldVisited $oldVisited) { + return new NewVisited($oldVisited->profileId()); + }), ); -$pipeline->run($oldStore->load()); -``` +$unwarp = function (iterable $messages) { + foreach ($messages as $message) { + yield $message->event(); + } +}; -!!! danger +Profile::createFromEvents($unwarp($pipe)); +``` - Unter keinen Umständen darf der selbe Store als Target verwendet werden - wie der, der als Source verwendet wird. Ansonsten wird der Store danach kaputt sein! +## Pipeline -The pipeline can also be used to create or rebuild a projection: +The pipeline uses the pipe internally and is an abstraction layer on top of it. +The pipeline is used when it comes to moving a lot of events from A to B. +A `target` must be defined where the events should flow to. +You can also define whether buffering should take place or not. ```php +use Patchlevel\EventSourcing\Pipeline\Middleware\ReplaceEventMiddleware; use Patchlevel\EventSourcing\Pipeline\Pipeline; -use Patchlevel\EventSourcing\Pipeline\Source\StoreSource; -use Patchlevel\EventSourcing\Pipeline\Target\ProjectionTarget; +use Patchlevel\EventSourcing\Pipeline\Target\StoreTarget; $pipeline = new Pipeline( - new StoreSource($store), - new ProjectionTarget($projection) + new StoreTarget($newStore), + new ReplaceEventMiddleware(OldVisited::class, static function (OldVisited $oldVisited) { + return new NewVisited($oldVisited->profileId()); + }), ); + +$pipeline->run($oldStore->load()); ``` -The principle remains the same. -There is a source where the data comes from. -A target where the data should flow. -And any number of middlewares to do something with the data beforehand. +!!! danger + + Under no circumstances should the same store be used as target as the one used as source. + Otherwise the store will be broken afterwards! ## Target -After you have a source, you still need the destination of the pipeline. + ### Store @@ -75,27 +85,15 @@ $target = new StoreTarget($store); It does not matter whether the previous store was a SingleTable or a MultiTable. You can switch back and forth between both store types using the pipeline. -### Projection +### Event Bus A projection can also be used as a target. For example, to set up a new projection or to build a new projection. ```php -use Patchlevel\EventSourcing\Pipeline\Target\ProjectionTarget; - -$target = new ProjectionTarget($projection); -``` - -### Projection Handler - -If you want to build or create all projections from scratch, -then you can also use the ProjectionRepositoryTarget. -In this, the individual projections are iterated and the events are then passed on. - -```php -use Patchlevel\EventSourcing\Pipeline\Target\ProjectionHandlerTarget; +use Patchlevel\EventSourcing\Pipeline\Target\EventBusTarget; -$target = new ProjectionHandlerTarget($projectionHandler); +$target = new EventBusTarget($projection); ``` ### In Memory diff --git a/src/Pipeline/StateProcessor.php b/src/Pipeline/StateProcessor.php new file mode 100644 index 000000000..92b09ca3b --- /dev/null +++ b/src/Pipeline/StateProcessor.php @@ -0,0 +1,178 @@ + + * @template OUT of array = STATE + */ +final class StateProcessor +{ + /** + * @var STATE + */ + private array $state = []; + + /** + * @var array> + */ + private array $handlers = []; + + /** + * @var list + */ + private array $anyHandlers = []; + + /** + * @var (Closure(STATE): OUT)|null + */ + private Closure|null $finalizeHandler = null; + + /** + * @var list + */ + private array $middlewares = []; + + /** + * @param STATE $state + * + * @return $this + */ + public function initState(array $state): self + { + $this->state = $state; + + return $this; + } + + /** + * @template T1 of object + * + * @param class-string $event + * @param Closure(Message, STATE): STATE $closure + * + * @return $this + */ + public function when(string $event, Closure $closure): self + { + $this->handlers[$event][] = $closure; + + return $this; + } + + /** + * @param Closure(Message, STATE): STATE $closure + * + * @return $this + */ + public function any(Closure $closure): self + { + $this->anyHandlers[] = $closure; + + return $this; + } + + /** + * @param array $map + * + * @return $this + */ + public function match(array $map): self + { + foreach ($map as $event => $closure) { + $this->when($event, $closure); + } + + return $this; + } + + /** + * @param Closure(STATE): OUT $closure + * + * @return $this + */ + public function finalize(Closure $closure): self + { + $this->finalizeHandler = $closure; + + return $this; + } + + public function middleware(Middleware ...$middlewares): self + { + foreach ($middlewares as $middleware) { + $this->middlewares[] = $middleware; + } + + return $this; + } + + /** + * @param iterable $messages + * + * @return OUT + */ + public function process(iterable $messages): array + { + if ($this->middlewares !== []) { + $messages = new Pipe($messages, $this->middlewares); + } + + foreach ($messages as $message) { + $event = $message->event(); + + if (isset($this->handlers[$event::class])) { + foreach ($this->handlers[$event::class] as $handler) { + $this->state = $handler($message, $this->state); + } + } + + foreach ($this->anyHandlers as $handler) { + $this->state = $handler($message, $this->state); + } + } + + if ($this->finalizeHandler !== null) { + $this->state = ($this->finalizeHandler)($this->state); + } + + return $this->state; + } +} + +/** + * @var StateProcessor> $state + */ +$state = (new StateProcessor()) + ->when(ProfileCreated::class, function (Message $message, array $state): array { + $event = $message->event(); + + $state[$event->email->toString()] = true; + + return $state; + }) + ->finalize(function (array $state): array { + return array_keys($state); + }) + ->middleware(new ClosureMiddleware(static function (Message $message): array { + return [$message]; + })) + ->process([]); + + +$state = (new StateProcessor()) + ->initState(['foo' => 'bar']) + ->any(function (Message $message, array $state): array { + return $state; + }) + ->finalize(function (array $state): array { + return $state; + }) + ->process([]); From 7a776c7abbdd310950e308b2b2b6fee3025e894f Mon Sep 17 00:00:00 2001 From: David Badura Date: Sun, 20 Oct 2024 22:54:47 +0200 Subject: [PATCH 06/12] refactor api --- docs/pages/message.md | 163 ++++++++- docs/pages/pipeline.md | 342 ------------------ src/Message/Pipeline.php | 74 ++++ .../Reducer.php} | 76 ++-- .../AggregateToStreamHeaderTranslator.php} | 4 +- src/Message/Translator/ChainTranslator.php | 1 - .../Translator}/ClosureMiddleware.php | 4 +- .../Translator/ExcludeEventTranslator.php | 1 - .../ExcludeEventWithHeaderTranslator.php | 1 - .../Translator/FilterEventTranslator.php | 1 - .../Translator/IncludeEventTranslator.php | 1 - .../IncludeEventWithHeaderTranslator.php | 1 - .../RecalculatePlayheadTranslator.php | 1 - .../Translator/ReplaceEventTranslator.php | 2 - src/Message/Translator/Translator.php | 1 - .../Translator/UntilEventTranslator.php | 1 - src/Pipeline/Middleware/ChainMiddleware.php | 46 --- .../Middleware/ExcludeEventMiddleware.php | 28 -- .../ExcludeEventWithHeaderMiddleware.php | 26 -- .../Middleware/FilterEventMiddleware.php | 31 -- .../Middleware/IncludeEventMiddleware.php | 28 -- .../IncludeEventWithHeaderMiddleware.php | 26 -- src/Pipeline/Middleware/Middleware.php | 13 - .../RecalculatePlayheadMiddleware.php | 76 ---- .../Middleware/ReplaceEventMiddleware.php | 40 -- .../Middleware/UntilEventMiddleware.php | 41 --- src/Pipeline/Pipe.php | 67 ---- src/Pipeline/Pipeline.php | 57 --- src/Pipeline/Target/EventBusTarget.php | 21 -- src/Pipeline/Target/InMemoryTarget.php | 31 -- src/Pipeline/Target/StoreTarget.php | 21 -- src/Pipeline/Target/Target.php | 12 - ...igrateAggregateToStreamStoreSubscriber.php | 37 +- ...AggregateToStreamHeaderTranslatorTest.php} | 12 +- .../Translator/ClosureTranslatorTest.php} | 8 +- .../Middleware/ChainMiddlewareTest.php | 43 --- .../Middleware/ExcludeEventMiddlewareTest.php | 48 --- .../ExcludeEventWithHeaderMiddlewareTest.php | 49 --- .../Middleware/FilterEventMiddlewareTest.php | 52 --- .../Middleware/IncludeEventMiddlewareTest.php | 48 --- .../IncludeEventWithHeaderMiddlewareTest.php | 49 --- .../RecalculatePlayheadMiddlewareTest.php | 145 -------- .../Middleware/ReplaceEventMiddlewareTest.php | 73 ---- .../Middleware/UntilEventMiddlewareTest.php | 72 ---- tests/Unit/Pipeline/PipeTest.php | 205 ----------- tests/Unit/Pipeline/PipelineTest.php | 207 ----------- .../Pipeline/Target/EventBusTargetTest.php | 34 -- .../Pipeline/Target/InMemoryTargetTest.php | 46 --- .../Unit/Pipeline/Target/StoreTargetTest.php | 34 -- 49 files changed, 295 insertions(+), 2105 deletions(-) delete mode 100644 docs/pages/pipeline.md create mode 100644 src/Message/Pipeline.php rename src/{Pipeline/StateProcessor.php => Message/Reducer.php} (54%) rename src/{Pipeline/Middleware/AggregateToStreamHeaderMiddleware.php => Message/Translator/AggregateToStreamHeaderTranslator.php} (86%) rename src/{Pipeline/Middleware => Message/Translator}/ClosureMiddleware.php (78%) delete mode 100644 src/Pipeline/Middleware/ChainMiddleware.php delete mode 100644 src/Pipeline/Middleware/ExcludeEventMiddleware.php delete mode 100644 src/Pipeline/Middleware/ExcludeEventWithHeaderMiddleware.php delete mode 100644 src/Pipeline/Middleware/FilterEventMiddleware.php delete mode 100644 src/Pipeline/Middleware/IncludeEventMiddleware.php delete mode 100644 src/Pipeline/Middleware/IncludeEventWithHeaderMiddleware.php delete mode 100644 src/Pipeline/Middleware/Middleware.php delete mode 100644 src/Pipeline/Middleware/RecalculatePlayheadMiddleware.php delete mode 100644 src/Pipeline/Middleware/ReplaceEventMiddleware.php delete mode 100644 src/Pipeline/Middleware/UntilEventMiddleware.php delete mode 100644 src/Pipeline/Pipe.php delete mode 100644 src/Pipeline/Pipeline.php delete mode 100644 src/Pipeline/Target/EventBusTarget.php delete mode 100644 src/Pipeline/Target/InMemoryTarget.php delete mode 100644 src/Pipeline/Target/StoreTarget.php delete mode 100644 src/Pipeline/Target/Target.php rename tests/Unit/{Pipeline/Middleware/AggregateToStreamHeaderMiddlewareTest.php => Message/Translator/AggregateToStreamHeaderTranslatorTest.php} (80%) rename tests/Unit/{Pipeline/Middleware/ClosureMiddlewareTest.php => Message/Translator/ClosureTranslatorTest.php} (74%) delete mode 100644 tests/Unit/Pipeline/Middleware/ChainMiddlewareTest.php delete mode 100644 tests/Unit/Pipeline/Middleware/ExcludeEventMiddlewareTest.php delete mode 100644 tests/Unit/Pipeline/Middleware/ExcludeEventWithHeaderMiddlewareTest.php delete mode 100644 tests/Unit/Pipeline/Middleware/FilterEventMiddlewareTest.php delete mode 100644 tests/Unit/Pipeline/Middleware/IncludeEventMiddlewareTest.php delete mode 100644 tests/Unit/Pipeline/Middleware/IncludeEventWithHeaderMiddlewareTest.php delete mode 100644 tests/Unit/Pipeline/Middleware/RecalculatePlayheadMiddlewareTest.php delete mode 100644 tests/Unit/Pipeline/Middleware/ReplaceEventMiddlewareTest.php delete mode 100644 tests/Unit/Pipeline/Middleware/UntilEventMiddlewareTest.php delete mode 100644 tests/Unit/Pipeline/PipeTest.php delete mode 100644 tests/Unit/Pipeline/PipelineTest.php delete mode 100644 tests/Unit/Pipeline/Target/EventBusTargetTest.php delete mode 100644 tests/Unit/Pipeline/Target/InMemoryTargetTest.php delete mode 100644 tests/Unit/Pipeline/Target/StoreTargetTest.php diff --git a/docs/pages/message.md b/docs/pages/message.md index bf0e4faff..398ddb170 100644 --- a/docs/pages/message.md +++ b/docs/pages/message.md @@ -97,7 +97,169 @@ use Patchlevel\EventSourcing\Message\Message; /** @var Message $message */ $message->header(ApplicationHeader::class); ``` +## Translator +Translator can be used to manipulate, filter or expand messages or events. +This can be used for anti-corruption layers, data migration, or to fix errors in the event stream. + +### Exclude + +With this translator you can exclude certain events. + +```php +use Patchlevel\EventSourcing\Message\Translator\ExcludeEventTranslator; + +$translator = new ExcludeEventTranslator([EmailChanged::class]); +``` +### Include + +With this translator you can only allow certain events. + +```php +use Patchlevel\EventSourcing\Message\Translator\IncludeEventTranslator; + +$translator = new IncludeEventTranslator([ProfileCreated::class]); +``` +### Filter + +If the translator `ExcludeEventTranslator` and `IncludeEventTranslator` are not sufficient, +you can also write your own filter. +This translator expects a callback that returns either true to allow events or false to not allow them. + +```php +use Patchlevel\EventSourcing\Message\Translator\FilterEventTranslator; + +$translator = new FilterEventTranslator(static function (object $event) { + if (!$event instanceof ProfileCreated) { + return true; + } + + return $event->allowNewsletter(); +}); +``` +### Exclude Events with Header + +With this translator you can exclude event with specific header. + +```php +use Patchlevel\EventSourcing\Message\Translator\ExcludeEventWithHeaderTranslator; +use Patchlevel\EventSourcing\Store\ArchivedHeader; + +$translator = new ExcludeEventWithHeaderTranslator(ArchivedHeader::class); +``` +### Only Events with Header + +With this translator you can only allow events with a specific header. + +```php +use Patchlevel\EventSourcing\Message\Translator\IncludeEventWithHeaderTranslator; + +$translator = new IncludeEventWithHeaderTranslator(ArchivedHeader::class); +``` +### Replace + +If you want to replace an event, you can use the `ReplaceEventTranslator`. +The first parameter you have to define is the event class that you want to replace. +And as a second parameter a callback, that the old event awaits and a new event returns. + +```php +use Patchlevel\EventSourcing\Message\Translator\ReplaceEventTranslator; + +$translator = new ReplaceEventTranslator(OldVisited::class, static function (OldVisited $oldVisited) { + return new NewVisited($oldVisited->profileId()); +}); +``` +### Until + +A use case could also be that you want to look at the projection from a previous point in time. +You can use the `UntilEventTranslator` to only allow events that were `recorded` before this point in time. + +```php +use Patchlevel\EventSourcing\Message\Translator\UntilEventTranslator; + +$translator = new UntilEventTranslator(new DateTimeImmutable('2020-01-01 12:00:00')); +``` +### Recalculate playhead + +This translator can be used to recalculate the playhead. +The playhead must always be in ascending order so that the data is valid. +Some translator can break this order and the translator `RecalculatePlayheadTranslator` can fix this problem. + +```php +use Patchlevel\EventSourcing\Message\Translator\RecalculatePlayheadTranslator; + +$translator = new RecalculatePlayheadTranslator(); +``` +!!! tip + + If you migrate your event stream, you can use the `RecalculatePlayheadTranslator` to fix the playhead. + +### Chain + +If you want to group your translator, you can use one or more `ChainTranslator`. + +```php +use Patchlevel\EventSourcing\Message\Translator\ChainTranslator; +use Patchlevel\EventSourcing\Message\Translator\ExcludeEventTranslator; +use Patchlevel\EventSourcing\Message\Translator\RecalculatePlayheadTranslator; + +$translator = new ChainTranslator([ + new ExcludeEventTranslator([EmailChanged::class]), + new RecalculatePlayheadTranslator(), +]); +``` +### Custom Translator + +You can also write a custom translator. The translator gets a message and can return `n` messages. +There are the following possibilities: + +* Return only the message to an array to leave it unchanged. +* Put another message in the array to swap the message. +* Return an empty array to remove the message. +* Or return multiple messages to enrich the stream. + +In our case, the domain has changed a bit. +In the beginning we had a `ProfileCreated` event that just created a profile. +Now we have a `ProfileRegistered` and a `ProfileActivated` event, +which should replace the `ProfileCreated` event. + +```php +use Patchlevel\EventSourcing\Message\Message; +use Patchlevel\EventSourcing\Message\Translator\Translator; + +final class SplitProfileCreatedTranslator implements Translator +{ + public function __invoke(Message $message): array + { + $event = $message->event(); + + if (!$event instanceof ProfileCreated) { + return [$message]; + } + + $profileRegisteredMessage = Message::createWithHeaders( + new ProfileRegistered($event->id(), $event->name()), + $message->headers(), + ); + + $profileActivatedMessage = Message::createWithHeaders( + new ProfileActivated($event->id()), + $message->headers(), + ); + + return [$profileRegisteredMessage, $profileActivatedMessage]; + } +} +``` +!!! warning + + Since we changed the number of messages, we have to recalculate the playhead. + +!!! tip + + You don't have to migrate the store directly for every change, + but you can also use the [upcasting](upcasting.md) feature. + ## Learn more * [How to decorate messages](message_decorator.md) @@ -105,4 +267,3 @@ $message->header(ApplicationHeader::class); * [How to store messages](store.md) * [How to use subscriptions](subscription.md) * [How to use the event bus](event_bus.md) -* [How to migrate messages](pipeline.md) diff --git a/docs/pages/pipeline.md b/docs/pages/pipeline.md deleted file mode 100644 index 69ad8ce6a..000000000 --- a/docs/pages/pipeline.md +++ /dev/null @@ -1,342 +0,0 @@ -# Pipeline / Anti Corruption Layer - -As soon as you have the case where you want to flow a lot of events from A to B, -and maybe even influence the events in the form of filters or manipulations, -then the pipeline comes into play. - -There are several situations in which a pipeline makes sense: - -* Migration of the event store and its events. -* As an anti-corruption layer when publishing events to other systems. -* Or as an anti-corruption layer when importing events from other systems. - -## Pipe - -```php -use Patchlevel\EventSourcing\Pipeline\Middleware\ReplaceEventMiddleware; -use Patchlevel\EventSourcing\Pipeline\Pipeline; -use Patchlevel\EventSourcing\Pipeline\Target\StoreTarget; -use Patchlevel\EventSourcing\Tests\Unit\Fixture\Profile - -$pipe = new Pipe( - $oldStore->load(), - new ReplaceEventMiddleware(OldVisited::class, static function (OldVisited $oldVisited) { - return new NewVisited($oldVisited->profileId()); - }), -); - -$unwarp = function (iterable $messages) { - foreach ($messages as $message) { - yield $message->event(); - } -}; - -Profile::createFromEvents($unwarp($pipe)); -``` - -## Pipeline - -The pipeline uses the pipe internally and is an abstraction layer on top of it. -The pipeline is used when it comes to moving a lot of events from A to B. -A `target` must be defined where the events should flow to. -You can also define whether buffering should take place or not. - -```php -use Patchlevel\EventSourcing\Pipeline\Middleware\ReplaceEventMiddleware; -use Patchlevel\EventSourcing\Pipeline\Pipeline; -use Patchlevel\EventSourcing\Pipeline\Target\StoreTarget; - -$pipeline = new Pipeline( - new StoreTarget($newStore), - new ReplaceEventMiddleware(OldVisited::class, static function (OldVisited $oldVisited) { - return new NewVisited($oldVisited->profileId()); - }), -); - -$pipeline->run($oldStore->load()); -``` - -!!! danger - - Under no circumstances should the same store be used as target as the one used as source. - Otherwise the store will be broken afterwards! - -## Target - - - -### Store - -You can use a store to save the final result. - -```php -use Patchlevel\EventSourcing\Pipeline\Target\StoreTarget; - -$target = new StoreTarget($store); -``` - -!!! danger - - Under no circumstances may the same store be used that is used for the source. - Otherwise the store will be broken afterwards! - -!!! note - - It does not matter whether the previous store was a SingleTable or a MultiTable. - You can switch back and forth between both store types using the pipeline. - -### Event Bus - -A projection can also be used as a target. -For example, to set up a new projection or to build a new projection. - -```php -use Patchlevel\EventSourcing\Pipeline\Target\EventBusTarget; - -$target = new EventBusTarget($projection); -``` - -### In Memory - -There is also an in-memory variant for the target. This target can also be used for tests. -With the `messages` method you get all `Messages` that have reached the target. - -```php -use Patchlevel\EventSourcing\Pipeline\Target\InMemoryTarget; - -$target = new InMemoryTarget(); - -// run pipeline - -$messages = $target->messages(); -``` - -### Custom Target - -You can also define your own target. To do this, you need to implement the `Target` interface. - -```php -use Patchlevel\EventSourcing\EventBus\Message; - -final class OtherStoreTarget implements Target -{ - private OtherStore $store; - - public function __construct(OtherStore $store) - { - $this->store = $store; - } - - public function save(Message $message): void - { - $this->store->save($message); - } -} -``` - -## Middlewares - -Middelwares can be used to manipulate, delete or expand messages or events during the process. - -!!! warning - - It is important to know that some middlewares require recalculation from the playhead, - if the target is a store. This is a numbering of the events that must be in ascending order. - A corresponding note is supplied with every middleware. - -### Exclude - -With this middleware you can exclude certain events. - -```php -use Patchlevel\EventSourcing\Pipeline\Middleware\ExcludeEventMiddleware; - -$middleware = new ExcludeEventMiddleware([EmailChanged::class]); -``` - -!!! warning - - After this middleware, the playhead must be recalculated! - -### Include - - -With this middleware you can only allow certain events. - -```php -use Patchlevel\EventSourcing\Pipeline\Middleware\IncludeEventMiddleware; - -$middleware = new IncludeEventMiddleware([ProfileCreated::class]); -``` - -!!! warning - - After this middleware, the playhead must be recalculated! - -### Filter - -If the middlewares `ExcludeEventMiddleware` and `IncludeEventMiddleware` are not sufficient, -you can also write your own filter. -This middleware expects a callback that returns either true to allow events or false to not allow them. - -```php -use Patchlevel\EventSourcing\Aggregate\AggregateChanged; -use Patchlevel\EventSourcing\Pipeline\Middleware\FilterEventMiddleware; - -$middleware = new FilterEventMiddleware(function (AggregateChanged $event) { - if (!$event instanceof ProfileCreated) { - return true; - } - - return $event->allowNewsletter(); -}); -``` - -!!! warning - - After this middleware, the playhead must be recalculated! - -### Exclude Archived Events - -With this middleware you can exclude archived events. - -```php -use Patchlevel\EventSourcing\Pipeline\Middleware\ExcludeArchivedEventMiddleware; - -$middleware = new ExcludeArchivedEventMiddleware(); -``` - -!!! warning - - After this middleware, the playhead must be recalculated! - -### Only Archived Events - - -With this middleware you can only allow archived events. - -```php -use Patchlevel\EventSourcing\Pipeline\Middleware\OnlyArchivedEventMiddleware; - -$middleware = new OnlyArchivedEventMiddleware(); -``` - -!!! warning - - After this middleware, the playhead must be recalculated! - -### Replace - -If you want to replace an event, you can use the `ReplaceEventMiddleware`. -The first parameter you have to define is the event class that you want to replace. -And as a second parameter a callback, that the old event awaits and a new event returns. - -```php -use Patchlevel\EventSourcing\Pipeline\Middleware\ReplaceEventMiddleware; - -$middleware = new ReplaceEventMiddleware(OldVisited::class, static function (OldVisited $oldVisited) { - return new NewVisited($oldVisited->profileId()); -}); -``` - -!!! note - - The middleware takes over the playhead and recordedAt information. - -### Until - -A use case could also be that you want to look at the projection from a previous point in time. -You can use the `UntilEventMiddleware` to only allow events that were `recorded` before this point in time. - -```php -use Patchlevel\EventSourcing\Pipeline\Middleware\ClassRenameMiddleware; - -$middleware = new UntilEventMiddleware(new DateTimeImmutable('2020-01-01 12:00:00')); -``` - -!!! warning - - After this middleware, the playhead must be recalculated! - -### Recalculate playhead - -This middleware can be used to recalculate the playhead. -The playhead must always be in ascending order so that the data is valid. -Some middleware can break this order and the middleware `RecalculatePlayheadMiddleware` can fix this problem. - -```php -use Patchlevel\EventSourcing\Pipeline\Middleware\RecalculatePlayheadMiddleware; - -$middleware = new RecalculatePlayheadMiddleware(); -``` - -!!! note - - You only need to add this middleware once at the end of the pipeline. - -### Chain - -If you want to group your middleware, you can use one or more `ChainMiddleware`. - -```php -use Patchlevel\EventSourcing\Pipeline\Middleware\ChainMiddleware; -use Patchlevel\EventSourcing\Pipeline\Middleware\ExcludeEventMiddleware; -use Patchlevel\EventSourcing\Pipeline\Middleware\RecalculatePlayheadMiddleware; - -$middleware = new ChainMiddleware([ - new ExcludeEventMiddleware([EmailChanged::class]), - new RecalculatePlayheadMiddleware() -]); -``` - -### Custom middleware - -You can also write a custom middleware. The middleware gets a message and can return `N` messages. -There are the following possibilities: - -* Return only the message to an array to leave it unchanged. -* Put another message in the array to swap the message. -* Return an empty array to remove the message. -* Or return multiple messages to enrich the stream. - -In our case, the domain has changed a bit. -In the beginning we had a `ProfileCreated` event that just created a profile. -Now we have a `ProfileRegistered` and a `ProfileActivated` event, -which should replace the `ProfileCreated` event. - -```php -use Patchlevel\EventSourcing\EventBus\Message; -use Patchlevel\EventSourcing\Pipeline\Middleware\Middleware; - -final class SplitProfileCreatedMiddleware implements Middleware -{ - public function __invoke(Message $message): array - { - $event = $message->event(); - - if (!$event instanceof ProfileCreated) { - return [$message]; - } - - $profileRegisteredMessage = Message::createWithHeaders( - new ProfileRegistered($event->id(), $event->name()), - $message->headers() - ); - - $profileActivatedMessage = Message::createWithHeaders( - new ProfileActivated($event->id()), - $message->headers() - ); - - return [$profileRegisteredMessage, $profileActivatedMessage]; - } -} -``` - -!!! warning - - Since we changed the number of messages, we have to recalculate the playhead. - -!!! note - - You can find more about messages [here](event_bus.md). \ No newline at end of file diff --git a/src/Message/Pipeline.php b/src/Message/Pipeline.php new file mode 100644 index 000000000..cfba44b49 --- /dev/null +++ b/src/Message/Pipeline.php @@ -0,0 +1,74 @@ + */ +final class Pipeline implements IteratorAggregate +{ + /** + * @param iterable $messages + * @param list $translators + */ + public function __construct( + private readonly iterable $messages, + private readonly array $translators = [], + ) { + } + + public function appendMiddleware(Translator $translator): self + { + return new self( + $this->messages, + [...$this->translators, $translator], + ); + } + + public function prependMiddleware(Translator $translator): self + { + return new self( + $this->messages, + [$translator, ...$this->translators], + ); + } + + /** @return Traversable */ + public function getIterator(): Traversable + { + return $this->createGenerator( + $this->messages, + new ChainTranslator($this->translators), + ); + } + + /** + * @return list + */ + public function toArray(): array + { + return iterator_to_array($this->getIterator()); + } + + /** + * @param iterable $messages + * + * @return Generator + */ + private function createGenerator(iterable $messages, Translator $translator): Generator + { + foreach ($messages as $message) { + $result = $translator($message); + + foreach ($result as $m) { + yield $m; + } + } + } +} diff --git a/src/Pipeline/StateProcessor.php b/src/Message/Reducer.php similarity index 54% rename from src/Pipeline/StateProcessor.php rename to src/Message/Reducer.php index 92b09ca3b..ae1002faa 100644 --- a/src/Pipeline/StateProcessor.php +++ b/src/Message/Reducer.php @@ -1,24 +1,20 @@ * @template OUT of array = STATE */ -final class StateProcessor +final class Reducer { /** * @var STATE */ - private array $state = []; + private array $initState = []; /** * @var array> @@ -36,18 +32,18 @@ final class StateProcessor private Closure|null $finalizeHandler = null; /** - * @var list + * @var list */ - private array $middlewares = []; + private array $translators = []; /** - * @param STATE $state + * @param STATE $initState * * @return $this */ - public function initState(array $state): self + public function initState(array $initState): self { - $this->state = $state; + $this->initState = $initState; return $this; } @@ -105,10 +101,10 @@ public function finalize(Closure $closure): self return $this; } - public function middleware(Middleware ...$middlewares): self + public function translator(Translator ...$translators): self { - foreach ($middlewares as $middleware) { - $this->middlewares[] = $middleware; + foreach ($translators as $translator) { + $this->translators[] = $translator; } return $this; @@ -119,60 +115,32 @@ public function middleware(Middleware ...$middlewares): self * * @return OUT */ - public function process(iterable $messages): array + public function reduce(iterable $messages): array { - if ($this->middlewares !== []) { - $messages = new Pipe($messages, $this->middlewares); + if ($this->translators !== []) { + $messages = new Pipeline($messages, $this->translators); } + $state = $this->initState; + foreach ($messages as $message) { $event = $message->event(); if (isset($this->handlers[$event::class])) { foreach ($this->handlers[$event::class] as $handler) { - $this->state = $handler($message, $this->state); + $state = $handler($message, $state); } } foreach ($this->anyHandlers as $handler) { - $this->state = $handler($message, $this->state); + $state = $handler($message, $state); } } if ($this->finalizeHandler !== null) { - $this->state = ($this->finalizeHandler)($this->state); + $state = ($this->finalizeHandler)($state); } - return $this->state; - } -} - -/** - * @var StateProcessor> $state - */ -$state = (new StateProcessor()) - ->when(ProfileCreated::class, function (Message $message, array $state): array { - $event = $message->event(); - - $state[$event->email->toString()] = true; - - return $state; - }) - ->finalize(function (array $state): array { - return array_keys($state); - }) - ->middleware(new ClosureMiddleware(static function (Message $message): array { - return [$message]; - })) - ->process([]); - - -$state = (new StateProcessor()) - ->initState(['foo' => 'bar']) - ->any(function (Message $message, array $state): array { return $state; - }) - ->finalize(function (array $state): array { - return $state; - }) - ->process([]); + } +} \ No newline at end of file diff --git a/src/Pipeline/Middleware/AggregateToStreamHeaderMiddleware.php b/src/Message/Translator/AggregateToStreamHeaderTranslator.php similarity index 86% rename from src/Pipeline/Middleware/AggregateToStreamHeaderMiddleware.php rename to src/Message/Translator/AggregateToStreamHeaderTranslator.php index 4c136bd92..c8dce10f6 100644 --- a/src/Pipeline/Middleware/AggregateToStreamHeaderMiddleware.php +++ b/src/Message/Translator/AggregateToStreamHeaderTranslator.php @@ -2,14 +2,14 @@ declare(strict_types=1); -namespace Patchlevel\EventSourcing\Pipeline\Middleware; +namespace Patchlevel\EventSourcing\Message\Translator; use Patchlevel\EventSourcing\Aggregate\AggregateHeader; use Patchlevel\EventSourcing\Message\Message; use Patchlevel\EventSourcing\Store\StreamHeader; /** @experimental */ -final class AggregateToStreamHeaderMiddleware implements Middleware +final class AggregateToStreamHeaderTranslator implements Translator { /** @return list */ public function __invoke(Message $message): array diff --git a/src/Message/Translator/ChainTranslator.php b/src/Message/Translator/ChainTranslator.php index 733838f52..708bdb3b4 100644 --- a/src/Message/Translator/ChainTranslator.php +++ b/src/Message/Translator/ChainTranslator.php @@ -8,7 +8,6 @@ use function array_values; -/** @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\ChainMiddleware instead */ final class ChainTranslator implements Translator { /** @param iterable $translators */ diff --git a/src/Pipeline/Middleware/ClosureMiddleware.php b/src/Message/Translator/ClosureMiddleware.php similarity index 78% rename from src/Pipeline/Middleware/ClosureMiddleware.php rename to src/Message/Translator/ClosureMiddleware.php index 339561251..8aaec3104 100644 --- a/src/Pipeline/Middleware/ClosureMiddleware.php +++ b/src/Message/Translator/ClosureMiddleware.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace Patchlevel\EventSourcing\Pipeline\Middleware; +namespace Patchlevel\EventSourcing\Message\Translator; use Closure; use Patchlevel\EventSourcing\Message\Message; -final class ClosureMiddleware implements Middleware +final class ClosureMiddleware implements Translator { /** @param Closure(Message): list $callable */ public function __construct( diff --git a/src/Message/Translator/ExcludeEventTranslator.php b/src/Message/Translator/ExcludeEventTranslator.php index 9c249d373..f4a26f632 100644 --- a/src/Message/Translator/ExcludeEventTranslator.php +++ b/src/Message/Translator/ExcludeEventTranslator.php @@ -6,7 +6,6 @@ use Patchlevel\EventSourcing\Message\Message; -/** @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\ExcludeEventMiddleware instead */ final class ExcludeEventTranslator implements Translator { /** @param list $classes */ diff --git a/src/Message/Translator/ExcludeEventWithHeaderTranslator.php b/src/Message/Translator/ExcludeEventWithHeaderTranslator.php index 2776b8fc8..4fa4b7854 100644 --- a/src/Message/Translator/ExcludeEventWithHeaderTranslator.php +++ b/src/Message/Translator/ExcludeEventWithHeaderTranslator.php @@ -6,7 +6,6 @@ use Patchlevel\EventSourcing\Message\Message; -/** @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\ExcludeEventWithHeaderMiddleware instead */ final class ExcludeEventWithHeaderTranslator implements Translator { /** @param class-string $header */ diff --git a/src/Message/Translator/FilterEventTranslator.php b/src/Message/Translator/FilterEventTranslator.php index aad97330a..b8f3c5b78 100644 --- a/src/Message/Translator/FilterEventTranslator.php +++ b/src/Message/Translator/FilterEventTranslator.php @@ -6,7 +6,6 @@ use Patchlevel\EventSourcing\Message\Message; -/** @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\FilterEventMiddleware instead */ final class FilterEventTranslator implements Translator { /** @var callable(object $event):bool */ diff --git a/src/Message/Translator/IncludeEventTranslator.php b/src/Message/Translator/IncludeEventTranslator.php index 152b2dfb5..ae2a412f5 100644 --- a/src/Message/Translator/IncludeEventTranslator.php +++ b/src/Message/Translator/IncludeEventTranslator.php @@ -6,7 +6,6 @@ use Patchlevel\EventSourcing\Message\Message; -/** @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\IncludeEventWithHeaderMiddleware instead */ final class IncludeEventTranslator implements Translator { /** @param list $classes */ diff --git a/src/Message/Translator/IncludeEventWithHeaderTranslator.php b/src/Message/Translator/IncludeEventWithHeaderTranslator.php index bb47c55ad..8c88514ae 100644 --- a/src/Message/Translator/IncludeEventWithHeaderTranslator.php +++ b/src/Message/Translator/IncludeEventWithHeaderTranslator.php @@ -6,7 +6,6 @@ use Patchlevel\EventSourcing\Message\Message; -/** @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\IncludeEventWithHeaderMiddleware instead */ final class IncludeEventWithHeaderTranslator implements Translator { /** @param class-string $header */ diff --git a/src/Message/Translator/RecalculatePlayheadTranslator.php b/src/Message/Translator/RecalculatePlayheadTranslator.php index 23c5bbd34..27185baaa 100644 --- a/src/Message/Translator/RecalculatePlayheadTranslator.php +++ b/src/Message/Translator/RecalculatePlayheadTranslator.php @@ -11,7 +11,6 @@ use function array_key_exists; -/** @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\RecalculatePlayheadMiddleware instead */ final class RecalculatePlayheadTranslator implements Translator { /** @var array */ diff --git a/src/Message/Translator/ReplaceEventTranslator.php b/src/Message/Translator/ReplaceEventTranslator.php index c6c343fa7..597379229 100644 --- a/src/Message/Translator/ReplaceEventTranslator.php +++ b/src/Message/Translator/ReplaceEventTranslator.php @@ -7,8 +7,6 @@ use Patchlevel\EventSourcing\Message\Message; /** - * @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\ReplaceEventMiddleware instead - * * @template T of object */ final class ReplaceEventTranslator implements Translator diff --git a/src/Message/Translator/Translator.php b/src/Message/Translator/Translator.php index 592c599d7..66fe20057 100644 --- a/src/Message/Translator/Translator.php +++ b/src/Message/Translator/Translator.php @@ -6,7 +6,6 @@ use Patchlevel\EventSourcing\Message\Message; -/** @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\Middleware instead */ interface Translator { /** @return list */ diff --git a/src/Message/Translator/UntilEventTranslator.php b/src/Message/Translator/UntilEventTranslator.php index 8779f138b..3cdf739ad 100644 --- a/src/Message/Translator/UntilEventTranslator.php +++ b/src/Message/Translator/UntilEventTranslator.php @@ -10,7 +10,6 @@ use Patchlevel\EventSourcing\Message\Message; use Patchlevel\EventSourcing\Store\StreamHeader; -/** @deprecated use Patchlevel\EventSourcing\Pipeline\Middleware\UntilEventMiddleware instead */ final class UntilEventTranslator implements Translator { public function __construct( diff --git a/src/Pipeline/Middleware/ChainMiddleware.php b/src/Pipeline/Middleware/ChainMiddleware.php deleted file mode 100644 index bf67db990..000000000 --- a/src/Pipeline/Middleware/ChainMiddleware.php +++ /dev/null @@ -1,46 +0,0 @@ - $middlewares */ - public function __construct( - private readonly iterable $middlewares, - ) { - } - - /** @return list */ - public function __invoke(Message $message): array - { - $messages = [$message]; - - foreach ($this->middlewares as $middleware) { - $messages = $this->process($middleware, $messages); - } - - return $messages; - } - - /** - * @param list $messages - * - * @return list - */ - private function process(Middleware $middleware, array $messages): array - { - $result = []; - - foreach ($messages as $message) { - $result += $middleware($message); - } - - return array_values($result); - } -} diff --git a/src/Pipeline/Middleware/ExcludeEventMiddleware.php b/src/Pipeline/Middleware/ExcludeEventMiddleware.php deleted file mode 100644 index 0fcb96376..000000000 --- a/src/Pipeline/Middleware/ExcludeEventMiddleware.php +++ /dev/null @@ -1,28 +0,0 @@ - $classes */ - public function __construct( - private readonly array $classes, - ) { - } - - /** @return list */ - public function __invoke(Message $message): array - { - foreach ($this->classes as $class) { - if ($message->event() instanceof $class) { - return []; - } - } - - return [$message]; - } -} diff --git a/src/Pipeline/Middleware/ExcludeEventWithHeaderMiddleware.php b/src/Pipeline/Middleware/ExcludeEventWithHeaderMiddleware.php deleted file mode 100644 index 608612285..000000000 --- a/src/Pipeline/Middleware/ExcludeEventWithHeaderMiddleware.php +++ /dev/null @@ -1,26 +0,0 @@ - */ - public function __invoke(Message $message): array - { - if ($message->hasHeader($this->header)) { - return []; - } - - return [$message]; - } -} diff --git a/src/Pipeline/Middleware/FilterEventMiddleware.php b/src/Pipeline/Middleware/FilterEventMiddleware.php deleted file mode 100644 index 9aeaefb55..000000000 --- a/src/Pipeline/Middleware/FilterEventMiddleware.php +++ /dev/null @@ -1,31 +0,0 @@ -callable = $callable; - } - - /** @return list */ - public function __invoke(Message $message): array - { - $result = ($this->callable)($message->event()); - - if ($result) { - return [$message]; - } - - return []; - } -} diff --git a/src/Pipeline/Middleware/IncludeEventMiddleware.php b/src/Pipeline/Middleware/IncludeEventMiddleware.php deleted file mode 100644 index 7b3262423..000000000 --- a/src/Pipeline/Middleware/IncludeEventMiddleware.php +++ /dev/null @@ -1,28 +0,0 @@ - $classes */ - public function __construct( - private readonly array $classes, - ) { - } - - /** @return list */ - public function __invoke(Message $message): array - { - foreach ($this->classes as $class) { - if ($message->event() instanceof $class) { - return [$message]; - } - } - - return []; - } -} diff --git a/src/Pipeline/Middleware/IncludeEventWithHeaderMiddleware.php b/src/Pipeline/Middleware/IncludeEventWithHeaderMiddleware.php deleted file mode 100644 index 3ad46446b..000000000 --- a/src/Pipeline/Middleware/IncludeEventWithHeaderMiddleware.php +++ /dev/null @@ -1,26 +0,0 @@ - */ - public function __invoke(Message $message): array - { - if ($message->hasHeader($this->header)) { - return [$message]; - } - - return []; - } -} diff --git a/src/Pipeline/Middleware/Middleware.php b/src/Pipeline/Middleware/Middleware.php deleted file mode 100644 index 490f19514..000000000 --- a/src/Pipeline/Middleware/Middleware.php +++ /dev/null @@ -1,13 +0,0 @@ - */ - public function __invoke(Message $message): array; -} diff --git a/src/Pipeline/Middleware/RecalculatePlayheadMiddleware.php b/src/Pipeline/Middleware/RecalculatePlayheadMiddleware.php deleted file mode 100644 index cabb99f93..000000000 --- a/src/Pipeline/Middleware/RecalculatePlayheadMiddleware.php +++ /dev/null @@ -1,76 +0,0 @@ - */ - private array $index = []; - - /** @return list */ - public function __invoke(Message $message): array - { - try { - $header = $message->header(AggregateHeader::class); - } catch (HeaderNotFound) { - try { - $header = $message->header(StreamHeader::class); - } catch (HeaderNotFound) { - return [$message]; - } - } - - $stream = $header instanceof StreamHeader ? $header->streamName : $header->streamName(); - - $playhead = $this->nextPlayhead($stream); - - if ($header->playhead === $playhead) { - return [$message]; - } - - if ($header instanceof StreamHeader) { - return [ - $message->withHeader(new StreamHeader( - $header->streamName, - $playhead, - $header->recordedOn, - )), - ]; - } - - return [ - $message->withHeader(new AggregateHeader( - $header->aggregateName, - $header->aggregateId, - $playhead, - $header->recordedOn, - )), - ]; - } - - public function reset(): void - { - $this->index = []; - } - - /** @return positive-int */ - private function nextPlayhead(string $stream): int - { - if (!array_key_exists($stream, $this->index)) { - $this->index[$stream] = 1; - } else { - $this->index[$stream]++; - } - - return $this->index[$stream]; - } -} diff --git a/src/Pipeline/Middleware/ReplaceEventMiddleware.php b/src/Pipeline/Middleware/ReplaceEventMiddleware.php deleted file mode 100644 index 9dc92fdb5..000000000 --- a/src/Pipeline/Middleware/ReplaceEventMiddleware.php +++ /dev/null @@ -1,40 +0,0 @@ - $class - * @param callable(T $event):object $callable - */ - public function __construct( - private readonly string $class, - callable $callable, - ) { - $this->callable = $callable; - } - - /** @return list */ - public function __invoke(Message $message): array - { - $event = $message->event(); - - if (!$event instanceof $this->class) { - return [$message]; - } - - $callable = $this->callable; - $newEvent = $callable($event); - - return [Message::createWithHeaders($newEvent, $message->headers())]; - } -} diff --git a/src/Pipeline/Middleware/UntilEventMiddleware.php b/src/Pipeline/Middleware/UntilEventMiddleware.php deleted file mode 100644 index 44cc201ba..000000000 --- a/src/Pipeline/Middleware/UntilEventMiddleware.php +++ /dev/null @@ -1,41 +0,0 @@ - */ - public function __invoke(Message $message): array - { - try { - $header = $message->header(AggregateHeader::class); - } catch (HeaderNotFound) { - try { - $header = $message->header(StreamHeader::class); - } catch (HeaderNotFound) { - return [$message]; - } - } - - $recordedOn = $header->recordedOn; - - if ($recordedOn < $this->until) { - return [$message]; - } - - return []; - } -} diff --git a/src/Pipeline/Pipe.php b/src/Pipeline/Pipe.php deleted file mode 100644 index 688b9e628..000000000 --- a/src/Pipeline/Pipe.php +++ /dev/null @@ -1,67 +0,0 @@ - */ -final class Pipe implements IteratorAggregate -{ - /** - * @param iterable $messages - * @param list $middlewares - */ - public function __construct( - private readonly iterable $messages, - private readonly array $middlewares = [], - ) { - } - - public function appendMiddleware(Middleware $middleware): self - { - return new self( - $this->messages, - [...$this->middlewares, $middleware], - ); - } - - public function prependMiddleware(Middleware $middleware): self - { - return new self( - $this->messages, - [$middleware, ...$this->middlewares], - ); - } - - /** @return Traversable */ - public function getIterator(): Traversable - { - return $this->createGenerator( - $this->messages, - new ChainMiddleware($this->middlewares), - ); - } - - /** - * @param iterable $messages - * - * @return Generator - */ - private function createGenerator(iterable $messages, Middleware $middleware): Generator - { - foreach ($messages as $message) { - $result = $middleware($message); - - foreach ($result as $m) { - yield $m; - } - } - } -} diff --git a/src/Pipeline/Pipeline.php b/src/Pipeline/Pipeline.php deleted file mode 100644 index bb8b528e6..000000000 --- a/src/Pipeline/Pipeline.php +++ /dev/null @@ -1,57 +0,0 @@ -|Middleware $middlewares */ - public function __construct( - private readonly Target $target, - array|Middleware $middlewares = [], - private readonly float|int $bufferSize = 1_000, - ) { - if ($middlewares instanceof Middleware) { - $this->middlewares = [$middlewares]; - } else { - $this->middlewares = $middlewares; - } - } - - /** @param iterable $messages */ - public function run(iterable $messages): void - { - $stream = new Pipe( - $messages, - $this->middlewares, - ); - - $buffer = []; - - foreach ($stream as $message) { - $buffer[] = $message; - - if (count($buffer) < $this->bufferSize) { - continue; - } - - $this->target->save(...$buffer); - $buffer = []; - } - - if ($buffer === []) { - return; - } - - $this->target->save(...$buffer); - } -} diff --git a/src/Pipeline/Target/EventBusTarget.php b/src/Pipeline/Target/EventBusTarget.php deleted file mode 100644 index 7d07f5905..000000000 --- a/src/Pipeline/Target/EventBusTarget.php +++ /dev/null @@ -1,21 +0,0 @@ -eventBus->dispatch(...$message); - } -} diff --git a/src/Pipeline/Target/InMemoryTarget.php b/src/Pipeline/Target/InMemoryTarget.php deleted file mode 100644 index a012176b8..000000000 --- a/src/Pipeline/Target/InMemoryTarget.php +++ /dev/null @@ -1,31 +0,0 @@ - */ - private array $messages = []; - - public function save(Message ...$message): void - { - foreach ($message as $m) { - $this->messages[] = $m; - } - } - - /** @return list */ - public function messages(): array - { - return $this->messages; - } - - public function clear(): void - { - $this->messages = []; - } -} diff --git a/src/Pipeline/Target/StoreTarget.php b/src/Pipeline/Target/StoreTarget.php deleted file mode 100644 index 1283da453..000000000 --- a/src/Pipeline/Target/StoreTarget.php +++ /dev/null @@ -1,21 +0,0 @@ -store->save(...$message); - } -} diff --git a/src/Pipeline/Target/Target.php b/src/Pipeline/Target/Target.php deleted file mode 100644 index 0302a7d53..000000000 --- a/src/Pipeline/Target/Target.php +++ /dev/null @@ -1,12 +0,0 @@ - */ private array $messages = []; - private readonly Pipeline $pipeline; + /** + * @var list + */ + private readonly array $middlewares; public function __construct( private readonly StreamDoctrineDbalStore $targetStore, @@ -41,11 +41,9 @@ public function __construct( new ChainDoctrineSchemaConfigurator([$targetStore]), ); - $this->pipeline = new Pipeline( - new StoreTarget($this->targetStore), - new AggregateToStreamHeaderMiddleware(), - INF, - ); + $this->middlewares = [ + new AggregateToStreamHeaderTranslator() + ]; } #[Subscribe('*')] @@ -54,6 +52,16 @@ public function handle(Message $message): void $this->messages[] = $message; } + #[Subscribe('*')] + public function kafka(Message $message): void + { + $pipeline = new Pipeline([$message], $this->middlewares); + + foreach ($pipeline as $message) { + $this->kafka->publish($message); + } + } + public function beginBatch(): void { $this->messages = []; @@ -61,9 +69,10 @@ public function beginBatch(): void public function commitBatch(): void { - $this->pipeline->run($this->messages); - + $pipeline = new Pipeline($this->messages, $this->middlewares); $this->messages = []; + + $this->targetStore->save(...$pipeline); } public function rollbackBatch(): void diff --git a/tests/Unit/Pipeline/Middleware/AggregateToStreamHeaderMiddlewareTest.php b/tests/Unit/Message/Translator/AggregateToStreamHeaderTranslatorTest.php similarity index 80% rename from tests/Unit/Pipeline/Middleware/AggregateToStreamHeaderMiddlewareTest.php rename to tests/Unit/Message/Translator/AggregateToStreamHeaderTranslatorTest.php index 22ff18310..009e096e6 100644 --- a/tests/Unit/Pipeline/Middleware/AggregateToStreamHeaderMiddlewareTest.php +++ b/tests/Unit/Message/Translator/AggregateToStreamHeaderTranslatorTest.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace Patchlevel\EventSourcing\Tests\Unit\Pipeline\Middleware; +namespace Patchlevel\EventSourcing\Tests\Unit\Message\Translator; use DateTimeImmutable; use Patchlevel\EventSourcing\Aggregate\AggregateHeader; use Patchlevel\EventSourcing\Message\Message; -use Patchlevel\EventSourcing\Pipeline\Middleware\AggregateToStreamHeaderMiddleware; +use Patchlevel\EventSourcing\Message\Translator\AggregateToStreamHeaderTranslator; use Patchlevel\EventSourcing\Store\StreamHeader; use Patchlevel\EventSourcing\Tests\Unit\Fixture\Email; use Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileCreated; @@ -15,8 +15,8 @@ use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; -/** @covers \Patchlevel\EventSourcing\Pipeline\Middleware\AggregateToStreamHeaderMiddleware */ -final class AggregateToStreamHeaderMiddlewareTest extends TestCase +/** @covers \Patchlevel\EventSourcing\Message\Translator\AggregateToStreamHeaderTranslator */ +final class AggregateToStreamHeaderTranslatorTest extends TestCase { use ProphecyTrait; @@ -29,7 +29,7 @@ public function testMissingHeader(): void ), ); - $middleware = new AggregateToStreamHeaderMiddleware(); + $middleware = new AggregateToStreamHeaderTranslator(); $result = $middleware($message); @@ -52,7 +52,7 @@ public function testMigrateHeader(): void ), ))->withHeader($aggregateHeader); - $middleware = new AggregateToStreamHeaderMiddleware(); + $middleware = new AggregateToStreamHeaderTranslator(); $result = $middleware($message); diff --git a/tests/Unit/Pipeline/Middleware/ClosureMiddlewareTest.php b/tests/Unit/Message/Translator/ClosureTranslatorTest.php similarity index 74% rename from tests/Unit/Pipeline/Middleware/ClosureMiddlewareTest.php rename to tests/Unit/Message/Translator/ClosureTranslatorTest.php index c81494a78..814e7a2b5 100644 --- a/tests/Unit/Pipeline/Middleware/ClosureMiddlewareTest.php +++ b/tests/Unit/Message/Translator/ClosureTranslatorTest.php @@ -2,17 +2,17 @@ declare(strict_types=1); -namespace Patchlevel\EventSourcing\Tests\Unit\Pipeline\Middleware; +namespace Patchlevel\EventSourcing\Tests\Unit\Message\Translator; use Patchlevel\EventSourcing\Message\Message; -use Patchlevel\EventSourcing\Pipeline\Middleware\ClosureMiddleware; +use Patchlevel\EventSourcing\Message\Translator\ClosureMiddleware; use Patchlevel\EventSourcing\Tests\Unit\Fixture\Email; use Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileCreated; use Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileId; use PHPUnit\Framework\TestCase; -/** @covers \Patchlevel\EventSourcing\Pipeline\Middleware\ClosureMiddleware */ -final class ClosureMiddlewareTest extends TestCase +/** @covers \Patchlevel\EventSourcing\Message\Translator\ClosureMiddleware */ +final class ClosureTranslatorTest extends TestCase { public function testClosure(): void { diff --git a/tests/Unit/Pipeline/Middleware/ChainMiddlewareTest.php b/tests/Unit/Pipeline/Middleware/ChainMiddlewareTest.php deleted file mode 100644 index 8f6b0ea36..000000000 --- a/tests/Unit/Pipeline/Middleware/ChainMiddlewareTest.php +++ /dev/null @@ -1,43 +0,0 @@ -prophesize(Middleware::class); - $child1->__invoke($message)->willReturn([$message])->shouldBeCalled(); - - $child2 = $this->prophesize(Middleware::class); - $child2->__invoke($message)->willReturn([$message])->shouldBeCalled(); - - $middleware = new ChainMiddleware([ - $child1->reveal(), - $child2->reveal(), - ]); - - $middleware($message); - } -} diff --git a/tests/Unit/Pipeline/Middleware/ExcludeEventMiddlewareTest.php b/tests/Unit/Pipeline/Middleware/ExcludeEventMiddlewareTest.php deleted file mode 100644 index d305f1bd3..000000000 --- a/tests/Unit/Pipeline/Middleware/ExcludeEventMiddlewareTest.php +++ /dev/null @@ -1,48 +0,0 @@ -withHeader(new ArchivedHeader()); - - $result = $middleware($message); - - self::assertSame([], $result); - } - - public function testIncludeEvent(): void - { - $middleware = new ExcludeEventWithHeaderMiddleware(ArchivedHeader::class); - - $message = Message::create( - new ProfileCreated( - ProfileId::fromString('1'), - Email::fromString('hallo@patchlevel.de'), - ), - ); - - $result = $middleware($message); - - self::assertSame([$message], $result); - } -} diff --git a/tests/Unit/Pipeline/Middleware/FilterEventMiddlewareTest.php b/tests/Unit/Pipeline/Middleware/FilterEventMiddlewareTest.php deleted file mode 100644 index 5ddc0f7e1..000000000 --- a/tests/Unit/Pipeline/Middleware/FilterEventMiddlewareTest.php +++ /dev/null @@ -1,52 +0,0 @@ -withHeader(new ArchivedHeader()); - - $result = $middleware($message); - - self::assertSame([$message], $result); - } -} diff --git a/tests/Unit/Pipeline/Middleware/RecalculatePlayheadMiddlewareTest.php b/tests/Unit/Pipeline/Middleware/RecalculatePlayheadMiddlewareTest.php deleted file mode 100644 index 46d7c272b..000000000 --- a/tests/Unit/Pipeline/Middleware/RecalculatePlayheadMiddlewareTest.php +++ /dev/null @@ -1,145 +0,0 @@ -withHeader(new AggregateHeader('profile', '1', 5, new DateTimeImmutable())); - - $result = $middleware($message); - - self::assertCount(1, $result); - self::assertSame('profile', $result[0]->header(AggregateHeader::class)->aggregateName); - self::assertSame(1, $result[0]->header(AggregateHeader::class)->playhead); - } - - public function testRecalculatePlayheadWithSamePlayhead(): void - { - $middleware = new RecalculatePlayheadMiddleware(); - - $event = new ProfileCreated( - ProfileId::fromString('1'), - Email::fromString('hallo@patchlevel.de'), - ); - - $message = Message::create($event) - ->withHeader(new AggregateHeader('profile', '1', 1, new DateTimeImmutable())); - - $result = $middleware($message); - - self::assertSame([$message], $result); - } - - public function testRecalculateMultipleMessages(): void - { - $middleware = new RecalculatePlayheadMiddleware(); - - $event = new ProfileCreated( - ProfileId::fromString('1'), - Email::fromString('hallo@patchlevel.de'), - ); - - $message = Message::create($event) - ->withHeader(new AggregateHeader('profile', '1', 5, new DateTimeImmutable())); - $result = $middleware($message); - - self::assertCount(1, $result); - self::assertSame('profile', $result[0]->header(AggregateHeader::class)->aggregateName); - self::assertSame(1, $result[0]->header(AggregateHeader::class)->playhead); - - $message = Message::create($event) - ->withHeader(new AggregateHeader('profile', '1', 8, new DateTimeImmutable())); - - $result = $middleware($message); - - self::assertCount(1, $result); - self::assertSame('profile', $result[0]->header(AggregateHeader::class)->aggregateName); - self::assertSame(2, $result[0]->header(AggregateHeader::class)->playhead); - } - - public function testReset(): void - { - $middleware = new RecalculatePlayheadMiddleware(); - - $event = new ProfileCreated( - ProfileId::fromString('1'), - Email::fromString('hallo@patchlevel.de'), - ); - - $message = Message::create($event) - ->withHeader(new AggregateHeader('profile', '1', 5, new DateTimeImmutable())); - $result = $middleware($message); - - self::assertCount(1, $result); - self::assertSame('profile', $result[0]->header(AggregateHeader::class)->aggregateName); - self::assertSame(1, $result[0]->header(AggregateHeader::class)->playhead); - - $message = Message::create($event) - ->withHeader(new AggregateHeader('profile', '1', 8, new DateTimeImmutable())); - - $middleware->reset(); - $result = $middleware($message); - - self::assertCount(1, $result); - self::assertSame('profile', $result[0]->header(AggregateHeader::class)->aggregateName); - self::assertSame(1, $result[0]->header(AggregateHeader::class)->playhead); - } - - public function testRecalculatePlayheadWithStreamHeader(): void - { - $middleware = new RecalculatePlayheadMiddleware(); - - $event = new ProfileCreated( - ProfileId::fromString('1'), - Email::fromString('hallo@patchlevel.de'), - ); - - $message = Message::create($event) - ->withHeader(new StreamHeader('profile-1', 5, new DateTimeImmutable())); - - $result = $middleware($message); - - self::assertCount(1, $result); - self::assertSame('profile-1', $result[0]->header(StreamHeader::class)->streamName); - self::assertSame(1, $result[0]->header(StreamHeader::class)->playhead); - } - - public function testRecalculateWithoutHeader(): void - { - $middleware = new RecalculatePlayheadMiddleware(); - - $event = new ProfileCreated( - ProfileId::fromString('1'), - Email::fromString('hallo@patchlevel.de'), - ); - - $message = Message::create($event); - - $result = $middleware($message); - - self::assertSame([$message], $result); - } -} diff --git a/tests/Unit/Pipeline/Middleware/ReplaceEventMiddlewareTest.php b/tests/Unit/Pipeline/Middleware/ReplaceEventMiddlewareTest.php deleted file mode 100644 index d7098a9f0..000000000 --- a/tests/Unit/Pipeline/Middleware/ReplaceEventMiddlewareTest.php +++ /dev/null @@ -1,73 +0,0 @@ -profileId, - ); - }, - ); - - $message = new Message( - new ProfileCreated( - ProfileId::fromString('1'), - Email::fromString('hallo@patchlevel.de'), - ), - ); - - $result = $middleware($message); - - self::assertCount(1, $result); - - $event = $result[0]->event(); - - self::assertInstanceOf(ProfileVisited::class, $event); - } - - public function testReplaceInvalidClass(): void - { - /** @psalm-suppress InvalidArgument */ - $middleware = new ReplaceEventMiddleware( - MessagePublished::class, - static function (ProfileCreated $event) { - return new ProfileVisited( - $event->profileId, - ); - }, - ); - - $message = new Message( - new ProfileCreated( - ProfileId::fromString('1'), - Email::fromString('hallo@patchlevel.de'), - ), - ); - - $result = $middleware($message); - - self::assertCount(1, $result); - - $event = $result[0]->event(); - - self::assertInstanceOf(ProfileCreated::class, $event); - } -} diff --git a/tests/Unit/Pipeline/Middleware/UntilEventMiddlewareTest.php b/tests/Unit/Pipeline/Middleware/UntilEventMiddlewareTest.php deleted file mode 100644 index 3d6f087d3..000000000 --- a/tests/Unit/Pipeline/Middleware/UntilEventMiddlewareTest.php +++ /dev/null @@ -1,72 +0,0 @@ -withHeader(new AggregateHeader('pofile', '1', 1, new DateTimeImmutable('2020-02-01 00:00:00'))); - - $result = $middleware($message); - - self::assertSame([$message], $result); - } - - public function testNegative(): void - { - $until = new DateTimeImmutable('2020-01-01 00:00:00'); - - $middleware = new UntilEventMiddleware($until); - - $message = Message::create( - new ProfileCreated( - ProfileId::fromString('1'), - Email::fromString('info@patchlevel.de'), - ), - )->withHeader(new AggregateHeader('pofile', '1', 1, new DateTimeImmutable('2020-02-01 00:00:00'))); - - $result = $middleware($message); - - self::assertSame([], $result); - } - - public function testWithoutHeader(): void - { - $until = new DateTimeImmutable('2020-01-01 00:00:00'); - - $middleware = new UntilEventMiddleware($until); - - $event = new ProfileCreated( - ProfileId::fromString('1'), - Email::fromString('hallo@patchlevel.de'), - ); - - $message = Message::create($event); - - $result = $middleware($message); - - self::assertSame([$message], $result); - } -} diff --git a/tests/Unit/Pipeline/PipeTest.php b/tests/Unit/Pipeline/PipeTest.php deleted file mode 100644 index 89906eb97..000000000 --- a/tests/Unit/Pipeline/PipeTest.php +++ /dev/null @@ -1,205 +0,0 @@ -messages(); - - $stream = new Pipe($messages); - - $resultMessages = iterator_to_array($stream); - - self::assertSame($messages, $resultMessages); - } - - public function testWithMiddlewares(): void - { - $messages = $this->messages(); - - $stream = new Pipe( - $messages, - [ - new ExcludeEventMiddleware([ProfileCreated::class]), - new RecalculatePlayheadMiddleware(), - ], - ); - - $resultMessages = iterator_to_array($stream); - - self::assertCount(3, $resultMessages); - - self::assertInstanceOf(ProfileVisited::class, $resultMessages[0]->event()); - self::assertSame('1', $resultMessages[0]->header(AggregateHeader::class)->aggregateId); - self::assertSame(1, $resultMessages[0]->header(AggregateHeader::class)->playhead); - - self::assertInstanceOf(ProfileVisited::class, $resultMessages[1]->event()); - self::assertSame('1', $resultMessages[1]->header(AggregateHeader::class)->aggregateId); - self::assertSame(2, $resultMessages[1]->header(AggregateHeader::class)->playhead); - - self::assertInstanceOf(ProfileVisited::class, $resultMessages[2]->event()); - self::assertSame('2', $resultMessages[2]->header(AggregateHeader::class)->aggregateId); - self::assertSame(1, $resultMessages[2]->header(AggregateHeader::class)->playhead); - } - - public function testAppendMiddleware(): void - { - $messages = $this->messages(); - - $stream = new Pipe( - $messages, - [ - new ExcludeEventMiddleware([ProfileCreated::class]), - ], - ); - - $stream = $stream->appendMiddleware( - new RecalculatePlayheadMiddleware(), - ); - - $resultMessages = iterator_to_array($stream); - - self::assertCount(3, $resultMessages); - - self::assertInstanceOf(ProfileVisited::class, $resultMessages[0]->event()); - self::assertSame('1', $resultMessages[0]->header(AggregateHeader::class)->aggregateId); - self::assertSame(1, $resultMessages[0]->header(AggregateHeader::class)->playhead); - - self::assertInstanceOf(ProfileVisited::class, $resultMessages[1]->event()); - self::assertSame('1', $resultMessages[1]->header(AggregateHeader::class)->aggregateId); - self::assertSame(2, $resultMessages[1]->header(AggregateHeader::class)->playhead); - - self::assertInstanceOf(ProfileVisited::class, $resultMessages[2]->event()); - self::assertSame('2', $resultMessages[2]->header(AggregateHeader::class)->aggregateId); - self::assertSame(1, $resultMessages[2]->header(AggregateHeader::class)->playhead); - } - - public function testPrependMiddleware(): void - { - $messages = $this->messages(); - - $stream = new Pipe( - $messages, - [ - new RecalculatePlayheadMiddleware(), - ], - ); - - $stream = $stream->prependMiddleware( - new ExcludeEventMiddleware([ProfileCreated::class]), - ); - - $resultMessages = iterator_to_array($stream); - - self::assertCount(3, $resultMessages); - - self::assertInstanceOf(ProfileVisited::class, $resultMessages[0]->event()); - self::assertSame('1', $resultMessages[0]->header(AggregateHeader::class)->aggregateId); - self::assertSame(1, $resultMessages[0]->header(AggregateHeader::class)->playhead); - - self::assertInstanceOf(ProfileVisited::class, $resultMessages[1]->event()); - self::assertSame('1', $resultMessages[1]->header(AggregateHeader::class)->aggregateId); - self::assertSame(2, $resultMessages[1]->header(AggregateHeader::class)->playhead); - - self::assertInstanceOf(ProfileVisited::class, $resultMessages[2]->event()); - self::assertSame('2', $resultMessages[2]->header(AggregateHeader::class)->aggregateId); - self::assertSame(1, $resultMessages[2]->header(AggregateHeader::class)->playhead); - } - - /** @return list */ - private function messages(): array - { - return [ - Message::create( - new ProfileCreated( - ProfileId::fromString('1'), - Email::fromString('hallo@patchlevel.de'), - ), - ) - ->withHeader(new AggregateHeader( - 'profile', - '1', - 1, - new DateTimeImmutable(), - )), - Message::create( - new ProfileVisited( - ProfileId::fromString('1'), - ), - ) - ->withHeader(new AggregateHeader( - 'profile', - '1', - 2, - new DateTimeImmutable(), - )), - Message::create( - new ProfileVisited( - ProfileId::fromString('1'), - ), - ) - ->withHeader(new AggregateHeader( - 'profile', - '1', - 3, - new DateTimeImmutable(), - )), - - Message::create( - new ProfileCreated( - ProfileId::fromString('2'), - Email::fromString('hallo@patchlevel.de'), - ), - ) - ->withHeader(new AggregateHeader( - 'profile', - '2', - 1, - new DateTimeImmutable(), - )), - - Message::create( - new ProfileVisited( - ProfileId::fromString('2'), - ), - ) - ->withHeader(new AggregateHeader( - 'profile', - '2', - 2, - new DateTimeImmutable(), - )), - ]; - } -} diff --git a/tests/Unit/Pipeline/PipelineTest.php b/tests/Unit/Pipeline/PipelineTest.php deleted file mode 100644 index 8adab8256..000000000 --- a/tests/Unit/Pipeline/PipelineTest.php +++ /dev/null @@ -1,207 +0,0 @@ -messages(); - - $target = new InMemoryTarget(); - $pipeline = new Pipeline($target); - - $pipeline->run($messages); - - self::assertSame($messages, $target->messages()); - } - - public function testPipelineWithOneMiddleware(): void - { - $messages = $this->messages(); - - $target = new InMemoryTarget(); - $pipeline = new Pipeline( - $target, - new ExcludeEventMiddleware([ProfileCreated::class]), - ); - - $pipeline->run($messages); - - $resultMessages = $target->messages(); - - self::assertCount(3, $resultMessages); - - self::assertInstanceOf(ProfileVisited::class, $resultMessages[0]->event()); - self::assertSame('1', $resultMessages[0]->header(AggregateHeader::class)->aggregateId); - - self::assertInstanceOf(ProfileVisited::class, $resultMessages[1]->event()); - self::assertSame('1', $resultMessages[1]->header(AggregateHeader::class)->aggregateId); - - self::assertInstanceOf(ProfileVisited::class, $resultMessages[2]->event()); - self::assertSame('2', $resultMessages[2]->header(AggregateHeader::class)->aggregateId); - } - - public function testPipelineWithMiddleware(): void - { - $messages = $this->messages(); - - $target = new InMemoryTarget(); - $pipeline = new Pipeline( - $target, - [ - new ExcludeEventMiddleware([ProfileCreated::class]), - new RecalculatePlayheadMiddleware(), - ], - ); - - $pipeline->run($messages); - - $resultMessages = $target->messages(); - - self::assertCount(3, $resultMessages); - - self::assertInstanceOf(ProfileVisited::class, $resultMessages[0]->event()); - self::assertSame('1', $resultMessages[0]->header(AggregateHeader::class)->aggregateId); - self::assertSame(1, $resultMessages[0]->header(AggregateHeader::class)->playhead); - - self::assertInstanceOf(ProfileVisited::class, $resultMessages[1]->event()); - self::assertSame('1', $resultMessages[1]->header(AggregateHeader::class)->aggregateId); - self::assertSame(2, $resultMessages[1]->header(AggregateHeader::class)->playhead); - - self::assertInstanceOf(ProfileVisited::class, $resultMessages[2]->event()); - self::assertSame('2', $resultMessages[2]->header(AggregateHeader::class)->aggregateId); - self::assertSame(1, $resultMessages[2]->header(AggregateHeader::class)->playhead); - } - - public function testBatching(): void - { - $messages = $this->messages(); - - $target = $this->prophesize(Target::class); - - $target->save($messages[0], $messages[1], $messages[2])->shouldBeCalled(); - $target->save($messages[3], $messages[4])->shouldBeCalled(); - - $pipeline = new Pipeline($target->reveal(), bufferSize: 3); - - $pipeline->run($messages); - } - - public function testBatchingInf(): void - { - $messages = $this->messages(); - - $target = $this->prophesize(Target::class); - $target->save(...$messages)->shouldBeCalled(); - - $pipeline = new Pipeline($target->reveal(), bufferSize: INF); - - $pipeline->run($messages); - } - - public function testBatchingNothing(): void - { - $messages = $this->messages(); - - $target = $this->prophesize(Target::class); - - $target->save($messages[0])->shouldBeCalled(); - $target->save($messages[1])->shouldBeCalled(); - $target->save($messages[2])->shouldBeCalled(); - $target->save($messages[3])->shouldBeCalled(); - $target->save($messages[4])->shouldBeCalled(); - - $pipeline = new Pipeline($target->reveal(), bufferSize: 0); - - $pipeline->run($messages); - } - - /** @return list */ - private function messages(): array - { - return [ - Message::create( - new ProfileCreated( - ProfileId::fromString('1'), - Email::fromString('hallo@patchlevel.de'), - ), - ) - ->withHeader(new AggregateHeader( - 'profile', - '1', - 1, - new DateTimeImmutable(), - )), - Message::create( - new ProfileVisited( - ProfileId::fromString('1'), - ), - ) - ->withHeader(new AggregateHeader( - 'profile', - '1', - 2, - new DateTimeImmutable(), - )), - Message::create( - new ProfileVisited( - ProfileId::fromString('1'), - ), - ) - ->withHeader(new AggregateHeader( - 'profile', - '1', - 3, - new DateTimeImmutable(), - )), - - Message::create( - new ProfileCreated( - ProfileId::fromString('2'), - Email::fromString('hallo@patchlevel.de'), - ), - ) - ->withHeader(new AggregateHeader( - 'profile', - '2', - 1, - new DateTimeImmutable(), - )), - - Message::create( - new ProfileVisited( - ProfileId::fromString('2'), - ), - ) - ->withHeader(new AggregateHeader( - 'profile', - '2', - 2, - new DateTimeImmutable(), - )), - ]; - } -} diff --git a/tests/Unit/Pipeline/Target/EventBusTargetTest.php b/tests/Unit/Pipeline/Target/EventBusTargetTest.php deleted file mode 100644 index c9f7fd63c..000000000 --- a/tests/Unit/Pipeline/Target/EventBusTargetTest.php +++ /dev/null @@ -1,34 +0,0 @@ -prophesize(EventBus::class); - $pipelineStore->dispatch($message)->shouldBeCalled(); - - $storeTarget = new EventBusTarget($pipelineStore->reveal()); - - $storeTarget->save($message); - } -} diff --git a/tests/Unit/Pipeline/Target/InMemoryTargetTest.php b/tests/Unit/Pipeline/Target/InMemoryTargetTest.php deleted file mode 100644 index e5aadb306..000000000 --- a/tests/Unit/Pipeline/Target/InMemoryTargetTest.php +++ /dev/null @@ -1,46 +0,0 @@ -save($message); - - $messages = $inMemoryTarget->messages(); - - self::assertSame([$message], $messages); - } - - public function testClear(): void - { - $inMemoryTarget = new InMemoryTarget(); - - $message = new Message( - new ProfileCreated(ProfileId::fromString('1'), Email::fromString('foo@test.com')), - ); - - $inMemoryTarget->save($message); - $inMemoryTarget->clear(); - - $messages = $inMemoryTarget->messages(); - - self::assertSame([], $messages); - } -} diff --git a/tests/Unit/Pipeline/Target/StoreTargetTest.php b/tests/Unit/Pipeline/Target/StoreTargetTest.php deleted file mode 100644 index 187ac642a..000000000 --- a/tests/Unit/Pipeline/Target/StoreTargetTest.php +++ /dev/null @@ -1,34 +0,0 @@ -prophesize(Store::class); - $pipelineStore->save($message)->shouldBeCalled(); - - $storeTarget = new StoreTarget($pipelineStore->reveal()); - - $storeTarget->save($message); - } -} From af44aa1b610d41ba0c925cc94a6e46077893d6a7 Mon Sep 17 00:00:00 2001 From: David Badura Date: Tue, 22 Oct 2024 09:07:54 +0200 Subject: [PATCH 07/12] refactoring --- baseline.xml | 134 ++----------- src/Message/{Pipeline.php => Pipe.php} | 44 ++--- src/Message/Reducer.php | 52 ++--- .../Translator/ReplaceEventTranslator.php | 4 +- ...igrateAggregateToStreamStoreSubscriber.php | 25 +-- tests/Unit/Message/MessageTest.php | 38 +++- tests/Unit/Message/PipeTest.php | 176 +++++++++++++++++ tests/Unit/Message/ReducerTest.php | 183 ++++++++++++++++++ 8 files changed, 441 insertions(+), 215 deletions(-) rename src/Message/{Pipeline.php => Pipe.php} (56%) create mode 100644 tests/Unit/Message/PipeTest.php create mode 100644 tests/Unit/Message/ReducerTest.php diff --git a/baseline.xml b/baseline.xml index ef6bd6cc0..53f5d4abc 100644 --- a/baseline.xml +++ b/baseline.xml @@ -32,58 +32,22 @@ getName()]]> + + + ]]> + + + + + handlers]]> + + + - - - - - ]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -260,82 +224,6 @@ - - - reveal(), - $child2->reveal(), - ])]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - profileId, - ); - }, - )]]> - profileId, - ); - }, - )]]> - - - - - - - - diff --git a/src/Message/Pipeline.php b/src/Message/Pipe.php similarity index 56% rename from src/Message/Pipeline.php rename to src/Message/Pipe.php index cfba44b49..ae485a870 100644 --- a/src/Message/Pipeline.php +++ b/src/Message/Pipe.php @@ -10,33 +10,25 @@ use Patchlevel\EventSourcing\Message\Translator\Translator; use Traversable; -/** @implements IteratorAggregate */ -final class Pipeline implements IteratorAggregate +use function array_values; +use function iterator_to_array; + +/** @implements IteratorAggregate */ +final class Pipe implements IteratorAggregate { + private Translator $translator; + /** - * @param iterable $messages - * @param list $translators + * @param iterable $messages + * @param list|Translator $translators */ public function __construct( private readonly iterable $messages, - private readonly array $translators = [], + array|Translator $translators = [], ) { - } - - public function appendMiddleware(Translator $translator): self - { - return new self( - $this->messages, - [...$this->translators, $translator], - ); - } - - public function prependMiddleware(Translator $translator): self - { - return new self( - $this->messages, - [$translator, ...$this->translators], - ); + $this->translator = $translators instanceof Translator + ? $translators + : new ChainTranslator($translators); } /** @return Traversable */ @@ -44,16 +36,16 @@ public function getIterator(): Traversable { return $this->createGenerator( $this->messages, - new ChainTranslator($this->translators), + $this->translator, ); } - /** - * @return list - */ + /** @return list */ public function toArray(): array { - return iterator_to_array($this->getIterator()); + return array_values( + iterator_to_array($this->getIterator()), + ); } /** diff --git a/src/Message/Reducer.php b/src/Message/Reducer.php index ae1002faa..d2dee5659 100644 --- a/src/Message/Reducer.php +++ b/src/Message/Reducer.php @@ -1,9 +1,10 @@ @@ -11,31 +12,18 @@ */ final class Reducer { - /** - * @var STATE - */ + /** @var STATE */ private array $initState = []; - /** - * @var array> - */ + /** @var array> */ private array $handlers = []; - /** - * @var list - */ + /** @var list */ private array $anyHandlers = []; - /** - * @var (Closure(STATE): OUT)|null - */ + /** @var (Closure(STATE): OUT)|null */ private Closure|null $finalizeHandler = null; - /** - * @var list - */ - private array $translators = []; - /** * @param STATE $initState * @@ -49,15 +37,19 @@ public function initState(array $initState): self } /** - * @template T1 of object - * - * @param class-string $event + * @param class-string $event * @param Closure(Message, STATE): STATE $closure * * @return $this + * + * @template T1 of object */ public function when(string $event, Closure $closure): self { + if (!isset($this->handlers[$event])) { + $this->handlers[$event] = []; + } + $this->handlers[$event][] = $closure; return $this; @@ -101,26 +93,14 @@ public function finalize(Closure $closure): self return $this; } - public function translator(Translator ...$translators): self - { - foreach ($translators as $translator) { - $this->translators[] = $translator; - } - - return $this; - } - /** * @param iterable $messages * - * @return OUT + * @return OUT|STATE + * @psalm-return (OUT is STATE ? STATE : OUT) */ public function reduce(iterable $messages): array { - if ($this->translators !== []) { - $messages = new Pipeline($messages, $this->translators); - } - $state = $this->initState; foreach ($messages as $message) { @@ -143,4 +123,4 @@ public function reduce(iterable $messages): array return $state; } -} \ No newline at end of file +} diff --git a/src/Message/Translator/ReplaceEventTranslator.php b/src/Message/Translator/ReplaceEventTranslator.php index 597379229..3e13fa416 100644 --- a/src/Message/Translator/ReplaceEventTranslator.php +++ b/src/Message/Translator/ReplaceEventTranslator.php @@ -6,9 +6,7 @@ use Patchlevel\EventSourcing\Message\Message; -/** - * @template T of object - */ +/** @template T of object */ final class ReplaceEventTranslator implements Translator { /** @var callable(T $event):object */ diff --git a/tests/Integration/Subscription/Subscriber/MigrateAggregateToStreamStoreSubscriber.php b/tests/Integration/Subscription/Subscriber/MigrateAggregateToStreamStoreSubscriber.php index 22e95982a..fd674133d 100644 --- a/tests/Integration/Subscription/Subscriber/MigrateAggregateToStreamStoreSubscriber.php +++ b/tests/Integration/Subscription/Subscriber/MigrateAggregateToStreamStoreSubscriber.php @@ -9,15 +9,16 @@ use Patchlevel\EventSourcing\Attribute\Subscriber; use Patchlevel\EventSourcing\Attribute\Teardown; use Patchlevel\EventSourcing\Message\Message; -use Patchlevel\EventSourcing\Message\Pipeline; +use Patchlevel\EventSourcing\Message\Pipe; use Patchlevel\EventSourcing\Message\Translator\AggregateToStreamHeaderTranslator; -use Patchlevel\EventSourcing\Pipeline\Middleware\Middleware; +use Patchlevel\EventSourcing\Message\Translator\Translator; use Patchlevel\EventSourcing\Schema\ChainDoctrineSchemaConfigurator; use Patchlevel\EventSourcing\Schema\DoctrineSchemaDirector; use Patchlevel\EventSourcing\Schema\SchemaDirector; use Patchlevel\EventSourcing\Store\StreamDoctrineDbalStore; use Patchlevel\EventSourcing\Subscription\RunMode; use Patchlevel\EventSourcing\Subscription\Subscriber\BatchableSubscriber; + use function count; #[Subscriber('migrate', RunMode::Once)] @@ -28,9 +29,7 @@ final class MigrateAggregateToStreamStoreSubscriber implements BatchableSubscrib /** @var list */ private array $messages = []; - /** - * @var list - */ + /** @var list */ private readonly array $middlewares; public function __construct( @@ -41,9 +40,7 @@ public function __construct( new ChainDoctrineSchemaConfigurator([$targetStore]), ); - $this->middlewares = [ - new AggregateToStreamHeaderTranslator() - ]; + $this->middlewares = [new AggregateToStreamHeaderTranslator()]; } #[Subscribe('*')] @@ -52,16 +49,6 @@ public function handle(Message $message): void $this->messages[] = $message; } - #[Subscribe('*')] - public function kafka(Message $message): void - { - $pipeline = new Pipeline([$message], $this->middlewares); - - foreach ($pipeline as $message) { - $this->kafka->publish($message); - } - } - public function beginBatch(): void { $this->messages = []; @@ -69,7 +56,7 @@ public function beginBatch(): void public function commitBatch(): void { - $pipeline = new Pipeline($this->messages, $this->middlewares); + $pipeline = new Pipe($this->messages, $this->middlewares); $this->messages = []; $this->targetStore->save(...$pipeline); diff --git a/tests/Unit/Message/MessageTest.php b/tests/Unit/Message/MessageTest.php index d04bfae5d..c901c1641 100644 --- a/tests/Unit/Message/MessageTest.php +++ b/tests/Unit/Message/MessageTest.php @@ -18,7 +18,7 @@ /** @covers \Patchlevel\EventSourcing\Message\Message */ final class MessageTest extends TestCase { - public function testEmptyMessage(): void + public function testMessage(): void { $event = new ProfileCreated( ProfileId::fromString('1'), @@ -28,13 +28,6 @@ public function testEmptyMessage(): void $message = new Message($event); self::assertEquals($event, $message->event()); - } - - public function testEmptyAllHeaders(): void - { - $message = Message::create(new class { - }); - self::assertSame([], $message->headers()); } @@ -98,6 +91,20 @@ public function testCreateWithAllHeaders(): void self::assertSame($headers, $message->headers()); } + public function testHasHeader(): void + { + $message = Message::create(new class { + })->withHeader(new AggregateHeader( + 'profile', + '1', + 1, + new DateTimeImmutable('2020-05-06 13:34:24'), + )); + + self::assertTrue($message->hasHeader(AggregateHeader::class)); + self::assertFalse($message->hasHeader(ArchivedHeader::class)); + } + public function testChangeHeader(): void { $message = Message::create(new class { @@ -118,6 +125,21 @@ public function testChangeHeader(): void self::assertSame(2, $message->header(AggregateHeader::class)->playhead); } + public function testRemoveHeader(): void + { + $message = Message::create(new class { + })->withHeader(new AggregateHeader( + 'profile', + '1', + 1, + new DateTimeImmutable('2020-05-06 13:34:24'), + )); + + $message = $message->removeHeader(AggregateHeader::class); + + self::assertFalse($message->hasHeader(AggregateHeader::class)); + } + public function testHeaderNotFound(): void { $message = Message::create(new class { diff --git a/tests/Unit/Message/PipeTest.php b/tests/Unit/Message/PipeTest.php new file mode 100644 index 000000000..4383545d6 --- /dev/null +++ b/tests/Unit/Message/PipeTest.php @@ -0,0 +1,176 @@ +messages(); + + $stream = new Pipe($messages); + + $resultMessages = iterator_to_array($stream); + + self::assertSame($messages, $resultMessages); + } + + public function testToArray(): void + { + $messages = $this->messages(); + + $stream = new Pipe($messages); + + self::assertSame($messages, $stream->toArray()); + } + + public function testWithOneMiddleware(): void + { + $messages = $this->messages(); + + $stream = new Pipe( + $messages, + new ExcludeEventTranslator([ProfileCreated::class]), + ); + + $resultMessages = iterator_to_array($stream); + + self::assertCount(3, $resultMessages); + + self::assertInstanceOf(ProfileVisited::class, $resultMessages[0]->event()); + self::assertSame('1', $resultMessages[0]->header(AggregateHeader::class)->aggregateId); + self::assertSame(2, $resultMessages[0]->header(AggregateHeader::class)->playhead); + + self::assertInstanceOf(ProfileVisited::class, $resultMessages[1]->event()); + self::assertSame('1', $resultMessages[1]->header(AggregateHeader::class)->aggregateId); + self::assertSame(3, $resultMessages[1]->header(AggregateHeader::class)->playhead); + + self::assertInstanceOf(ProfileVisited::class, $resultMessages[2]->event()); + self::assertSame('2', $resultMessages[2]->header(AggregateHeader::class)->aggregateId); + self::assertSame(2, $resultMessages[2]->header(AggregateHeader::class)->playhead); + } + + public function testWithMiddlewares(): void + { + $messages = $this->messages(); + + $stream = new Pipe( + $messages, + [ + new ExcludeEventTranslator([ProfileCreated::class]), + new RecalculatePlayheadTranslator(), + ], + ); + + $resultMessages = iterator_to_array($stream); + + self::assertCount(3, $resultMessages); + + self::assertInstanceOf(ProfileVisited::class, $resultMessages[0]->event()); + self::assertSame('1', $resultMessages[0]->header(AggregateHeader::class)->aggregateId); + self::assertSame(1, $resultMessages[0]->header(AggregateHeader::class)->playhead); + + self::assertInstanceOf(ProfileVisited::class, $resultMessages[1]->event()); + self::assertSame('1', $resultMessages[1]->header(AggregateHeader::class)->aggregateId); + self::assertSame(2, $resultMessages[1]->header(AggregateHeader::class)->playhead); + + self::assertInstanceOf(ProfileVisited::class, $resultMessages[2]->event()); + self::assertSame('2', $resultMessages[2]->header(AggregateHeader::class)->aggregateId); + self::assertSame(1, $resultMessages[2]->header(AggregateHeader::class)->playhead); + } + + /** @return list */ + private function messages(): array + { + return [ + Message::create( + new ProfileCreated( + ProfileId::fromString('1'), + Email::fromString('hallo@patchlevel.de'), + ), + ) + ->withHeader(new AggregateHeader( + 'profile', + '1', + 1, + new DateTimeImmutable(), + )), + Message::create( + new ProfileVisited( + ProfileId::fromString('1'), + ), + ) + ->withHeader(new AggregateHeader( + 'profile', + '1', + 2, + new DateTimeImmutable(), + )), + Message::create( + new ProfileVisited( + ProfileId::fromString('1'), + ), + ) + ->withHeader(new AggregateHeader( + 'profile', + '1', + 3, + new DateTimeImmutable(), + )), + + Message::create( + new ProfileCreated( + ProfileId::fromString('2'), + Email::fromString('hallo@patchlevel.de'), + ), + ) + ->withHeader(new AggregateHeader( + 'profile', + '2', + 1, + new DateTimeImmutable(), + )), + + Message::create( + new ProfileVisited( + ProfileId::fromString('2'), + ), + ) + ->withHeader(new AggregateHeader( + 'profile', + '2', + 2, + new DateTimeImmutable(), + )), + ]; + } +} diff --git a/tests/Unit/Message/ReducerTest.php b/tests/Unit/Message/ReducerTest.php new file mode 100644 index 000000000..edfb1a149 --- /dev/null +++ b/tests/Unit/Message/ReducerTest.php @@ -0,0 +1,183 @@ +reduce([]); + + self::assertSame([], $state); + } + + public function testWithMessages(): void + { + $messages = $this->messages(); + + $reducer = new Reducer(); + $state = $reducer->reduce($messages); + + self::assertSame([], $state); + } + + public function testInitState(): void + { + $state = (new Reducer()) + ->initState(['count' => 0]) + ->reduce([]); + + self::assertSame(['count' => 0], $state); + } + + public function testAny(): void + { + $messages = $this->messages(); + + $state = (new Reducer()) + ->any(static function (Message $message, array $state): array { + return [...$state, $message]; + }) + ->reduce($messages); + + self::assertSame($messages, $state); + } + + public function testWhen(): void + { + $messages = $this->messages(); + + $state = (new Reducer()) + ->when( + ProfileCreated::class, + static function (Message $message, array $state): array { + return [...$state, $message]; + }, + ) + ->reduce($messages); + + self::assertSame([$messages[0], $messages[3]], $state); + } + + public function testMatch(): void + { + $messages = $this->messages(); + + $state = (new Reducer()) + ->match([ + ProfileCreated::class => static function (Message $message, array $state): array { + return [...$state, $message]; + }, + ]) + ->reduce($messages); + + self::assertSame([$messages[0], $messages[3]], $state); + } + + public function testFinalize(): void + { + $messages = $this->messages(); + + $state = (new Reducer()) + ->any(static function (Message $message, array $state): array { + return [...$state, $message]; + }) + ->finalize(static function (array $state): array { + return [ + 'count' => count($state), + 'messages' => $state, + ]; + }) + ->reduce($messages); + + self::assertSame([ + 'count' => 5, + 'messages' => $messages, + ], $state); + } + + /** @return list */ + private function messages(): array + { + return [ + Message::create( + new ProfileCreated( + ProfileId::fromString('1'), + Email::fromString('hallo@patchlevel.de'), + ), + ) + ->withHeader(new AggregateHeader( + 'profile', + '1', + 1, + new DateTimeImmutable(), + )), + Message::create( + new ProfileVisited( + ProfileId::fromString('1'), + ), + ) + ->withHeader(new AggregateHeader( + 'profile', + '1', + 2, + new DateTimeImmutable(), + )), + Message::create( + new ProfileVisited( + ProfileId::fromString('1'), + ), + ) + ->withHeader(new AggregateHeader( + 'profile', + '1', + 3, + new DateTimeImmutable(), + )), + + Message::create( + new ProfileCreated( + ProfileId::fromString('2'), + Email::fromString('hallo@patchlevel.de'), + ), + ) + ->withHeader(new AggregateHeader( + 'profile', + '2', + 1, + new DateTimeImmutable(), + )), + + Message::create( + new ProfileVisited( + ProfileId::fromString('2'), + ), + ) + ->withHeader(new AggregateHeader( + 'profile', + '2', + 2, + new DateTimeImmutable(), + )), + ]; + } +} From 7cd0c980fd59c8f04912fa8d57b5d559e1d9eef2 Mon Sep 17 00:00:00 2001 From: David Badura Date: Tue, 22 Oct 2024 10:11:45 +0200 Subject: [PATCH 08/12] update docs & add tests --- docs/mkdocs.yml | 1 - docs/pages/message.md | 60 ++++++++++++++++ docs/pages/subscription.md | 2 +- .../BasicIntegrationTest.php | 69 +++++++++++++++++++ .../Events/NameChanged.php | 16 +++++ .../BasicImplementation/Profile.php | 12 ++++ 6 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 tests/Integration/BasicImplementation/Events/NameChanged.php diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 4850d4dac..790f9a724 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -106,7 +106,6 @@ nav: - Upcasting: upcasting.md - Message Decorator: message_decorator.md - Split Stream: split_stream.md - - Pipeline / ACL: pipeline.md - Time / Clock: clock.md - Testing: testing.md - CLI: cli.md diff --git a/docs/pages/message.md b/docs/pages/message.md index 398ddb170..8547416b0 100644 --- a/docs/pages/message.md +++ b/docs/pages/message.md @@ -97,6 +97,66 @@ use Patchlevel\EventSourcing\Message\Message; /** @var Message $message */ $message->header(ApplicationHeader::class); ``` +## Pipe + +```php +$messages = new Pipe( + $messages, + new ExcludeEventTranslator([ProfileCreated::class]), +); + +foreach ($messages as $message) { + // do something with the message +} +``` +## Reducer + +### Initial state + +```php +$state = (new Reducer()) + ->initialState(['count' => 0]) + ->reduce($messages); + +// state is ['count' => 0] +``` +### When + +```php +$state = (new Reducer()) + ->initialState([ + 'names' => [], + ]) + ->when( + ProfileCreated::class, + static function (Message $message, array $state): array { + $state['names'][] = $message->event()->name; + + return $state; + }, + ) + ->reduce($messages); + +// state is ['names' => ['foo', 'bar']] +``` +### Match + +```php +$state = (new Reducer()) + ->match([ + ProfileCreated::class => static function (Message $message, array $state): array { + return [...$state, $message]; + }, + ]) + ->reduce($messages); +``` +### Any + + +### Finalize + + + ## Translator Translator can be used to manipulate, filter or expand messages or events. diff --git a/docs/pages/subscription.md b/docs/pages/subscription.md index 2851e564f..b7e1335dc 100644 --- a/docs/pages/subscription.md +++ b/docs/pages/subscription.md @@ -498,7 +498,7 @@ At this step, you must process all the data. The `rollbackBatch` method is called when an error occurs and the batching needs to be aborted. Here, you can respond to the error and potentially perform a database rollback. -The method `forceCommit` is called after each handled event, +The method `forceCommit` is called after each handled event, and you can decide whether the batch commit process should start now. This helps to determine the batch size and thus avoid memory overflow. diff --git a/tests/Integration/BasicImplementation/BasicIntegrationTest.php b/tests/Integration/BasicImplementation/BasicIntegrationTest.php index 532dba18d..81e3a5e09 100644 --- a/tests/Integration/BasicImplementation/BasicIntegrationTest.php +++ b/tests/Integration/BasicImplementation/BasicIntegrationTest.php @@ -4,20 +4,30 @@ namespace Patchlevel\EventSourcing\Tests\Integration\BasicImplementation; +use DateTimeImmutable; use Doctrine\DBAL\Connection; +use Patchlevel\EventSourcing\Message\Message; +use Patchlevel\EventSourcing\Message\Pipe; +use Patchlevel\EventSourcing\Message\Reducer; use Patchlevel\EventSourcing\Message\Serializer\DefaultHeadersSerializer; +use Patchlevel\EventSourcing\Message\Translator\UntilEventTranslator; use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry; use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager; use Patchlevel\EventSourcing\Schema\DoctrineSchemaDirector; use Patchlevel\EventSourcing\Serializer\DefaultEventSerializer; use Patchlevel\EventSourcing\Snapshot\Adapter\InMemorySnapshotAdapter; use Patchlevel\EventSourcing\Snapshot\DefaultSnapshotStore; +use Patchlevel\EventSourcing\Store\Criteria\AggregateIdCriterion; +use Patchlevel\EventSourcing\Store\Criteria\AggregateNameCriterion; +use Patchlevel\EventSourcing\Store\Criteria\Criteria; use Patchlevel\EventSourcing\Store\DoctrineDbalStore; use Patchlevel\EventSourcing\Subscription\Engine\DefaultSubscriptionEngine; use Patchlevel\EventSourcing\Subscription\Repository\RunSubscriptionEngineRepositoryManager; use Patchlevel\EventSourcing\Subscription\Store\InMemorySubscriptionStore; use Patchlevel\EventSourcing\Subscription\Subscriber\MetadataSubscriberAccessorRepository; use Patchlevel\EventSourcing\Tests\DbalManager; +use Patchlevel\EventSourcing\Tests\Integration\BasicImplementation\Events\NameChanged; +use Patchlevel\EventSourcing\Tests\Integration\BasicImplementation\Events\ProfileCreated; use Patchlevel\EventSourcing\Tests\Integration\BasicImplementation\MessageDecorator\FooMessageDecorator; use Patchlevel\EventSourcing\Tests\Integration\BasicImplementation\Processor\SendEmailProcessor; use Patchlevel\EventSourcing\Tests\Integration\BasicImplementation\Projection\ProfileProjector; @@ -170,4 +180,63 @@ public function testSnapshot(): void self::assertSame('John', $profile->name()); self::assertSame(1, SendEmailMock::count()); } + + public function testTempProjection(): void + { + $store = new DoctrineDbalStore( + $this->connection, + DefaultEventSerializer::createFromPaths([__DIR__ . '/Events']), + DefaultHeadersSerializer::createFromPaths([ + __DIR__ . '/Header', + ]), + ); + + $manager = new DefaultRepositoryManager( + new AggregateRootRegistry(['profile' => Profile::class]), + $store, + null, + new DefaultSnapshotStore(['default' => new InMemorySnapshotAdapter()]), + new FooMessageDecorator(), + ); + + $repository = $manager->get(Profile::class); + + $schemaDirector = new DoctrineSchemaDirector( + $this->connection, + $store, + ); + + $schemaDirector->create(); + + $profileId = ProfileId::generate(); + $profile = Profile::create($profileId, 'John'); + + for ($i = 0; $i < 100; $i++) { + $profile->changeName('John' . $i); + } + + $repository->save($profile); + + $state = (new Reducer()) + ->initState(['name' => 'unknown']) + ->match([ + ProfileCreated::class => static function (Message $message): array { + return ['name' => $message->event()->name]; + }, + NameChanged::class => static function (Message $message): array { + return ['name' => $message->event()->name]; + }, + ]) + ->reduce( + new Pipe( + $store->load(new Criteria( + new AggregateIdCriterion($profileId->toString()), + new AggregateNameCriterion('profile'), + )), + new UntilEventTranslator(new DateTimeImmutable()), + ), + ); + + self::assertSame(['name' => 'John99'], $state); + } } diff --git a/tests/Integration/BasicImplementation/Events/NameChanged.php b/tests/Integration/BasicImplementation/Events/NameChanged.php new file mode 100644 index 000000000..32bc6f223 --- /dev/null +++ b/tests/Integration/BasicImplementation/Events/NameChanged.php @@ -0,0 +1,16 @@ +recordThat(new NameChanged($name)); + } + #[Apply(ProfileCreated::class)] protected function applyProfileCreated(ProfileCreated $event): void { @@ -34,6 +40,12 @@ protected function applyProfileCreated(ProfileCreated $event): void $this->name = $event->name; } + #[Apply(NameChanged::class)] + protected function applyNameChanged(NameChanged $event): void + { + $this->name = $event->name; + } + public function name(): string { return $this->name; From f427528712678e612f282f5c0c5d59eef28c7e64 Mon Sep 17 00:00:00 2001 From: David Badura Date: Sat, 26 Oct 2024 21:46:26 +0200 Subject: [PATCH 09/12] simplify code Co-authored-by: Daniel Badura --- src/Message/Pipe.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Message/Pipe.php b/src/Message/Pipe.php index ae485a870..5b9750792 100644 --- a/src/Message/Pipe.php +++ b/src/Message/Pipe.php @@ -56,11 +56,7 @@ public function toArray(): array private function createGenerator(iterable $messages, Translator $translator): Generator { foreach ($messages as $message) { - $result = $translator($message); - - foreach ($result as $m) { - yield $m; - } + yield from $translator($message); } } } From 17921fef9b8d0994bc783241dcaf4bf6c8084fff Mon Sep 17 00:00:00 2001 From: David Badura Date: Tue, 5 Nov 2024 11:01:56 +0100 Subject: [PATCH 10/12] add more integration test --- .../IntegrationTest.php | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/tests/Integration/BankAccountSplitStream/IntegrationTest.php b/tests/Integration/BankAccountSplitStream/IntegrationTest.php index 120b0886d..b2f59aef6 100644 --- a/tests/Integration/BankAccountSplitStream/IntegrationTest.php +++ b/tests/Integration/BankAccountSplitStream/IntegrationTest.php @@ -154,4 +154,119 @@ public function testSuccessful(): void self::assertInstanceOf(MonthPassed::class, $bankAccount->appliedEvents[0]); self::assertInstanceOf(BalanceAdded::class, $bankAccount->appliedEvents[1]); } + + public function testRemoveArchived(): void + { + $store = new DoctrineDbalStore( + $this->connection, + DefaultEventSerializer::createFromPaths([__DIR__ . '/Events']), + ); + + $bankAccountProjector = new BankAccountProjector($this->connection); + + $engine = new DefaultSubscriptionEngine( + $store, + new InMemorySubscriptionStore(), + new MetadataSubscriberAccessorRepository([$bankAccountProjector]), + ); + + $manager = new DefaultRepositoryManager( + new AggregateRootRegistry(['bank_account' => BankAccount::class]), + $store, + null, + null, + new ChainMessageDecorator([ + new SplitStreamDecorator(new AttributeEventMetadataFactory()), + ]), + ); + $repository = $manager->get(BankAccount::class); + + $schemaDirector = new DoctrineSchemaDirector( + $this->connection, + $store, + ); + + $schemaDirector->create(); + $engine->setup(); + $engine->boot(); + + $bankAccountId = AccountId::generate(); + $bankAccount = BankAccount::create($bankAccountId, 'John'); + $bankAccount->addBalance(100); + $bankAccount->addBalance(500); + $repository->save($bankAccount); + + $engine->run(); + + $result = $this->connection->fetchAssociative( + 'SELECT * FROM projection_bank_account WHERE id = ?', + [$bankAccountId->toString()], + ); + + self::assertIsArray($result); + self::assertArrayHasKey('id', $result); + self::assertSame($bankAccountId->toString(), $result['id']); + self::assertSame('John', $result['name']); + self::assertSame(600, $result['balance_in_cents']); + + $manager = new DefaultRepositoryManager( + new AggregateRootRegistry(['bank_account' => BankAccount::class]), + $store, + null, + null, + new ChainMessageDecorator([ + new SplitStreamDecorator(new AttributeEventMetadataFactory()), + ]), + ); + $repository = $manager->get(BankAccount::class); + $bankAccount = $repository->load($bankAccountId); + + self::assertInstanceOf(BankAccount::class, $bankAccount); + self::assertEquals($bankAccountId, $bankAccount->aggregateRootId()); + self::assertSame(3, $bankAccount->playhead()); + self::assertSame('John', $bankAccount->name()); + self::assertSame(600, $bankAccount->balance()); + self::assertSame(3, count($bankAccount->appliedEvents)); + self::assertInstanceOf(BankAccountCreated::class, $bankAccount->appliedEvents[0]); + self::assertInstanceOf(BalanceAdded::class, $bankAccount->appliedEvents[1]); + self::assertInstanceOf(BalanceAdded::class, $bankAccount->appliedEvents[2]); + + $bankAccount->beginNewMonth(); + $bankAccount->addBalance(200); + $repository->save($bankAccount); + + $engine->run(); + + $result = $this->connection->fetchAssociative( + 'SELECT * FROM projection_bank_account WHERE id = ?', + [$bankAccountId->toString()], + ); + + self::assertIsArray($result); + self::assertArrayHasKey('id', $result); + self::assertSame($bankAccountId->toString(), $result['id']); + self::assertSame('John', $result['name']); + self::assertSame(800, $result['balance_in_cents']); + + $manager = new DefaultRepositoryManager( + new AggregateRootRegistry(['bank_account' => BankAccount::class]), + $store, + null, + null, + new ChainMessageDecorator([ + new SplitStreamDecorator(new AttributeEventMetadataFactory()), + ]), + ); + $repository = $manager->get(BankAccount::class); + $bankAccount = $repository->load($bankAccountId); + + self::assertInstanceOf(BankAccount::class, $bankAccount); + self::assertEquals($bankAccountId, $bankAccount->aggregateRootId()); + self::assertSame(5, $bankAccount->playhead()); + self::assertSame('John', $bankAccount->name()); + self::assertSame(800, $bankAccount->balance()); + self::assertSame(2, count($bankAccount->appliedEvents)); + self::assertInstanceOf(MonthPassed::class, $bankAccount->appliedEvents[0]); + self::assertInstanceOf(BalanceAdded::class, $bankAccount->appliedEvents[1]); + } } From a53aed275140f18cde4adab7f2afa36205f46ce8 Mon Sep 17 00:00:00 2001 From: David Badura Date: Thu, 7 Nov 2024 13:46:17 +0100 Subject: [PATCH 11/12] update api --- src/Message/Pipe.php | 15 ++++++--------- .../MigrateAggregateToStreamStoreSubscriber.php | 2 +- tests/Unit/Message/PipeTest.php | 6 ++---- .../Message/Translator/ChainTranslatorTest.php | 16 +++++++++++++++- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/Message/Pipe.php b/src/Message/Pipe.php index 5b9750792..2c38bcdc3 100644 --- a/src/Message/Pipe.php +++ b/src/Message/Pipe.php @@ -18,17 +18,12 @@ final class Pipe implements IteratorAggregate { private Translator $translator; - /** - * @param iterable $messages - * @param list|Translator $translators - */ + /** @param iterable $messages */ public function __construct( private readonly iterable $messages, - array|Translator $translators = [], + Translator ...$translators, ) { - $this->translator = $translators instanceof Translator - ? $translators - : new ChainTranslator($translators); + $this->translator = new ChainTranslator($translators); } /** @return Traversable */ @@ -56,7 +51,9 @@ public function toArray(): array private function createGenerator(iterable $messages, Translator $translator): Generator { foreach ($messages as $message) { - yield from $translator($message); + foreach ($translator($message) as $translatedMessage) { + yield $translatedMessage; + } } } } diff --git a/tests/Integration/Subscription/Subscriber/MigrateAggregateToStreamStoreSubscriber.php b/tests/Integration/Subscription/Subscriber/MigrateAggregateToStreamStoreSubscriber.php index fd674133d..557bda0ed 100644 --- a/tests/Integration/Subscription/Subscriber/MigrateAggregateToStreamStoreSubscriber.php +++ b/tests/Integration/Subscription/Subscriber/MigrateAggregateToStreamStoreSubscriber.php @@ -56,7 +56,7 @@ public function beginBatch(): void public function commitBatch(): void { - $pipeline = new Pipe($this->messages, $this->middlewares); + $pipeline = new Pipe($this->messages, ...$this->middlewares); $this->messages = []; $this->targetStore->save(...$pipeline); diff --git a/tests/Unit/Message/PipeTest.php b/tests/Unit/Message/PipeTest.php index 4383545d6..1ffcf4d51 100644 --- a/tests/Unit/Message/PipeTest.php +++ b/tests/Unit/Message/PipeTest.php @@ -85,10 +85,8 @@ public function testWithMiddlewares(): void $stream = new Pipe( $messages, - [ - new ExcludeEventTranslator([ProfileCreated::class]), - new RecalculatePlayheadTranslator(), - ], + new ExcludeEventTranslator([ProfileCreated::class]), + new RecalculatePlayheadTranslator(), ); $resultMessages = iterator_to_array($stream); diff --git a/tests/Unit/Message/Translator/ChainTranslatorTest.php b/tests/Unit/Message/Translator/ChainTranslatorTest.php index 6756d3c87..9a1a9a998 100644 --- a/tests/Unit/Message/Translator/ChainTranslatorTest.php +++ b/tests/Unit/Message/Translator/ChainTranslatorTest.php @@ -18,6 +18,20 @@ final class ChainTranslatorTest extends TestCase { use ProphecyTrait; + public function testEmptyChain(): void + { + $message = new Message( + new ProfileCreated( + ProfileId::fromString('1'), + Email::fromString('hallo@patchlevel.de'), + ), + ); + + $translator = new ChainTranslator([]); + + self::assertSame([$message], $translator($message)); + } + public function testChain(): void { $message = new Message( @@ -38,6 +52,6 @@ public function testChain(): void $child2->reveal(), ]); - $translator($message); + self::assertSame([$message], $translator($message)); } } From be9af288951dbf4b35857ae1a531658c0c57f6af Mon Sep 17 00:00:00 2001 From: David Badura Date: Thu, 7 Nov 2024 14:29:31 +0100 Subject: [PATCH 12/12] finish docs --- docs/pages/message.md | 151 ++++++++++++++++++++++++++++-------------- 1 file changed, 102 insertions(+), 49 deletions(-) diff --git a/docs/pages/message.md b/docs/pages/message.md index 8547416b0..e0c3921ed 100644 --- a/docs/pages/message.md +++ b/docs/pages/message.md @@ -99,68 +99,29 @@ $message->header(ApplicationHeader::class); ``` ## Pipe +The `Pipe` is a construct that allows you to chain multiple translators. +This can be used to manipulate, filter or expand messages or events. +This can be used for anti-corruption layers, data migration, or to fix errors in the event stream. + ```php +use Patchlevel\EventSourcing\Message\Pipe; +use Patchlevel\EventSourcing\Message\Translator\ExcludeEventTranslator; +use Patchlevel\EventSourcing\Message\Translator\RecalculatePlayheadTranslator; + $messages = new Pipe( $messages, new ExcludeEventTranslator([ProfileCreated::class]), + new RecalculatePlayheadTranslator(), ); foreach ($messages as $message) { // do something with the message } ``` -## Reducer - -### Initial state - -```php -$state = (new Reducer()) - ->initialState(['count' => 0]) - ->reduce($messages); - -// state is ['count' => 0] -``` -### When - -```php -$state = (new Reducer()) - ->initialState([ - 'names' => [], - ]) - ->when( - ProfileCreated::class, - static function (Message $message, array $state): array { - $state['names'][] = $message->event()->name; - - return $state; - }, - ) - ->reduce($messages); - -// state is ['names' => ['foo', 'bar']] -``` -### Match - -```php -$state = (new Reducer()) - ->match([ - ProfileCreated::class => static function (Message $message, array $state): array { - return [...$state, $message]; - }, - ]) - ->reduce($messages); -``` -### Any - - -### Finalize - - - ## Translator Translator can be used to manipulate, filter or expand messages or events. -This can be used for anti-corruption layers, data migration, or to fix errors in the event stream. +Translators can also be seen as middlewares. ### Exclude @@ -250,6 +211,11 @@ use Patchlevel\EventSourcing\Message\Translator\RecalculatePlayheadTranslator; $translator = new RecalculatePlayheadTranslator(); ``` +!!! warning + + The `RecalculatePlayheadTranslator` is and need to be stateful. + You can't reuse the translator for multiple streams. + !!! tip If you migrate your event stream, you can use the `RecalculatePlayheadTranslator` to fix the playhead. @@ -320,6 +286,93 @@ final class SplitProfileCreatedTranslator implements Translator You don't have to migrate the store directly for every change, but you can also use the [upcasting](upcasting.md) feature. +## Reducer + +The `Reducer` is a construct that allows you to reduce messages to a state. +This can be used to build temporal projections or to create a read model. + +### Initial state + +The initial state is the state that is used at the beginning of the reduction. + +```php +use Patchlevel\EventSourcing\Message\Reducer; + +$state = (new Reducer()) + ->initialState(['count' => 0]) + ->reduce($messages); // state is ['count' => 0] +``` +### When + +The `when` method is used to define a function that is called when a specific event occurs. +It gets the message and the current state and returns the new state. + +```php +use Patchlevel\EventSourcing\Message\Message; +use Patchlevel\EventSourcing\Message\Reducer; + +$state = (new Reducer()) + ->initialState([ + 'names' => [], + ]) + ->when( + ProfileCreated::class, + static function (Message $message, array $state): array { + $state['names'][] = $message->event()->name; + + return $state; + }, + ) + ->reduce($messages); // state is ['names' => ['foo', 'bar']] +``` +### Match + +You can also use the `match` method to define multiple events at once. + +```php +use Patchlevel\EventSourcing\Message\Message; +use Patchlevel\EventSourcing\Message\Reducer; + +$state = (new Reducer()) + ->match([ + ProfileCreated::class => static function (Message $message, array $state): array { + return [...$state, $message]; + }, + ]) + ->reduce($messages); +``` +### Any + +If you want to react to any event, you can use the `any` method. + +```php +use Patchlevel\EventSourcing\Message\Message; +use Patchlevel\EventSourcing\Message\Reducer; + +$state = (new Reducer()) + ->any( + static function (Message $message, array $state): array { + return [...$state, $message]; + }, + ) + ->reduce($messages); +``` +### Finalize + +If you want to do something with the state after the reduction, you can use the `finalize` method. +This method gets the state and returns the new state. + +```php +use Patchlevel\EventSourcing\Message\Reducer; + +$state = (new Reducer()) + ->finalize( + static function (array $state): array { + return ['count' => count($state['messages'])]; + }, + ) + ->reduce($messages); // state is ['count' => 2] +``` ## Learn more * [How to decorate messages](message_decorator.md)