### Event Sourcing CLI Setup Example Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/cli.md This PHP code demonstrates how to set up a Symfony Console application for managing event-sourcing functionalities. It includes adding commands for database, schema, and subscription management. ```php use Doctrine\DBAL\Connection; use Patchlevel\EventSourcing\Console\Command; use Patchlevel\EventSourcing\Console\DoctrineHelper; use Patchlevel\EventSourcing\Schema\DoctrineSchemaDirector; use Patchlevel\EventSourcing\Store\Store; use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngine; use Symfony\Component\Console\Application; $cli = new Application('Event-Sourcing CLI'); $cli->setCatchExceptions(true); $doctrineHelper = new DoctrineHelper(); /** * @var Connection $connection * @var Store $store */ $schemaDirector = new DoctrineSchemaDirector($connection, $store); /** @var SubscriptionEngine $subscriptionEngine */ $cli->addCommands([ new Command\DatabaseCreateCommand($connection, $doctrineHelper), new Command\DatabaseDropCommand($connection, $doctrineHelper), new Command\SubscriptionBootCommand($subscriptionEngine), new Command\SubscriptionPauseCommand($subscriptionEngine), new Command\SubscriptionRunCommand($subscriptionEngine, $store), new Command\SubscriptionTeardownCommand($subscriptionEngine), new Command\SubscriptionRemoveCommand($subscriptionEngine), new Command\SubscriptionReactivateCommand($subscriptionEngine), new Command\SubscriptionSetupCommand($subscriptionEngine), new Command\SubscriptionStatusCommand($subscriptionEngine), new Command\SchemaCreateCommand($schemaDirector), new Command\SchemaDropCommand($schemaDirector), new Command\SchemaUpdateCommand($schemaDirector), ]); $cli->run(); ``` -------------------------------- ### Setup Subscriptions Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/subscription.md Set up new subscriptions using the setup method with optional criteria. The engine attempts to call the subscription's setup method if available, transitioning the subscription to booting or active. ```php use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngine; use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngineCriteria; /** @var SubscriptionEngine $subscriptionEngine */ $subscriptionEngine->setup(new SubscriptionEngineCriteria()); ``` -------------------------------- ### Projector Setup Method Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/subscription.md Use the `Setup` attribute to define a method that runs when a subscription is created. This is useful for initializing resources like database tables. ```php use Doctrine\DBAL\Connection; use Patchlevel\EventSourcing\Attribute\Projector; use Patchlevel\EventSourcing\Attribute\Setup; #[Projector(self::TABLE)] final class ProfileProjector { private const TABLE = 'profile_v1'; private Connection $connection; #[Setup] public function create(): void { $this->connection->executeStatement( sprintf('CREATE TABLE IF NOT EXISTS %s (id VARCHAR PRIMARY KEY, name VARCHAR NOT NULL);', self::TABLE) ); } } ``` -------------------------------- ### Setup and Dispatch Commands with SyncCommandBus Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/command_bus.md Instantiate SyncCommandBus with a HandlerProvider and use the dispatch method to send commands. ```php use Patchlevel\EventSourcing\CommandBus\HandlerProvider; use Patchlevel\EventSourcing\CommandBus\SyncCommandBus; /** @var HandlerProvider $handlerProvider */ $commandBus = new SyncCommandBus($handlerProvider); $commandBus->dispatch(new CreateProfile($profileId, 'name')); $commandBus->dispatch(new ChangeProfileName($profileId, 'new name')); ``` -------------------------------- ### Install Event Sourcing Library Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/index.md Use Composer to install the event sourcing library. This command adds the package to your project dependencies. ```bash composer require patchlevel/event-sourcing ``` -------------------------------- ### Pipeline Processor Example Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/UPGRADE-3.0.md This processor demonstrates how to achieve the previous Pipeline behavior using the Subscription system with Projectors and MessageTranslators. ```php use Patchlevel\EventSourcing\Attribute\Processor; use Patchlevel\EventSourcing\Attribute\Subscribe; use Patchlevel\EventSourcing\EventBus\Message; use Patchlevel\EventSourcing\Message\Translator\Translator; use Patchlevel\EventSourcing\Store\Store; #[Processor('pipeline')] class PipelineProcessor { public function __construct( private readonly Store $store, // new eventstore private readonly Translator $translator, ) { } #[Subscribe('*')] public function publish(Message $message): void { $messages = ($this->translator)($message); $this->store->save(...$messages); } } ``` -------------------------------- ### Aggregate Root Before Upgrade Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/UPGRADE-3.0.md Example of an AggregateRoot implementation before v3, extending the AggregateRoot base class. ```php use Patchlevel\EventSourcing\Aggregate\AggregateRoot; use Patchlevel\EventSourcing\Attribute\Aggregate; #[Aggregate('profile')] final class Profile extends AggregateRoot { // ... } ``` -------------------------------- ### Setup and Dispatch Queries with SyncQueryBus Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/query_bus.md Instantiate the SyncQueryBus with a HandlerProvider and use the dispatch method to send queries. The result of the query is returned. ```php use Patchlevel\EventSourcing\QueryBus\HandlerProvider; use Patchlevel\EventSourcing\QueryBus\SyncQueryBus; /** @var HandlerProvider $handlerProvider */ $queryBus = new SyncQueryBus($handlerProvider); $result = $queryBus->dispatch(new QueryProfile($profileId)); ``` -------------------------------- ### Outbox Processor Example Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/UPGRADE-3.0.md This processor replicates the outbox behavior using the Subscription system. It dispatches messages via the EventBus. ```php use Patchlevel\EventSourcing\Attribute\Processor; use Patchlevel\EventSourcing\Attribute\Subscribe; use Patchlevel\EventSourcing\EventBus\Message; #[Processor('outbox')] class OutboxProcessor { public function __construct( private readonly EventBus $eventBus, ) { } #[Subscribe('*')] public function publish(Message $message): void { $this->eventBus->dispatch($message); } } ``` -------------------------------- ### Aggregate Root After Upgrade (BasicAggregateRoot) Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/UPGRADE-3.0.md Example of an AggregateRoot implementation after v3, extending the new BasicAggregateRoot class for seamless upgrade. ```php use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Attribute\Aggregate; #[Aggregate('profile')] final class Profile extends BasicAggregateRoot { // ... } ``` -------------------------------- ### Value Object Example Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/normalizer.md Define a value object that requires custom normalization. This example shows a 'Name' value object. ```php final class Name { public function __construct(private string $value) { if (strlen($value) < 3) { throw new NameIsToShortException($value); } } public function toString(): string { return $this->value; } } ``` -------------------------------- ### Running Subscribers From the Beginning Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/subscription.md Configure a subscriber to process all events from the event stream's start using `RunMode::FromBeginning`. This is ideal for building projections from scratch. The `Projector` attribute can also be used for this purpose. ```php use Patchlevel\EventSourcing\Attribute\Subscriber; use Patchlevel\EventSourcing\Subscription\RunMode; #[Subscriber('welcome_email', RunMode::FromBeginning)] final class WelcomeEmailSubscriber { // ... ``` -------------------------------- ### CreateHotel Event with Normalizer Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/normalizer.md This event example demonstrates normalizing the 'createAt' property using DateTimeImmutableNormalizer. Properties are normalized into a payload for database storage. ```php use Patchlevel\EventSourcing\Attribute\Event; use Patchlevel\Hydrator\Normalizer\DateTimeImmutableNormalizer; #[Event('hotel.create')] final class CreateHotel { public function __construct( public readonly string $name, #[DateTimeImmutableNormalizer] public readonly DateTimeImmutable $createAt, ) { } } ``` -------------------------------- ### Aggregate Root After Upgrade (Interface and Trait) Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/UPGRADE-3.0.md Example of an AggregateRoot implementation after v3, using the AggregateRoot interface with the AggregateRootAttributeBehaviour trait. ```php use Patchlevel\EventSourcing\Aggregate\AggregateRootAttributeBehaviour; use Patchlevel\EventSourcing\Aggregate\AggregateRoot; use Patchlevel\EventSourcing\Attribute\Aggregate; #[Aggregate('profile')] final class Profile implements AggregateRoot { use AggregateRootAttributeBehaviour; // ... } ``` -------------------------------- ### Define Aggregate with Custom ID Class Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/aggregate_id.md Example of an aggregate root using a custom-implemented ID class. ```php use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Attribute\Aggregate; use Patchlevel\EventSourcing\Attribute\Id; #[Aggregate('profile')] final class Profile extends BasicAggregateRoot { #[Id] private ProfileId $id; } ``` -------------------------------- ### Create Repository with Snapshot Store Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/repository.md Integrate a snapshot store with the DefaultRepositoryManager to speed up aggregate loading by caching aggregate states. This example uses a Psr16SnapshotAdapter. ```php use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry; use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager; use Patchlevel\EventSourcing\Snapshot\Adapter\Psr16SnapshotAdapter; use Patchlevel\EventSourcing\Snapshot\DefaultSnapshotStore; use Patchlevel\EventSourcing\Store\Store; $adapter = new Psr16SnapshotAdapter($cache); $snapshotStore = new DefaultSnapshotStore(['default' => $adapter]); /** * @var AggregateRootRegistry $aggregateRootRegistry * @var Store $store */ $repositoryManager = new DefaultRepositoryManager( $aggregateRootRegistry, $store, null, $snapshotStore, ); $repository = $repositoryManager->get(Profile::class); ``` -------------------------------- ### Aggregate Root ID Before Upgrade Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/UPGRADE-3.0.md Example of an AggregateRoot with a custom ID class before v3, requiring manual ID string conversion. ```php use Patchlevel\EventSourcing\Aggregate\AggregateRoot; use Patchlevel\EventSourcing\Attribute\Aggregate; #[Aggregate('profile')] final class Profile extends AggregateRoot { private ProfileId $id; public function aggregateRootId(): string { return $this->id->toString(); } } final class ProfileId { private function __construct(private string $id) { } public static function fromString(string $id): self { return new self($id); } public function toString(): string { return $this->id; } } ``` -------------------------------- ### Create Custom Message Decorator Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/message_decorator.md Implement the MessageDecorator interface to create your own decorator. This example adds a 'SystemHeader' to messages. ```php use Patchlevel\EventSourcing\Attribute\Header; use Patchlevel\EventSourcing\Message\Message; use Patchlevel\EventSourcing\Repository\MessageDecorator\MessageDecorator; #[Header('system')] final class SystemHeader { public function __construct( public string $system, ) { } } final class OnSystemRecordedDecorator implements MessageDecorator { public function __invoke(Message $message): Message { return $message->withHeader(new SystemHeader('system')); } } ``` -------------------------------- ### Order Aggregate with Child Aggregate Example Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/aggregate.md Defines an Order aggregate that contains a Shipping child aggregate. It initializes the child aggregate and delegates the arrive method. ```php use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Aggregate\Uuid; use Patchlevel\EventSourcing\Attribute\Aggregate; use Patchlevel\EventSourcing\Attribute\Apply; use Patchlevel\EventSourcing\Attribute\ChildAggregate; use Patchlevel\EventSourcing\Attribute\Id; #[Aggregate('order')] final class Order extends BasicAggregateRoot { #[Id] private Uuid $id; #[ChildAggregate] private Shipping $shipping; public static function create(Uuid $id, string $trackingId): static { $self = new static(); $self->recordThat(new OrderCreated($id, $trackingId)); return $self; } #[Apply] public function applyOrderCreated(OrderCreated $event): void { $this->shipping = new Shipping($event->trackingId); } public function arrive(): void { $this->shipping->arrive(); } } ``` -------------------------------- ### Hotel Aggregate with Snapshot Normalizer Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/normalizer.md This aggregate example shows how DateTimeImmutableNormalizer is used for snapshotting. It defines how the aggregate's state is saved in the snapshot store. ```php use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Attribute\Aggregate; use Patchlevel\EventSourcing\Attribute\Snapshot; use Patchlevel\Hydrator\Normalizer\DateTimeImmutableNormalizer; #[Aggregate('hotel')] #[Snapshot('default')] final class Hotel extends BasicAggregateRoot { private string $name; #[DateTimeImmutableNormalizer] private DateTimeImmutable $createAt; // ... } ``` -------------------------------- ### Aggregate Root ID After Upgrade Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/UPGRADE-3.0.md Example of an AggregateRoot after v3, using the #[Id] attribute and an ID class implementing AggregateRootId for type safety. ```php use Patchlevel\EventSourcing\Aggregate\AggregateRootId; use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Attribute\Aggregate; use Patchlevel\EventSourcing\Attribute\Id; #[Aggregate('profile')] final class Profile extends BasicAggregateRoot { #[Id] private ProfileId $id; } final class ProfileId implements AggregateRootId { private function __construct(private string $id) { } public static function fromString(string $id): self { return new self($id); } public function toString(): string { return $this->id; } } ``` -------------------------------- ### Test Subscriber with SubscriberUtilities Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/testing.md Use SubscriberUtilities to simplify testing subscribers by executing setup, run, and teardown methods that invoke attribute-defined functions. ```php use Patchlevel\EventSourcing\PhpUnit\Test\SubscriberUtilities; final class ProfileSubscriberTest extends TestCase { use SubscriberUtilities; public function testProfileCreated(): void { $subscriber = new ProfileSubscriber(/* inject deps or mock tests as needed */); $util = new SubscriberUtilities($subscriber); $util->executeSetup(); $util->executeRun( new ProfileCreated( ProfileId::fromString('1'), Email::fromString('hq@patchlevel.de'), ), ); $util->executeTeardown(); self::assertSame(3, $subscriber->count); } } ``` -------------------------------- ### Shipping Micro Aggregate Example Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/aggregate.md Defines a Shipping aggregate that listens to OrderCreated events from another aggregate. It initializes itself based on the OrderCreated event. ```php use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Aggregate\Uuid; use Patchlevel\EventSourcing\Attribute\Aggregate; use Patchlevel\EventSourcing\Attribute\Apply; use Patchlevel\EventSourcing\Attribute\Id; use Patchlevel\EventSourcing\Attribute\SharedApplyContext; use Patchlevel\EventSourcing\Attribute\Stream; #[Aggregate('shipping')] #[Stream(Order::class)] #[SharedApplyContext([Order::class])] final class Shipping extends BasicAggregateRoot { #[Id] private Uuid $id; private bool $arrived = false; public function arrive(): void { $this->recordThat(new Arrived()); } #[Apply] public function applyOrderCreated(OrderCreated $event): void { $this->id = $event->id; } #[Apply] public function applyArrived(Arrived $event): void { $this->arrived = true; } public function isArrived(): bool { return $this->arrived; } } ``` -------------------------------- ### Initialize DefaultCleaner with DbalCleanupTaskHandler using ConnectionRegistry Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/subscription.md Initializes the DefaultCleaner with DbalCleanupTaskHandler using a Doctrine ConnectionRegistry. Allows specifying connection names within cleanup tasks for multi-database setups. ```php use Doctrine\Persistence\ConnectionRegistry; use Patchlevel\EventSourcing\Subscription\Cleanup\Dbal\DbalCleanupTaskHandler; use Patchlevel\EventSourcing\Subscription\Cleanup\DefaultCleaner; /** @var ConnectionRegistry $connectionRegistry */ $cleaner = new DefaultCleaner([ new DbalCleanupTaskHandler($connectionRegistry), ]); ``` -------------------------------- ### Define a Command Class Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/command_bus.md Create a command class as a simple data transfer object representing an intention to perform an action. This example shows a `CreateProfile` command with an ID and name. ```php final class CreateProfile { public function __construct( public readonly ProfileId $id, public readonly string $name, ) { } } ``` -------------------------------- ### Define Aggregate with Custom ID Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/aggregate_id.md Example of defining an aggregate root that uses a custom string ID. Requires configuration changes in the store. ```php use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Aggregate\CustomId; use Patchlevel\EventSourcing\Attribute\Aggregate; use Patchlevel\EventSourcing\Attribute\Id; #[Aggregate('profile')] final class Profile extends BasicAggregateRoot { #[Id] private CustomId $id; } ``` -------------------------------- ### Create a Service Handler Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/command_bus.md Implement a handler class to contain the business logic for a command. The `#[Handle]` attribute marks the method that will be invoked when a command is dispatched. This example shows a basic `CreateProfileHandler`. ```php use Patchlevel\EventSourcing\Attribute\Handle; final class CreateProfileHandler { #[Handle] public function __invoke(CreateProfile $command): void { // handle command } } ``` -------------------------------- ### Projector Cleanup Method with Dbal Task Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/subscription.md The `Cleanup` attribute defines a method that runs on subscription creation and stores tasks in the Subscription Store. This allows for cleanup operations without requiring the subscriber code to exist when the subscription is removed. This example uses `DropIndexTask` for Dbal. ```php use Doctrine\DBAL\Connection; use Patchlevel\EventSourcing\Attribute\Cleanup; use Patchlevel\EventSourcing\Attribute\Projector; use Patchlevel\EventSourcing\Subscription\Cleanup\Dbal\DropIndexTask; #[Projector(self::TABLE)] final class ProfileProjector { private const TABLE = 'profile_v1'; private Connection $connection; #[Cleanup] public function drop(): array { return [new DropIndexTask(self::TABLE)]; } } ``` -------------------------------- ### Normalized JSON Output Example Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/normalizer.md Example of the resulting JSON structure when using the 'NormalizedName' attribute to rename a field. ```json { "profile_name": "David" } ``` -------------------------------- ### Boot Subscriptions Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/subscription.md Boot subscriptions to catch up to the current event stream. This method transitions booting subscriptions to an active or finished state. ```php use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngine; use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngineCriteria; /** @var SubscriptionEngine $subscriptionEngine */ $subscriptionEngine->boot(new SubscriptionEngineCriteria()); ``` -------------------------------- ### Reducer Initial State Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/message.md Sets the initial state for the reducer. The reduction process starts with this state. ```php use Patchlevel\EventSourcing\Message\Reducer; $state = (new Reducer()) ->initialState(['count' => 0]) ->reduce($messages); // state is ['count' => 0] ``` -------------------------------- ### Create Database Schema Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/store.md Use the `create` method on SchemaDirector to initialize the database table from scratch. ```php use Patchlevel\EventSourcing\Schema\SchemaDirector; /** @var SchemaDirector $schemaDirector */ $schemaDirector->create(); ``` -------------------------------- ### DTO with DateTimeImmutable Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/normalizer.md This example shows a DTO with a DateTimeImmutable property. The normalizer can often be inferred from the type hint. ```php final class DTO { public DateTimeImmutable $date; } ``` -------------------------------- ### Configure Event Sourcing System Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/getting_started.md Set up the necessary components like serializer, aggregate registry, event store, and repository manager. This is required for basic event sourcing functionality. ```php use Doctrine\DBAL\DriverManager; use Doctrine\DBAL\Tools\DsnParser; use Patchlevel\EventSourcing\Metadata\AggregateRoot\AttributeAggregateRootRegistryFactory; use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager; use Patchlevel\EventSourcing\Serializer\DefaultEventSerializer; use Patchlevel\EventSourcing\Store\DoctrineDbalStore; use Patchlevel\EventSourcing\Subscription\Engine\DefaultSubscriptionEngine; use Patchlevel\EventSourcing\Subscription\Repository\RunSubscriptionEngineRepositoryManager; use Patchlevel\EventSourcing\Subscription\Store\DoctrineSubscriptionStore; use Patchlevel\EventSourcing\Subscription\Subscriber\MetadataSubscriberAccessorRepository; $connection = DriverManager::getConnection( (new DsnParser())->parse('pdo-pgsql://user:secret@localhost/app'), ); $projectionConnection = DriverManager::getConnection( (new DsnParser())->parse('pdo-pgsql://user:secret@localhost/projection'), ); /* your own mailer */ $mailer; $serializer = DefaultEventSerializer::createFromPaths(['src/Domain/Hotel/Event']); $aggregateRegistry = (new AttributeAggregateRootRegistryFactory())->create(['src/Domain/Hotel']); $eventStore = new DoctrineDbalStore( $connection, $serializer, ); $hotelProjector = new HotelProjector($projectionConnection); $subscriberRepository = new MetadataSubscriberAccessorRepository([ $hotelProjector, new SendCheckInEmailProcessor($mailer), ]); $subscriptionStore = new DoctrineSubscriptionStore($connection); $engine = new DefaultSubscriptionEngine( $eventStore, $subscriptionStore, $subscriberRepository, ); $repositoryManager = new RunSubscriptionEngineRepositoryManager( new DefaultRepositoryManager( $aggregateRegistry, $eventStore, ), $engine, ); $hotelRepository = $repositoryManager->get(Hotel::class); ``` -------------------------------- ### Set up Database Schema Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/getting_started.md Configure and create the necessary database schema for event storage and subscriptions using Doctrine DBAL. This ensures data persistence. ```php use Doctrine\DBAL\Connection; use Patchlevel\EventSourcing\Schema\ChainDoctrineSchemaConfigurator; use Patchlevel\EventSourcing\Schema\DoctrineSchemaDirector; use Patchlevel\EventSourcing\Store\Store; use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngine; use Patchlevel\EventSourcing\Subscription\Store\SubscriptionStore; /** * @var Connection $connection * @var Store $eventStore * @var SubscriptionStore $subscriptionStore */ $schemaDirector = new DoctrineSchemaDirector( $connection, new ChainDoctrineSchemaConfigurator([ $eventStore, $subscriptionStore, ]), ); $schemaDirector->create(); /** @var SubscriptionEngine $engine */ $engine->setup(skipBooting: true); ``` -------------------------------- ### Initialize InMemoryStore Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/store.md Create an InMemoryStore for testing purposes. The store can be initialized with existing events passed to the constructor. ```php use Patchlevel\EventSourcing\Store\InMemoryStore; $store = new InMemoryStore(); ``` -------------------------------- ### Create a Default Repository Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/repository.md Instantiate a DefaultRepositoryManager with an AggregateRootRegistry and a Store to create a repository for a specific aggregate. ```php use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry; use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager; use Patchlevel\EventSourcing\Store\Store; /** * @var AggregateRootRegistry $aggregateRootRegistry * @var Store $store */ $repositoryManager = new DefaultRepositoryManager( $aggregateRootRegistry, $store, ); $repository = $repositoryManager->get(Profile::class); ``` -------------------------------- ### Get Subscription Status Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/subscription.md Retrieve the current status of all subscriptions using the subscriptions method. The status of each subscription can then be accessed. ```php use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngine; use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngineCriteria; /** @var SubscriptionEngine $subscriptionEngine */ $subscriptions = $subscriptionEngine->subscriptions(new SubscriptionEngineCriteria()); foreach ($subscriptions as $subscription) { echo $subscription->status()->value; } ``` -------------------------------- ### Instantiate Event Bus with Attribute Listener Provider Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/event_bus.md Sets up an Event Bus using the default AttributeListenerProvider and DefaultConsumer. The AttributeListenerProvider finds listeners based on attributes. ```php use Patchlevel\EventSourcing\EventBus\AttributeListenerProvider; use Patchlevel\EventSourcing\EventBus\DefaultConsumer; use Patchlevel\EventSourcing\EventBus\DefaultEventBus; $listenerProvider = new AttributeListenerProvider([$mailListener]); $eventBus = new DefaultEventBus( new DefaultConsumer($listenerProvider), ); ``` -------------------------------- ### Configure DoctrineDbalStore Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/store.md Instantiate the DoctrineDbalStore with a DBAL connection and an event serializer. The serializer should be configured with paths to your event classes. ```php use Doctrine\DBAL\DriverManager; use Doctrine\DBAL\Tools\DsnParser; use Patchlevel\EventSourcing\Serializer\DefaultEventSerializer; use Patchlevel\EventSourcing\Store\DoctrineDbalStore; $connection = DriverManager::getConnection( (new DsnParser())->parse('pdo-pgsql://user:secret@localhost/app'), ); $store = new DoctrineDbalStore( $connection, DefaultEventSerializer::createFromPaths(['src/Event']), ); ``` -------------------------------- ### Order Micro Aggregate Example Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/aggregate.md Defines an Order aggregate that can be part of a micro aggregate pattern. It records an OrderCreated event. ```php use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Aggregate\Uuid; use Patchlevel\EventSourcing\Attribute\Aggregate; use Patchlevel\EventSourcing\Attribute\Apply; use Patchlevel\EventSourcing\Attribute\Id; use Patchlevel\EventSourcing\Attribute\SharedApplyContext; #[Aggregate('order')] #[SharedApplyContext([Shipping::class])] final class Order extends BasicAggregateRoot { #[Id] private Uuid $id; public static function create(Uuid $id): static { $self = new static(); $self->recordThat(new OrderCreated($id)); return $self; } #[Apply] public function applyOrderCreated(OrderCreated $event): void { $this->id = $event->id; } } ``` -------------------------------- ### DTO with Explicit DateTimeImmutableNormalizer Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/normalizer.md This example explicitly annotates the DateTimeImmutable property with DateTimeImmutableNormalizer, achieving the same result as type hint inference. ```php use Patchlevel\Hydrator\Normalizer\DateTimeImmutableNormalizer; final class DTO { #[DateTimeImmutableNormalizer] public DateTimeImmutable $date; } ``` -------------------------------- ### Implement Custom Aggregate ID Class Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/aggregate_id.md Example of creating a dedicated aggregate-specific ID class that implements the AggregateRootId interface. ```php use Patchlevel\EventSourcing\Aggregate\AggregateRootId; class ProfileId implements AggregateRootId { private function __construct( private readonly string $id, ) { } public function toString(): string { return $this->id; } public static function fromString(string $id): self { return new self($id); } } ``` -------------------------------- ### Create Repository with Event Bus Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/repository.md Configure the DefaultRepositoryManager with an event bus to dispatch events synchronously after they are saved. Be aware of potential at-least-once delivery issues. ```php use Patchlevel\EventSourcing\EventBus\DefaultEventBus; use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry; use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager; use Patchlevel\EventSourcing\Store\Store; $eventBus = DefaultEventBus::create([/* listeners */]); /** * @var AggregateRootRegistry $aggregateRootRegistry * @var Store $store */ $repositoryManager = new DefaultRepositoryManager( $aggregateRootRegistry, $store, $eventBus, ); $repository = $repositoryManager->get(Profile::class); ``` -------------------------------- ### Configure RepositoryManager with Message Decorator Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/message_decorator.md Pass a MessageDecorator instance to the DefaultRepositoryManager to apply it to all repositories. This example shows chaining SplitStreamDecorator. ```php use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry; use Patchlevel\EventSourcing\Metadata\Event\EventMetadataFactory; use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager; use Patchlevel\EventSourcing\Repository\MessageDecorator\ChainMessageDecorator; use Patchlevel\EventSourcing\Repository\MessageDecorator\SplitStreamDecorator; use Patchlevel\EventSourcing\Store\Store; /** @var EventMetadataFactory $eventMetadataFactory */ $decorator = new ChainMessageDecorator([new SplitStreamDecorator($eventMetadataFactory)]); /** * @var AggregateRootRegistry $aggregateRootRegistry * @var Store $store */ $repositoryManager = new DefaultRepositoryManager( $aggregateRootRegistry, $store, null, null, $decorator, ); $repository = $repositoryManager->get(Profile::class); ``` -------------------------------- ### Use Event Sourcing System Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/getting_started.md Demonstrates loading, modifying, and saving aggregates using the configured repository. This is the core usage pattern for event sourcing. ```php use Patchlevel\EventSourcing\Aggregate\Uuid; use Patchlevel\EventSourcing\Repository\Repository; $hotel1 = Hotel::create(Uuid::generate(), 'HOTEL'); $hotel1->checkIn('David'); $hotel1->checkIn('Daniel'); $hotel1->checkOut('David'); /** @var Repository $hotelRepository */ $hotelRepository->save($hotel1); $hotel2 = $hotelRepository->load(Uuid::fromString('d0d0d0d0-d0d0-d0d0-d0d0-d0d0d0d0d0d0')); $hotel2->checkIn('David'); $hotelRepository->save($hotel2); $hotels = $hotelProjection->getHotels(); ``` -------------------------------- ### Configure Snapshot Store Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/snapshots.md Define a snapshot store with adapters for different caches. Each cache needs a name to map aggregates to specific storage locations. ```php use Patchlevel\EventSourcing\Snapshot\Adapter\Psr16SnapshotAdapter; use Patchlevel\EventSourcing\Snapshot\DefaultSnapshotStore; $snapshotStore = new DefaultSnapshotStore([ 'default' => new Psr16SnapshotAdapter($defaultCache), 'other_cache' => new Psr16SnapshotAdapter($otherCache), ]); ``` -------------------------------- ### Configure Default Subscription Engine Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/subscription.md Instantiate the default subscription engine by providing necessary services like message loader, subscription store, subscriber accessor repository, and optionally a retry strategy, logger, and cleanup handlers. ```php use Doctrine\DBAL\Connection; use Patchlevel\EventSourcing\Subscription\Cleanup\Dbal\DbalCleanupTaskHandler; use Patchlevel\EventSourcing\Subscription\Cleanup\DefaultCleaner; use Patchlevel\EventSourcing\Subscription\Engine\DefaultSubscriptionEngine; use Patchlevel\EventSourcing\Subscription\Engine\MessageLoader; use Patchlevel\EventSourcing\Subscription\RetryStrategy\RetryStrategyRepository; use Patchlevel\EventSourcing\Subscription\Store\DoctrineSubscriptionStore; use Patchlevel\EventSourcing\Subscription\Subscriber\MetadataSubscriberAccessorRepository; /** * @var MessageLoader $messageLoader * @var DoctrineSubscriptionStore $subscriptionStore * @var MetadataSubscriberAccessorRepository $subscriberAccessorRepository * @var RetryStrategyRepository $retryStrategyRepository * @var LoggerInterface $logger * @var Connection $projectionConnection */ $subscriptionEngine = new DefaultSubscriptionEngine( $messageLoader, $subscriptionStore, $subscriberAccessorRepository, $retryStrategyRepository, // optional, if not set the default retry strategy is used $logger, // optional new DefaultCleaner([new DbalCleanupTaskHandler($projectionConnection)]), // optional but required if you want to use the cleanup feature ); ``` -------------------------------- ### Define Aggregate with UUID ID Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/aggregate_id.md Example of defining an aggregate root that uses a UUID for its ID. Ensure the Uuid class is imported. ```php use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Aggregate\Uuid; use Patchlevel\EventSourcing\Attribute\Aggregate; use Patchlevel\EventSourcing\Attribute\Id; #[Aggregate('profile')] final class Profile extends BasicAggregateRoot { #[Id] private Uuid $id; } ``` -------------------------------- ### Initialize StreamDoctrineDbalStore Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/store.md Instantiate the StreamDoctrineDbalStore with a DBAL connection and an event serializer. The store merges aggregate ID and name into a single 'stream' column and allows a nullable 'playhead' column. ```php use Doctrine\DBAL\DriverManager; use Doctrine\DBAL\Tools\DsnParser; use Patchlevel\EventSourcing\Serializer\DefaultEventSerializer; use Patchlevel\EventSourcing\Store\StreamDoctrineDbalStore; $connection = DriverManager::getConnection( (new DsnParser())->parse('pdo-pgsql://user:secret@localhost/app'), ); $store = new StreamDoctrineDbalStore( $connection, DefaultEventSerializer::createFromPaths(['src/Event']), ); ``` -------------------------------- ### Get Current Datetime with SystemClock Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/clock.md Uses the native system clock to return a DateTimeImmutable instance. Each call to now() will return a new instance. ```php use Patchlevel\EventSourcing\Clock\SystemClock; $clock = new SystemClock(); $date = $clock->now(); // get the actual datetime $date2 = $clock->now(); // $date == $date2 => false // $date === $date2 => false ``` -------------------------------- ### Setting up Doctrine Cipher Key Store Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/personal_data.md Initializes the `DoctrineCipherKeyStore` using a Doctrine DBAL connection. This service is responsible for storing encryption keys. ```php use Doctrine\DBAL\Connection; use Patchlevel\EventSourcing\Cryptography\DoctrineCipherKeyStore; /** @var Connection $dbalConnection */ $cipherKeyStore = new DoctrineCipherKeyStore($dbalConnection); ``` -------------------------------- ### Create a Simple Message Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/message.md Demonstrates the creation of a basic message without any headers. This is typically handled automatically by the repository. ```php use Patchlevel\EventSourcing\Message\Message; $message = Message::create(new NameChanged('foo')); ``` -------------------------------- ### Saving an Aggregate via Repository Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/aggregate.md Handles a command to create a new profile aggregate and saves it using a repository. Requires a Repository dependency. ```php use Patchlevel\EventSourcing\Repository\Repository; final class CreateProfileHandler { public function __construct( private readonly Repository $profileRepository, ) { } public function __invoke(CreateProfile $command): void { $profile = Profile::register($command->id()); $this->profileRepository->save($profile); } } ``` -------------------------------- ### DTO with ArrayNormalizer for DateTimeImmutable Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/normalizer.md This example demonstrates using ArrayNormalizer to apply DateTimeImmutableNormalizer to each element within an array of dates. Array keys are preserved. ```php use Patchlevel\Hydrator\Normalizer\ArrayNormalizer; use Patchlevel\Hydrator\Normalizer\DateTimeImmutableNormalizer; final class DTO { #[ArrayNormalizer(new DateTimeImmutableNormalizer())] public array $dates; } ``` -------------------------------- ### Initialize DoctrineSchemaDirector Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/store.md Instantiate the DoctrineSchemaDirector using a DBAL connection and a Store. This director is responsible for creating, updating, and deleting database schemas. ```php use Doctrine\DBAL\Connection; use Patchlevel\EventSourcing\Schema\DoctrineSchemaDirector; use Patchlevel\EventSourcing\Store\Store; /** * @var Connection $connection * @var Store $store */ $schemaDirector = new DoctrineSchemaDirector( $connection, $store, ); ``` -------------------------------- ### Implement In-Memory Snapshot Adapter Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/snapshots.md Use the `InMemorySnapshotAdapter` for testing purposes. It stores snapshots in memory, providing a simple and fast way to manage snapshots during development or testing. ```php use Patchlevel\EventSourcing\Snapshot\Adapter\InMemorySnapshotAdapter; $adapter = new InMemorySnapshotAdapter(); ``` -------------------------------- ### Generate and Use UUIDs Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/aggregate_id.md Demonstrates how to generate a new UUID or create one from a string using the Uuid class. ```php use Patchlevel\EventSourcing\Aggregate\Uuid; $uuid = Uuid::generate(); $uuid = Uuid::fromString('d6e8d7a0-4b0b-4e6a-8a9a-3a0b2d9d0e4e'); ``` -------------------------------- ### Dry Run Create Schema SQL Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/store.md Use DryRunSchemaDirector to get the SQL statements necessary for creating the database schema without executing them. ```php use Patchlevel\EventSourcing\Schema\DryRunSchemaDirector; /** @var DryRunSchemaDirector $schemaDirector */ $sql = $schemaDirector->dryRunCreate(); ``` -------------------------------- ### Initialize SplitStreamDecorator Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/message_decorator.md Instantiate the SplitStreamDecorator, which is required for the split stream feature. It needs an EventMetadataFactory. ```php use Patchlevel\EventSourcing\Metadata\Event\AttributeEventMetadataFactory; use Patchlevel\EventSourcing\Repository\MessageDecorator\SplitStreamDecorator; $eventMetadataFactory = new AttributeEventMetadataFactory(); $decorator = new SplitStreamDecorator($eventMetadataFactory); ``` -------------------------------- ### Implement PSR-16 Snapshot Adapter Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/snapshots.md Utilize the `Psr16SnapshotAdapter` for compatibility with PSR-16 simple cache interfaces. This adapter requires a `CacheInterface` instance. ```php use Patchlevel\EventSourcing\Snapshot\Adapter\Psr16SnapshotAdapter; use Psr\SimpleCache\CacheInterface; /** @var CacheInterface $cache */ $adapter = new Psr16SnapshotAdapter($cache); ``` -------------------------------- ### Create Subscription Engine Criteria Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/subscription.md Instantiate SubscriptionEngineCriteria to filter subscriptions by IDs and groups. An OR check is performed for the respective criteria, and all criteria are checked with an AND. ```php use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngineCriteria; $criteria = new SubscriptionEngineCriteria( ids: ['profile_1', 'welcome_email'], groups: ['default'], ); ``` -------------------------------- ### Count Events in Store Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/store.md Get the total number of events stored. The count method can optionally accept a Criteria object to filter the events before counting. ```php use Patchlevel\EventSourcing\Store\Store; /** @var Store $store */ $count = $store->count(); ``` ```php use Patchlevel\EventSourcing\Store\Criteria\Criteria; use Patchlevel\EventSourcing\Store\Store; /** @var Store $store */ $count = $store->count( new Criteria(), // filter criteria ); ``` -------------------------------- ### Aggregate Handler Provider Initialization Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/command_bus.md Initialize the AggregateHandlerProvider with the AggregateRootRegistry and RepositoryManager to handle commands by invoking methods on aggregates. ```php use Patchlevel\EventSourcing\CommandBus\AggregateHandlerProvider; use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry; use Patchlevel\EventSourcing\Repository\RepositoryManager; /** * @var AggregateRootRegistry $aggregateRootRegistry * @var RepositoryManager $repositoryManager */ $provider = new AggregateHandlerProvider( $aggregateRootRegistry, $repositoryManager, ); ``` -------------------------------- ### NameChanged Event with Name Value Object Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/aggregate.md This example shows how to adapt an event payload to accommodate a `Name` value object, ensuring proper serialization and deserialization. ```php use Patchlevel\EventSourcing\Attribute\Event; #[Event('profile.name_changed')] final class NameChanged { public function __construct( #[NameNormalizer] public readonly Name $name, ) { } } ``` -------------------------------- ### Create and Use Consumer Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/event_bus.md Instantiates a DefaultConsumer with listeners and uses it to consume a message. The consumer finds listeners via a ListenerProvider. ```php use Patchlevel\EventSourcing\EventBus\DefaultConsumer; $consumer = DefaultConsumer::create([$mailListener]); $consumer->consume($message); ``` -------------------------------- ### Define a Hotel Projection Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/getting_started.md This projector handles hotel-related events to maintain a table of hotels and their current guest counts. It includes setup and teardown methods for table management. ```php use Doctrine\DBAL\Connection; use Patchlevel\EventSourcing\Attribute\Projector; use Patchlevel\EventSourcing\Attribute\Setup; use Patchlevel\EventSourcing\Attribute\Subscribe; use Patchlevel\EventSourcing\Attribute\Teardown; #[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); } #[Subscribe(HotelCreated::class)] public function handleHotelCreated(HotelCreated $event): void { $this->db->insert( self::TABLE, [ 'id' => $event->hotelId->toString(), 'name' => $event->hotelName, 'guests' => 0, ], ); } #[Subscribe(GuestIsCheckedIn::class)] public function handleGuestIsCheckedIn(GuestIsCheckedIn $event): void { $this->db->executeStatement( sprintf('UPDATE %s SET guests = guests + 1 WHERE id = ?;', self::TABLE), [$event->hotelId->toString()], ); } #[Subscribe(GuestIsCheckedOut::class)] public function handleGuestIsCheckedOut(GuestIsCheckedOut $event): void { $this->db->executeStatement( sprintf('UPDATE %s SET guests = guests - 1 WHERE id = ?;', self::TABLE), [$event->hotelId->toString()], ); } #[Setup] public function create(): void { $this->db->executeStatement(sprintf('CREATE TABLE IF NOT EXISTS %s (id VARCHAR PRIMARY KEY, name VARCHAR, guests INTEGER);', self::TABLE)); } #[Teardown] public function drop(): void { $this->db->executeStatement(sprintf('DROP TABLE IF EXISTS %s;', self::TABLE)); } } ``` -------------------------------- ### Shipping Child Aggregate Example Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/aggregate.md Defines a Shipping child aggregate that is nested within a root aggregate. It records an Arrived event and requires public apply methods. ```php use Patchlevel\EventSourcing\Aggregate\BasicChildAggregate; use Patchlevel\EventSourcing\Attribute\Apply; final class Shipping extends BasicChildAggregate { private bool $arrived = false; public function __construct( private string $trackingId, ) { } public function arrive(): void { $this->recordThat(new Arrived()); } #[Apply] public function applyArrived(Arrived $event): void { $this->arrived = true; } public function isArrived(): bool { return $this->arrived; } } ``` -------------------------------- ### Aggregate Root Unit Test with Given/When/Then Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/testing.md Extend AggregateRootTestCase and implement aggregateClass to test aggregate creation and initial state. ```PHP use Patchlevel\EventSourcing\PhpUnit\Test\AggregateRootTestCase; final class ProfileTest extends AggregateRootTestCase { protected function aggregateClass(): string { return Profile::class; } public function testCreateProfile(): void { $this ->when(static fn () => Profile::createProfile(new CreateProfile(ProfileId::fromString('1'), Email::fromString('hq@patchlevel.de')))) ->then( new ProfileCreated(ProfileId::fromString('1'), Email::fromString('hq@patchlevel.de')), static function (Profile $profile): void { self::assertSame('1', $profile->id()->toString()); self::assertSame('hq@patchlevel.de', $profile->email()->toString()); self::assertSame(0, $profile->visited()); }, ); } } ``` -------------------------------- ### Item Class with ObjectNormalizer Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/normalizer.md This example shows how to apply ObjectNormalizer at the class level for child entities or value objects, enabling recursive serialization without annotating each property. ```php use Patchlevel\Hydrator\Normalizer\ObjectNormalizer; #[ObjectNormalizer] final class Item { public function __construct( public readonly int $number, public readonly DateTimeImmutable $addedAt, ) { } } ``` -------------------------------- ### Accessing Message Headers Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/message.md Illustrates how to retrieve header information from a message object. You can check for the existence of a specific header type, get a header instance, or retrieve all headers. ```php use Patchlevel\EventSourcing\Aggregate\AggregateHeader; use Patchlevel\EventSourcing\Message\Message; /** @var Message $message */ $message->header(AggregateHeader::class); // AggregateHeader object $message->hasHeader(AggregateHeader::class); // true $message->headers(); // [AggregateHeader object] ``` -------------------------------- ### Configure MetadataSubscriberAccessorRepository Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/subscription.md Creates a subscriber accessor repository using metadata. This repository provides subscribers to the subscription engine based on their metadata. ```php use Patchlevel\EventSourcing\Subscription\Subscriber\MetadataSubscriberAccessorRepository; /** * @var object $subscriber1 * @var object $subscriber2 * @var object $subscriber3 */ $subscriberAccessorRepository = new MetadataSubscriberAccessorRepository([ $subscriber1, $subscriber2, $subscriber3, ]); ``` -------------------------------- ### Create a Message with Aggregate Header Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/message.md Shows how to create a message and add an AggregateHeader, which includes details about the aggregate, its ID, playhead, and recording time. The message object is immutable, creating a new instance with the added header. ```php use Patchlevel\EventSourcing\Aggregate\AggregateHeader; use Patchlevel\EventSourcing\Clock\SystemClock; use Patchlevel\EventSourcing\Message\Message; $clock = new SystemClock(); $message = Message::create(new NameChanged('foo')) ->withHeader(new AggregateHeader( aggregateName: 'profile', aggregateId: 'bca7576c-536f-4428-b694-7b1f00c714b7', playhead: 2, recordedOn: $clock->now(), )); ``` -------------------------------- ### DTO with Property Promotion and Readonly Source: https://github.com/patchlevel/event-sourcing/blob/3.20.x/docs/pages/normalizer.md Demonstrates using DateTimeImmutableNormalizer with property promotion and readonly properties in a DTO's constructor. ```php use Patchlevel\Hydrator\Normalizer\DateTimeImmutableNormalizer; final class DTO { public function __construct( #[DateTimeImmutableNormalizer] public readonly DateTimeImmutable $date, ) { } } ```