From 524f2a50effa63fdbf3e44611088cc954b51b1db Mon Sep 17 00:00:00 2001 From: David Badura Date: Sat, 13 Jun 2026 09:03:35 +0200 Subject: [PATCH 1/2] docs: update upgrade guide and message docs for 4.0 breaking changes Document the breaking changes from the merged 4.0 PRs that were missing from the upgrade guide: - stream refactor (#847): Store\Stream interface replaced by the concrete Message\Stream class, removed store-specific stream implementations, Message\Pipe removed in favor of Stream::transform() - removed deprecated SubscriberHelper and SubscriberUtil (#805) - removed SubscriberAccessor / RealSubscriberAccessor interfaces and deprecated accessor methods, removed AggregateIdArgumentResolver (#756) - ArgumentMetadata now carries a symfony/type-info Type (#814) Also update the message docs Pipe section to use the new Stream API. --- docs/UPGRADE-4.0.md | 84 +++++++++++++++++++++++++++++++++++++++++++++ docs/message.md | 18 ++++++---- 2 files changed, 96 insertions(+), 6 deletions(-) diff --git a/docs/UPGRADE-4.0.md b/docs/UPGRADE-4.0.md index 9efd02bf8..413a12b42 100644 --- a/docs/UPGRADE-4.0.md +++ b/docs/UPGRADE-4.0.md @@ -140,6 +140,47 @@ Further changes: * `ProcessedResult` now extends `Result`, so the `execute` method always returns a `Result`. The `Boot` and `Run` commands return a `ProcessedResult`. * The `DefaultSubscriptionEngine` accepts an optional `EventDispatcherInterface` as last constructor argument to hook into the engine with own listeners. +### SubscriberHelper and SubscriberUtil + +The deprecated `Patchlevel\EventSourcing\Subscription\Subscriber\SubscriberHelper` +and the `Patchlevel\EventSourcing\Subscription\Subscriber\SubscriberUtil` trait have been removed. + +If you need the subscriber id, read it from the metadata instead: + +```php +use Patchlevel\EventSourcing\Metadata\Subscriber\AttributeSubscriberMetadataFactory; + +$metadata = (new AttributeSubscriberMetadataFactory())->metadata($subscriber::class); +$subscriberId = $metadata->id; +``` + +### SubscriberAccessor and RealSubscriberAccessor + +The `Patchlevel\EventSourcing\Subscription\Subscriber\SubscriberAccessor` and +`Patchlevel\EventSourcing\Subscription\Subscriber\RealSubscriberAccessor` interfaces have been removed. +Use `Patchlevel\EventSourcing\Subscription\Subscriber\MetadataSubscriberAccessor` directly. + +Accordingly, `SubscriberAccessorRepository::get()` now returns a `MetadataSubscriberAccessor|null` +instead of a `SubscriberAccessor|null`. + +The deprecated methods `id()`, `group()` and `runMode()` on `MetadataSubscriberAccessor` have been removed. +Use `->metadata()->id`, `->metadata()->group` and `->metadata()->runMode` instead. + +### AggregateIdArgumentResolver + +The deprecated `Patchlevel\EventSourcing\Subscription\Subscriber\ArgumentResolver\AggregateIdArgumentResolver` +has been removed. Automatically resolving the aggregate id in a stream store is not possible. +Add the aggregate id to your events instead. + +### ArgumentMetadata + +The subscriber argument resolver now uses `symfony/type-info` to describe argument types. + +`Patchlevel\EventSourcing\Metadata\Subscriber\ArgumentMetadata` no longer carries a `string $type` +and a `bool $allowsNull` property. Instead it now has a single `Symfony\Component\TypeInfo\Type $type` property. + +If you implemented a custom `ArgumentResolver`, adjust it to read the type from the new `Type` object. + ## Store ### StreamStore @@ -160,6 +201,49 @@ And all the associated classes: `StreamReadOnlyStore` was been merged in `ReadOnlyStore`. +## Stream + +The stream handling has been reworked. Previously the `Stream` was an interface that every store had to +implement on its own (`ArrayStream`, `StreamDoctrineDbalStoreStream`, `TaggableDoctrineDbalStoreStream`, +`GeneratorStream`, ...). Now there is a single generic implementation that you can reuse. + +### Stream interface + +The `Patchlevel\EventSourcing\Store\Stream` interface has been removed and replaced by the concrete final +class `Patchlevel\EventSourcing\Message\Stream`. + +All stores now return a `Patchlevel\EventSourcing\Message\Stream` from their `load()` method. +The following store specific stream implementations have been removed: + +* `Patchlevel\EventSourcing\Store\ArrayStream` +* `Patchlevel\EventSourcing\Store\StreamDoctrineDbalStoreStream` +* `Patchlevel\EventSourcing\Store\TaggableDoctrineDbalStoreStream` +* `Patchlevel\EventSourcing\Subscription\Engine\GeneratorStream` + +The new `Stream` class implements `Iterator` and accepts any `iterable` in its constructor. +The `index()`, `position()`, `end()` and `close()` methods remain available. +In addition there are now the helper methods `toList()`, `toArray()`, `transform()` and `chunk()`. + +### Pipe + +`Patchlevel\EventSourcing\Message\Pipe` has been removed. Use `Stream::transform()` instead. + +before: + +```php +use Patchlevel\EventSourcing\Message\Pipe; + +$messages = (new Pipe($messages, $translator))->toArray(); +``` + +after: + +```php +use Patchlevel\EventSourcing\Message\Stream; + +$messages = (new Stream($messages))->transform($translator)->toList(); +``` + ## Message ### AggregateHeader diff --git a/docs/message.md b/docs/message.md index a50248c07..c3acce9e9 100644 --- a/docs/message.md +++ b/docs/message.md @@ -97,27 +97,33 @@ use Patchlevel\EventSourcing\Message\Message; /** @var Message $message */ $message->header(ApplicationHeader::class); ``` -## Pipe +## Stream -The `Pipe` is a construct that allows you to chain multiple translators. +A `Stream` wraps an iterable of messages and allows you to chain multiple translators with `transform`. 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\Stream; use Patchlevel\EventSourcing\Message\Translator\ExcludeEventTranslator; use Patchlevel\EventSourcing\Message\Translator\RecalculatePlayheadTranslator; -$messages = new Pipe( - $messages, +$stream = (new Stream($messages))->transform( new ExcludeEventTranslator([ProfileCreated::class]), new RecalculatePlayheadTranslator(), ); -foreach ($messages as $message) { +foreach ($stream as $message) { // do something with the message } ``` + +:::tip +A `Stream` is also what every store returns from its `load` method, so you can apply the same +transformations to the messages you read from the store. Besides iterating, a `Stream` offers +`toList()`, `toArray()` and `chunk()` to consume the messages. +::: + ## Translator Translator can be used to manipulate, filter or expand messages or events. From 05b81e473654eb74e723212e99a14f437fa4fae3 Mon Sep 17 00:00:00 2001 From: David Badura Date: Sun, 14 Jun 2026 14:56:05 +0200 Subject: [PATCH 2/2] docs: add projector const example to SubscriberHelper upgrade notes Show the recommended const TABLE pattern as replacement for SubscriberHelper/SubscriberUtil and fix the sprintf argument placement in the getting-started projector example. --- docs/UPGRADE-4.0.md | 30 +++++++++++++++++++++++++++++- docs/getting-started.md | 2 +- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/docs/UPGRADE-4.0.md b/docs/UPGRADE-4.0.md index 413a12b42..ccb88ebc8 100644 --- a/docs/UPGRADE-4.0.md +++ b/docs/UPGRADE-4.0.md @@ -145,7 +145,35 @@ Further changes: The deprecated `Patchlevel\EventSourcing\Subscription\Subscriber\SubscriberHelper` and the `Patchlevel\EventSourcing\Subscription\Subscriber\SubscriberUtil` trait have been removed. -If you need the subscriber id, read it from the metadata instead: +If you used them inside a projector to keep the projector id and the table name in sync, +use a constant instead: + +```php +use Doctrine\DBAL\Connection; +use Patchlevel\EventSourcing\Attribute\Projector; + +#[Projector(self::TABLE)] +final class HotelProjector +{ + // use a const for easier access in the projector & to keep projector id and table name in sync + private const TABLE = 'hotel'; + + public function __construct( + private readonly Connection $db, + ) { + } + + /** @return list */ + public function getHotels(): array + { + return $this->db->fetchAllAssociative(sprintf('SELECT id, name, guests FROM %s;', self::TABLE)); + } + + // ... +} +``` + +If you still need the subscriber id elsewhere, read it from the metadata instead: ```php use Patchlevel\EventSourcing\Metadata\Subscriber\AttributeSubscriberMetadataFactory; diff --git a/docs/getting-started.md b/docs/getting-started.md index baa3819c4..829c6870c 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -178,7 +178,7 @@ final class HotelProjector /** @return list */ public function getHotels(): array { - return $this->db->fetchAllAssociative(sprintf('SELECT id, name, guests FROM %s;'), self::TABLE); + return $this->db->fetchAllAssociative(sprintf('SELECT id, name, guests FROM %s;', self::TABLE)); } #[Subscribe(HotelCreated::class)]