diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index a33d3cb7..4b39c500 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -83,17 +83,10 @@ nav: - Introduction: index.md - Installation: installation.md - Getting Started: getting_started.md - - Basics: - - Aggregate: aggregate.md - - Events: events.md - - Repository: repository.md - - Store: store.md - - Subscription: subscription.md - - Event Bus: event_bus.md - - Advanced: - - Normalizer: normalizer.md - - Snapshots: snapshots.md - - Upcasting: upcasting.md - - Message Decorator: message_decorator.md - - Time / Clock: clock.md - - CLI: cli.md + - Configuration: configuration.md + - Usage: usage.md + - Links: + - Library Documentation: https://patchlevel.github.io/event-sourcing-docs/latest/ + - Admin Bundle: https://github.com/patchlevel/event-sourcing-admin-bundle + - Psalm Plugin: https://github.com/patchlevel/event-sourcing-psalm-plugin + - Hydrator: https://github.com/patchlevel/hydrator diff --git a/docs/pages/aggregate.md b/docs/pages/aggregate.md deleted file mode 100644 index a66153ad..00000000 --- a/docs/pages/aggregate.md +++ /dev/null @@ -1,44 +0,0 @@ -# Aggregate - -!!! info - - You can find out more about aggregates in the library - [documentation](https://patchlevel.github.io/event-sourcing-docs/latest/aggregate/). - This documentation is limited to bundle integration. - -## Register aggregates - -A path must be specified for Event Sourcing to know where to look for your aggregates. - -```yaml -patchlevel_event_sourcing: - aggregates: '%kernel.project_dir%/src' -``` -!!! tip - - You can also define multiple paths by specifying an array. - -## Define aggregates - -Next, you need to create a class to serve as an aggregate. -In our example it is a hotel. -This class must be marked with the `#[Aggregate]` attribute. -And has to implement the `Patchlevel\EventSourcing\Aggregate\AggregateRoot` interface. -To do this, you can extend the `Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot` class. - -```php -namespace App\Domain\Hotel; - -use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; -use Patchlevel\EventSourcing\Attribute\Aggregate; - -#[Aggregate(name: 'hotel')] -final class Hotel extends BasicAggregateRoot -{ - // ... -} -``` -!!! note - - You should read [here](https://patchlevel.github.io/event-sourcing-docs/latest/aggregates/) how the aggregates then work internally. - \ No newline at end of file diff --git a/docs/pages/cli.md b/docs/pages/cli.md deleted file mode 100644 index 7f9df64b..00000000 --- a/docs/pages/cli.md +++ /dev/null @@ -1,37 +0,0 @@ -# CLI - -The bundle also offers `cli commands` to create or delete `databases`. -It is also possible to manage the `schema` and `projections`. - -## Database commands - -There are two commands for creating and deleting a database. - -* `event-sourcing:database:create` -* `event-sourcing:database:drop` - -## Schema commands - -The database schema can also be created, updated and dropped. - -* `event-sourcing:schema:create` -* `event-sourcing:schema:update` -* `event-sourcing:schema:drop` - -!!! note - - You can also register doctrine migration commands, - see the [store](./store.md#Migration) documentation for this. - -## Migration commands - -After the migration lib has been installed, the migration commands are automatically configured: - -* ExecuteCommand: `event-sourcing:migrations:execute` -* GenerateCommand: `event-sourcing:migrations:generate` -* LatestCommand: `event-sourcing:migrations:latest` -* ListCommand: `event-sourcing:migrations:list` -* MigrateCommand: `event-sourcing:migrations:migrate` -* DiffCommand: `event-sourcing:migrations:diff` -* StatusCommand: `event-sourcing:migrations:status` -* VersionCommand: `event-sourcing:migrations:version` diff --git a/docs/pages/clock.md b/docs/pages/clock.md deleted file mode 100644 index 7e9ae40d..00000000 --- a/docs/pages/clock.md +++ /dev/null @@ -1,49 +0,0 @@ -# Clock - -The clock is used to return the current time as DateTimeImmutable. -There is a system clock and a frozen clock for test purposes. - -!!! info - - You can find out more about clock in the library - [documentation](https://patchlevel.github.io/event-sourcing-docs/latest/clock/). - This documentation is limited to bundle integration. - -## Testing - -You can freeze the clock for testing purposes: - -```yaml -when@test: - patchlevel_event_sourcing: - clock: - freeze: '2020-01-01 22:00:00' -``` -!!! note - - If freeze is not set, then the system clock is used. - -## PSR-20 - -You can also use your own implementation of your choice. -They only have to implement the interface of the [psr-20](https://www.php-fig.org/psr/psr-20/). -You can then specify this service here: - -```yaml -patchlevel_event_sourcing: - clock: - service: 'my_own_clock_service' -``` -## Symfony Clock - -Since symfony 6.2 there is a [clock](https://symfony.com/doc/current/components/clock.html) implementation -based on psr-20 that you can use. - -```bash -composer require symfony/clock -``` -```yaml -patchlevel_event_sourcing: - clock: - service: 'clock' -``` \ No newline at end of file diff --git a/docs/pages/configuration.md b/docs/pages/configuration.md new file mode 100644 index 00000000..2ca0cbcc --- /dev/null +++ b/docs/pages/configuration.md @@ -0,0 +1,403 @@ +# Configuration + +!!! info + + You can find out more about event sourcing in the library + [documentation](https://patchlevel.github.io/event-sourcing-docs/latest/). + This documentation is limited to bundle integration and configuration. + +## Aggregate + +A path must be specified for Event Sourcing to know where to look for your aggregates. +If you want you can use glob patterns to specify multiple paths. + +```yaml +patchlevel_event_sourcing: + aggregates: '%kernel.project_dir%/src/*/Domain' +``` + +Or use an array to specify multiple paths. + +```yaml +patchlevel_event_sourcing: + aggregates: + - '%kernel.project_dir%/src/Hotel/Domain' + - '%kernel.project_dir%/src/Room/Domain' +``` + +!!! note + + The library will automatically register all classes marked with the `#[Aggregate]` attribute in the specified paths. + +!!! tip + + If you want to learn more about aggregates, read the [library documentation](https://patchlevel.github.io/event-sourcing-docs/latest/aggregate/). + +## Events + +A path must be specified for Event Sourcing to know where to look for your events. +If you want you can use glob patterns to specify multiple paths. + +```yaml +patchlevel_event_sourcing: + events: '%kernel.project_dir%/src/*/Domain/Event' +``` + +Or use an array to specify multiple paths. + +```yaml +patchlevel_event_sourcing: + events: + - '%kernel.project_dir%/src/Hotel/Domain/Event' + - '%kernel.project_dir%/src/Room/Domain/Event' +``` + +!!! tip + + If you want to learn more about events, read the [library documentation](https://patchlevel.github.io/event-sourcing-docs/latest/events/). + +## Connection + +You have to specify the connection url to the event store. + +```yaml +patchlevel_event_sourcing: + connection: + url: '%env(EVENTSTORE_URL)%' +``` + +!!! note + + You can find out more about how to create a connection + [here](https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html) + +## Doctrine Bundle Connection + +If you have installed the [doctrine bundle](https://github.com/doctrine/DoctrineBundle), +you can also define the connection via doctrine and then use it in the store. + +```yaml +doctrine: + dbal: + connections: + eventstore: + url: '%env(EVENTSTORE_URL)%' + +patchlevel_event_sourcing: + connection: + service: doctrine.dbal.eventstore_connection +``` +!!! warning + + If you want to use the same connection as doctrine orm, then you have to set the flag `merge_orm_schema`. + Otherwise you should avoid using the same connection as other tools. + +!!! note + + You can find out more about the dbal configuration + [here](https://symfony.com/bundles/DoctrineBundle/current/configuration.html). + +## Store + +### Change table Name + +You can change the table name of the event store. + +```yaml +patchlevel_event_sourcing: + store: + table_name: 'my_event_store' +``` + +### Merge ORM Schema + +You can also merge the schema with doctrine orm. You have to set the following flag for this: + +```yaml +patchlevel_event_sourcing: + store: + merge_orm_schema: true +``` +!!! note + + All schema relevant commands are removed if you activate this option. You should use the doctrine commands then. + +!!! warning + + If you want to merge the schema, then the same doctrine connection must be used as with the doctrine orm. + Otherwise errors may occur! + +## Migration + +You can use [doctrine migrations](https://www.doctrine-project.org/projects/migrations.html) to manage the schema. + +```bash + +```yaml +patchlevel_event_sourcing: + migration: + namespace: EventSourcingMigrations + path: "%kernel.project_dir%/migrations" +``` + +## Subscription + +### Catch Up + +If aggregates are used in the processors and new events are generated there, +then they are not part of the current subscription engine `run` and will only be processed during the next run or boot. +This is usually not a problem in dev or prod environment because a worker is used +and these events will be processed at some point. But in testing it is not so easy. +For this reason, you can activate the `catch_up` option. + +```yaml +when@test: + patchlevel_event_sourcing: + subscription: + catch_up: true +``` + +### Throw on Error + +You can activate the `throw_on_error` option to throw an exception if a subscription engine run has an error. +This is useful for testing or development to get directly feedback if something is wrong. + +```yaml +when@test: + patchlevel_event_sourcing: + subscription: + throw_on_error: true +``` + +!!! warning + + This option should not be used in production. The normal behavior is to log the error and continue. + +### Run After Aggregate Save + +If you want to run the subscription engine after an aggregate is saved, you can activate this option. +This is useful for testing or development, so you don't have run a worker to process the events. + +```yaml +when@test: + patchlevel_event_sourcing: + subscription: + run_after_aggregate_save: true +``` + +### Rebuild After File Change + +If you want to rebuild the subscription engine after a file change, you can activate this option. +This is also useful for development, so you don't have to rebuild the projections manually. + +```yaml +when@test: + patchlevel_event_sourcing: + subscription: + rebuild_after_file_change: true +``` + +## Event Bus + +We supply our own event bus, but also have the option of replacing the event bus with an implementation. +We offer various options for this. + +!!! note + + We recommend using the default event bus for better integration. + +### Patchlevel (Default) Event Bus + +First of all we have our own default event bus. +This works best with the library, as the `#[Subscribe]` attribute is used there, among other things. + +```yaml +patchlevel_event_sourcing: + event_bus: + type: default +``` +!!! note + + You don't have to specify this as it is the default value. + +### Symfony Event Bus + +But you can also use [Symfony Messenger](https://symfony.com/doc/current/messenger.html). +To do this, you first have to define a suitable message bus. +This must be "allow_no_handlers" so that this messenger can be an event bus according to the definition. + +```yaml +# messenger.yaml +framework: + messenger: + buses: + event.bus: + default_middleware: allow_no_handlers +``` +We can then use this messenger or event bus in event sourcing: + +```yaml +patchlevel_event_sourcing: + event_bus: + service: event.bus +``` +Since the event bus was replaced, event sourcing own attributes no longer work. +You use the Symfony attributes instead. + +```php +use Patchlevel\EventSourcing\EventBus\Message; +use Symfony\Component\Messenger\Attribute\AsMessageHandler; + +#[AsMessageHandler('event.bus')] +class SmsNotificationHandler +{ + public function __invoke(Message $message): void + { + if (!$message instanceof GuestIsCheckedIn) { + return; + } + + // ... do some work - like sending an SMS message! + } +} +``` +#### Command Bus + +If you use a command bus or cqrs as a pattern, then you should define a new message bus. +The whole thing can look like this: + +```yaml +framework: + messenger: + default_bus: command.bus + buses: + command.bus: ~ + event.bus: + default_middleware: allow_no_handlers +``` +!!! warning + + You should deactivate the autoconfigure feature for the handlers, + otherwise they will be registered in both messenger. + +### PSR-14 Event Bus + +You can also use any other event bus that implements the [PSR-14](https://www.php-fig.org/psr/psr-14/) standard. + +```yaml +patchlevel_event_sourcing: + event_bus: + type: psr14 + service: my.event.bus.service +``` +!!! note + + Like the Symfony event bus, the event sourcing attributes no longer work here. + You have to use the system that comes with the respective psr14 implementation. + +### Custom Event Bus + +You can also use your own event bus that implements the `Patchlevel\EventSourcing\EventBus\EventBus` interface. + +```yaml +patchlevel_event_sourcing: + event_bus: + type: custom + service: my.event.bus.service +``` +!!! note + + Like the Symfony event bus, the event sourcing attributes no longer work here. + You have to use the system that comes with the respective custom implementation. + +## Snapshot + +You can use symfony cache to define the target of the snapshot store. + +```yaml +framework: + cache: + default_redis_provider: 'redis://localhost' + pools: + event_sourcing.cache: + adapter: cache.adapter.redis +``` +After this, you need define the snapshot store. +Symfony cache implement the psr6 interface, so we need choose this type +and enter the id from the cache service. + +```yaml +patchlevel_event_sourcing: + snapshot_stores: + default: + service: event_sourcing.cache +``` +Finally, you have to tell the aggregate that it should use this snapshot store. + +```php +namespace App\Domain\Profile; + +use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; +use Patchlevel\EventSourcing\Attribute\Aggregate; +use Patchlevel\EventSourcing\Attribute\Snapshot; + +#[Aggregate(name: 'profile')] +#[Snapshot('default')] +final class Profile extends BasicAggregateRoot +{ + // ... +} +``` +!!! book + + You can find out more about the attributes [here](aggregate.md). + +# Clock + +The clock is used to return the current time as DateTimeImmutable. +There is a system clock and a frozen clock for test purposes. + +!!! info + + You can find out more about clock in the library + [documentation](https://patchlevel.github.io/event-sourcing-docs/latest/clock/). + This documentation is limited to bundle integration. + +## Testing + +You can freeze the clock for testing purposes: + +```yaml +when@test: + patchlevel_event_sourcing: + clock: + freeze: '2020-01-01 22:00:00' +``` +!!! note + + If freeze is not set, then the system clock is used. + +## PSR-20 + +You can also use your own implementation of your choice. +They only have to implement the interface of the [psr-20](https://www.php-fig.org/psr/psr-20/). +You can then specify this service here: + +```yaml +patchlevel_event_sourcing: + clock: + service: 'my_own_clock_service' +``` +## Symfony Clock + +Since symfony 6.2 there is a [clock](https://symfony.com/doc/current/components/clock.html) implementation +based on psr-20 that you can use. + +```bash +composer require symfony/clock +``` +```yaml +patchlevel_event_sourcing: + clock: + service: 'clock' +``` \ No newline at end of file diff --git a/docs/pages/event_bus.md b/docs/pages/event_bus.md deleted file mode 100644 index dfd24dfd..00000000 --- a/docs/pages/event_bus.md +++ /dev/null @@ -1,126 +0,0 @@ -# Event Bus - -This library uses the core principle called [event bus](https://martinfowler.com/articles/201701-event-driven.html). - -For all events that are persisted (when the `save` method has been executed on the [repository](./repository.md)), -the event will be dispatched to the `event bus`. All listeners are then called for each event. - -!!! info - - You can find out more about the event bus in the library - [documentation](https://patchlevel.github.io/event-sourcing-docs/latest/event_bus/). - This documentation is limited to bundle integration. - -## Event Bus Types - -We supply our own event bus, but also have the option of replacing the event bus with an implementation. -We offer various options for this. - -!!! note - - We recommend using the default event bus for better integration. - -### Patchlevel (Default) Event Bus - -First of all we have our own default event bus. -This works best with the library, as the `#[Subscribe]` attribute is used there, among other things. - -```yaml -patchlevel_event_sourcing: - event_bus: - type: default -``` -!!! note - - You don't have to specify this as it is the default value. - -### Symfony Event Bus - -But you can also use [Symfony Messenger](https://symfony.com/doc/current/messenger.html). -To do this, you first have to define a suitable message bus. -This must be "allow_no_handlers" so that this messenger can be an event bus according to the definition. - -```yaml -# messenger.yaml -framework: - messenger: - buses: - event.bus: - default_middleware: allow_no_handlers -``` -We can then use this messenger or event bus in event sourcing: - -```yaml -patchlevel_event_sourcing: - event_bus: - service: event.bus -``` -Since the event bus was replaced, event sourcing's own attributes no longer work. -You use the Symfony attributes instead. - -```php -use Patchlevel\EventSourcing\EventBus\Message; -use Symfony\Component\Messenger\Attribute\AsMessageHandler; - -#[AsMessageHandler('event.bus')] -class SmsNotificationHandler -{ - public function __invoke(Message $message): void - { - if (!$message instanceof GuestIsCheckedIn) { - return; - } - - // ... do some work - like sending an SMS message! - } -} -``` -#### Command Bus - -If you use a command bus or cqrs as a pattern, then you should define a new message bus. -The whole thing can look like this: - -```yaml -framework: - messenger: - default_bus: command.bus - buses: - command.bus: ~ - event.bus: - default_middleware: allow_no_handlers -``` -!!! warning - - You should deactivate the autoconfigure feature for the handlers, - otherwise they will be registered in both messenger. - -### PSR-14 Event Bus - -You can also use any other event bus that implements the [PSR-14](https://www.php-fig.org/psr/psr-14/) standard. - -```yaml -patchlevel_event_sourcing: - event_bus: - type: psr14 - service: my.event.bus.service -``` -!!! note - - Like the Symfony event bus, the event sourcing attributes no longer work here. - You have to use the system that comes with the respective psr14 implementation. - -### Custom Event Bus - -You can also use your own event bus that implements the `Patchlevel\EventSourcing\EventBus\EventBus` interface. - -```yaml -patchlevel_event_sourcing: - event_bus: - type: custom - service: my.event.bus.service -``` -!!! note - - Like the Symfony event bus, the event sourcing attributes no longer work here. - You have to use the system that comes with the respective custom implementation. - \ No newline at end of file diff --git a/docs/pages/events.md b/docs/pages/events.md deleted file mode 100644 index d8c42cb3..00000000 --- a/docs/pages/events.md +++ /dev/null @@ -1,56 +0,0 @@ -# Events - -Events are used to describe things that happened in the application. -Since the events already happened, they are also immutable. -In event sourcing, these are used to save and rebuild the current state. -You can also listen on events to react and perform different actions. - -!!! info - - You can find out more about events in the library - [documentation](https://patchlevel.github.io/event-sourcing-docs/latest/events/). - This documentation is limited to bundle integration. - -## Register events - -A path must be specified for Event Sourcing to know where to look for your evets. - -```yaml -patchlevel_event_sourcing: - events: '%kernel.project_dir%/src' -``` -!!! tip - - You can also define multiple paths by specifying an array. - -## Define events - -Next, you need to create a class to serve as an event. -This class must get the `Event` attribute with a unique event name. - -```php -namespace App\Domain\Hotel\Event; - -use Patchlevel\EventSourcing\Aggregate\Uuid; -use Patchlevel\EventSourcing\Attribute\Event; -use Patchlevel\EventSourcing\Serializer\Normalizer\IdNormalizer; - -#[Event('hotel.created')] -final class HotelCreated -{ - public function __construct( - #[IdNormalizer] - public readonly Uuid $id, - public readonly string $hotelName, - ) { - } -} -``` -!!! note - - If you want to learn more about events, read the [library documentation](https://patchlevel.github.io/event-sourcing-docs/latest/events/). - -!!! note - - You can read more about normalizer [here](normalizer.md). - \ No newline at end of file diff --git a/docs/pages/getting_started.md b/docs/pages/getting_started.md index f8362cc3..01303184 100644 --- a/docs/pages/getting_started.md +++ b/docs/pages/getting_started.md @@ -1,7 +1,7 @@ # Getting Started -In our little getting started example, we manage hotels. We keep the example small, so we can only create hotels and let -guests check in and check out. +In our little getting started example, we manage hotels. +We keep the example small, so we can only create hotels and let guests check in and check out. For this example we use following package: @@ -16,10 +16,10 @@ If you haven't already done so, see the [installation introduction](installation First we define the events that happen in our system. -A hotel can be created with a `name`: +A hotel can be created with a `name` and an `id`: ```php -namespace App\Domain\Hotel\Event; +namespace App\Hotel\Domain\Event; use Patchlevel\EventSourcing\Aggregate\Uuid; use Patchlevel\EventSourcing\Attribute\Event; @@ -36,10 +36,10 @@ final class HotelCreated } } ``` -A guest can check in by name: +A guest can check in by `name`: ```php -namespace App\Domain\Hotel\Event; +namespace App\Hotel\Domain\Event; use Patchlevel\EventSourcing\Attribute\Event; @@ -55,7 +55,7 @@ final class GuestIsCheckedIn And also check out again: ```php -namespace App\Domain\Hotel\Event; +namespace App\Hotel\Domain\Event; use Patchlevel\EventSourcing\Attribute\Event; @@ -70,20 +70,22 @@ final class GuestIsCheckedOut ``` !!! note - You can find out more about events [here](events.md). + You can find out more about events in the [library](https://patchlevel.github.io/event-sourcing-docs/latest/events/). ## Define aggregates -Next we need to define the aggregate. So the hotel and how the hotel should behave. -We have also defined the `create`, `checkIn` and `checkOut` methods accordingly. -These events are thrown here and the state of the hotel is also changed. +Next we need to define the hotel aggregate. +How you can interact with it, which events happen and what the business rules are. +For this we create the methods `create`, `checkIn` and `checkOut`. +In these methods the business checks are made and the events are recorded. +Last but not least, we need the associated apply methods to change the state. ```php -namespace App\Domain\Hotel; +namespace App\Hotel\Domain; -use App\Domain\Hotel\Event\GuestIsCheckedIn; -use App\Domain\Hotel\Event\GuestIsCheckedOut; -use App\Domain\Hotel\Event\HotelCreated; +use App\Hotel\Domain\Event\GuestIsCheckedIn; +use App\Hotel\Domain\Event\GuestIsCheckedOut; +use App\Hotel\Domain\Event\HotelCreated; use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Aggregate\Uuid; use Patchlevel\EventSourcing\Attribute\Aggregate; @@ -168,20 +170,20 @@ final class Hotel extends BasicAggregateRoot ``` !!! note - You can find out more about aggregates [here](aggregate.md). + You can find out more about aggregates in the [library](https://patchlevel.github.io/event-sourcing-docs/latest/aggregate/). ## Define projections -So that we can see all the hotels on our website -and also see how many guests are currently visiting the hotels, -we need a projection for it. +So that we can see all the hotels on our website and also see how many guests are currently visiting the hotels, +we need a projection for it. To create a projection we need a projector. +Each projector is then responsible for a specific projection. ```php -namespace App\Projection; +namespace App\Hotel\Infrastructure\Projection; -use App\Domain\Hotel\Event\GuestIsCheckedIn; -use App\Domain\Hotel\Event\GuestIsCheckedOut; -use App\Domain\Hotel\Event\HotelCreated; +use App\Hotel\Domain\Event\GuestIsCheckedIn; +use App\Hotel\Domain\Event\GuestIsCheckedOut; +use App\Hotel\Domain\Event\HotelCreated; use Doctrine\DBAL\Connection; use Patchlevel\EventSourcing\Attribute\Projector; use Patchlevel\EventSourcing\Attribute\Setup; @@ -259,23 +261,23 @@ final class HotelProjection !!! note - You can find out more about projections [here](subscription.md). + You can find out more about projections in the [library](https://patchlevel.github.io/event-sourcing-docs/latest/subscription/). ## Processor In our example we also want to send an email to the head office as soon as a guest is checked in. ```php -namespace App\Domain\Hotel\Listener; +namespace App\Hotel\Application\Processor; -use App\Domain\Hotel\Event\GuestIsCheckedIn; +use App\Hotel\Domain\Event\GuestIsCheckedIn; use Patchlevel\EventSourcing\Attribute\Processor; use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Mime\Email; use function sprintf; -#[Processor('send_check_in_email')] +#[Processor('admin_emails')] final class SendCheckInEmailListener { private function __construct( @@ -302,7 +304,7 @@ final class SendCheckInEmailListener !!! note - You can find out more about processor [here](subscription.md). + You can find out more about processor in the [library](https://patchlevel.github.io/event-sourcing-docs/latest/subscription/) ## Database setup @@ -311,21 +313,21 @@ So that we can actually write the data to a database, we need the associated sch ```bash bin/console event-sourcing:database:create bin/console event-sourcing:schema:create -bin/console event-sourcing:subscription:create +bin/console event-sourcing:subscription:setup ``` !!! note - You can find out more about the database [here](store.md). + You can find out more about the cli in the [library](https://patchlevel.github.io/event-sourcing-docs/latest/cli/). ### Usage We are now ready to use the Event Sourcing System. We can load, change and save aggregates. ```php -namespace App\Controller; +namespace App\Hotel\Infrastructure\Controller; -use App\Domain\Hotel\Hotel; -use App\Projection\HotelProjection; +use App\Hotel\Domain\Hotel; +use App\Hotel\Infrastructure\Projection\HotelProjection; use Patchlevel\EventSourcing\Aggregate\Uuid; use Patchlevel\EventSourcing\Repository\Repository; use Patchlevel\EventSourcing\Repository\RepositoryManager; @@ -391,4 +393,17 @@ final class HotelController return new JsonResponse(); } } -``` \ No newline at end of file +``` +## Result + +!!! success + + We have successfully implemented and used event sourcing. + + Feel free to browse further in the documentation for more detailed information. + If there are still open questions, create a ticket on Github and we will try to help you. + +!!! note + + This documentation is limited to the bundle integration. + You should also read the [library documentation](https://patchlevel.github.io/event-sourcing-docs/latest/). diff --git a/docs/pages/index.md b/docs/pages/index.md index c852d8d8..944c0cfd 100644 --- a/docs/pages/index.md +++ b/docs/pages/index.md @@ -1,7 +1,7 @@ # Event-Sourcing-Bundle -A lightweight but also all-inclusive event sourcing bundle -with a focus on developer experience and based on doctrine dbal. +An event sourcing bundle, complete with all the essential features, +powered by the reliable Doctrine ecosystem and focused on developer experience. This bundle is a [symfony](https://symfony.com/) integration for [event-sourcing](https://github.com/patchlevel/event-sourcing) library. @@ -10,9 +10,14 @@ for [event-sourcing](https://github.com/patchlevel/event-sourcing) library. * Everything is included in the package for event sourcing * Based on [doctrine dbal](https://github.com/doctrine/dbal) and their ecosystem * Developer experience oriented and fully typed -* [Snapshots](snapshots.md) system to quickly rebuild the aggregates -* [Scheme management](store.md) and [doctrine migration](store.md) support -* Built in [cli commands](cli.md) with [symfony](https://symfony.com/) +* Automatic [snapshot](https://patchlevel.github.io/event-sourcing-docs/latest/snapshots/)-system to boost your performance +* [Split](https://patchlevel.github.io/event-sourcing-docs/latest/split_stream/) big aggregates into multiple streams +* Versioned and managed lifecycle of [subscriptions](https://patchlevel.github.io/event-sourcing-docs/latest/subscription/) like projections and processors +* Safe usage of [Personal Data](https://patchlevel.github.io/event-sourcing-docs/latest/personal_data/) with crypto-shredding +* Smooth [upcasting](https://patchlevel.github.io/event-sourcing-docs/latest/upcasting/) of old events +* Simple setup with [scheme management](https://patchlevel.github.io/event-sourcing-docs/latest/store/) and [doctrine migration](https://patchlevel.github.io/event-sourcing-docs/latest/store/) +* Built in [cli commands](https://patchlevel.github.io/event-sourcing-docs/latest/cli/) with [symfony](https://symfony.com/) +* and much more... ## Installation @@ -27,3 +32,8 @@ composer require patchlevel/event-sourcing-bundle ## Integration * [Psalm](https://github.com/patchlevel/event-sourcing-psalm-plugin) + +!!! tip + + Start with the [quickstart](./getting_started.md) to get a feeling for the bundle. + \ No newline at end of file diff --git a/docs/pages/installation.md b/docs/pages/installation.md index 0dd161f0..fb0b8d82 100644 --- a/docs/pages/installation.md +++ b/docs/pages/installation.md @@ -26,35 +26,35 @@ return [ PatchlevelEventSourcingBundle::class => ['all' => true], ]; ``` -If you don't have `config/bundles.php` then you need to add the bundle in the kernel: - -```php -use Patchlevel\EventSourcingBundle\PatchlevelEventSourcingBundle; - -class AppKernel extends Kernel -{ - public function registerBundles(): void - { - $bundles = [ - new PatchlevelEventSourcingBundle(), - ]; - } -} -``` ## Configuration file -Now you have to add a minimal configuration file here `config/packages/patchlevel_event_sourcing.yaml`. +Now you have to add following recommended configuration file here `config/packages/patchlevel_event_sourcing.yaml`. ```yaml patchlevel_event_sourcing: aggregates: '%kernel.project_dir%/src' events: '%kernel.project_dir%/src' connection: - url: '%env(EVENTSTORE_URL)%' + url: '%env(EVENTSTORE_URL)%' + +when@dev: + patchlevel_event_sourcing: + subscription: + catch_up: true + throw_on_error: true + run_after_aggregate_save: true + rebuild_after_file_change: true + +when@test: + patchlevel_event_sourcing: + subscription: + catch_up: true + throw_on_error: true + run_after_aggregate_save: true ``` ## Dotenv -Finally we have to fill the ENV variable with a connection url. +Finally, we have to fill the ENV variable with a connection url. ```dotenv EVENTSTORE_URL=mysql://user:secret@localhost/app @@ -63,4 +63,7 @@ EVENTSTORE_URL=mysql://user:secret@localhost/app You can find out more about what a connection url looks like [here](https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url). -Now you can go back to [getting started](getting_started.md). +!!! success + + You have successfully installed the bundle. Now you can start with the [quickstart](./getting_started.md) to get a feeling for the bundle. + \ No newline at end of file diff --git a/docs/pages/message_decorator.md b/docs/pages/message_decorator.md deleted file mode 100644 index 16d42edc..00000000 --- a/docs/pages/message_decorator.md +++ /dev/null @@ -1,50 +0,0 @@ -# Message Decorator - -There are usecases where you want to add some extra context to your events like metadata which is not directly relevant -for your domain. With `MessageDecorator` we are providing a solution to add this metadata to your events. The metadata -will also be persisted in the database and can be retrieved later on. - -!!! info - - You can find out more about message decorator in the library - [documentation](https://patchlevel.github.io/event-sourcing-docs/latest/message_decorator/). - This documentation is limited to bundle integration. - -## Usage - -We want to add the header information which user was logged in when this event was generated. - -```php -use Patchlevel\EventSourcing\EventBus\Message; -use Patchlevel\EventSourcing\Repository\MessageDecorator\MessageDecorator; -use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; - -final class LoggedUserDecorator implements MessageDecorator -{ - public function __construct( - private readonly TokenStorageInterface $tokenStorage, - ) { - } - - public function __invoke(Message $message): Message - { - $token = $this->tokenStorage->getToken(); - - if (!$token) { - return $message; - } - - return $message->withCustomHeader('user', $token->getUserIdentifier()); - } -} -``` -If you have the symfony default service setting with `autowire`and `autoconfigure` enabled, -the message decorator is automatically recognized and registered at the `MessageDecorator` interface. -Otherwise you have to define the message decorator in the symfony service file: - -```yaml -services: - App\Message\Decorator\LoggedUserDecorator: - tags: - - event_sourcing.message_decorator -``` \ No newline at end of file diff --git a/docs/pages/normalizer.md b/docs/pages/normalizer.md deleted file mode 100644 index 1150a347..00000000 --- a/docs/pages/normalizer.md +++ /dev/null @@ -1,43 +0,0 @@ -# Normalizer - -Sometimes you also want to add more complex data in events as payload or in aggregates for the snapshots. -For example DateTime, enums or value objects. You can do that too. -However, you must define a normalizer for this so that the library knows how to write this data to the database -and load it again. - -!!! info - - You can find out more about normalizer in the library - [documentation](https://patchlevel.github.io/event-sourcing-docs/latest/normalizer/). - This documentation is limited to bundle integration. - -## Built-in Normalizer - -This bundle adds more Symfony specific normalizers in addition to the existing built-in normalizers. - -!!! note - - You can find the other build-in normalizers [here](https://patchlevel.github.io/event-sourcing-docs/latest/normalizer/#built-in-normalizer) - -### Uuid - -With the `Uuid` Normalizer, as the name suggests, you can convert Symfony Uuid objects to a string and back again. - -```php -use Patchlevel\EventSourcingBundle\Normalizer\SymfonyUuidNormalizer; -use Symfony\Component\Uid\Uuid; - -final class DTO -{ - #[SymfonyUuidNormalizer] - public Uuid $id; -} -``` -!!! warning - - The symfony uuid don't implement the `AggregateId` interface, so it can be used as aggregate id. - -!!! tip - - Use the `Uuid` implementation and `IdNormalizer` from the library to use it as an aggregate id. - \ No newline at end of file diff --git a/docs/pages/processor.md b/docs/pages/processor.md deleted file mode 100644 index 2c2ec734..00000000 --- a/docs/pages/processor.md +++ /dev/null @@ -1,92 +0,0 @@ -# Processor - -The `processor` is a kind of [event bus](./event_bus.md) listener that can execute actions on certain events. - -!!! info - - You can find out more about processor in the library - [documentation](https://patchlevel.github.io/event-sourcing-docs/latest/processor/). - This documentation is limited to bundle integration. - -!!! warning - - The following configuration option is only available with the default event bus. - If you use a different event bus, you will need to configure the listeners with that system. - You can find out more about this in the [event bus](./event_bus.md) documentation. - -## Usage - -A process can be for example used to send an email when a guest is checked in: - -```php -namespace App\Domain\Hotel\Listener; - -use App\Domain\Hotel\Event\GuestIsCheckedIn; -use Patchlevel\EventSourcing\Attribute\Subscribe; -use Patchlevel\EventSourcing\EventBus\Message; -use Patchlevel\EventSourcingBundle\Attribute\AsListener; -use Symfony\Component\Mailer\MailerInterface; -use Symfony\Component\Mime\Email; - -use function sprintf; - -#[AsListener] -final class SendCheckInEmailListener -{ - private function __construct(private MailerInterface $mailer) - { - } - - #[Subscribe(GuestIsCheckedIn::class)] - public function __invoke(Message $message): void - { - $event = $message->event(); - - $email = (new Email()) - ->from('noreply@patchlevel.de') - ->to('hq@patchlevel.de') - ->subject('Guest is checked in') - ->text(sprintf('A new guest named "%s" is checked in', $event->guestName())); - - $this->mailer->send($email); - } -} -``` -If you have the symfony default service setting with `autowire`and `autoconfiger` enabled, -the processor is automatically recognized and registered at the `AsProcessor` attribute. -Otherwise you have to define the processor in the symfony service file: - -```yaml -services: - App\Domain\Hotel\Listener\SendCheckInEmailListener: - tags: - - event_sourcing.processor -``` -## Priority - -You can also determine the `priority` in which the processors are executed. -The higher the priority, the earlier the processor is executed. -You have to add the tag manually and specify the priority. - -```php -namespace App\Domain\Hotel\Listener; - -#[AsProcessor(priority: 16)] -final class SendCheckInEmailListener -{ - // ... -} -``` -```yaml -services: - App\Domain\Hotel\Listener\SendCheckInEmailListener: - autoconfigure: false - tags: - - name: event_sourcing.processor - priority: 16 -``` -!!! warning - - You have to deactivate the `autoconfigure` for this service, - otherwise the service will be added twice. - \ No newline at end of file diff --git a/docs/pages/repository.md b/docs/pages/repository.md deleted file mode 100644 index 3224e142..00000000 --- a/docs/pages/repository.md +++ /dev/null @@ -1,76 +0,0 @@ -# Repository - -A `repository` takes care of storing and loading the `aggregates`. -The [design pattern](https://martinfowler.com/eaaCatalog/repository.html) of the same name is also used. - -Every aggregate needs a repository to be stored. -And each repository is only responsible for one aggregate. - -!!! info - - You can find out more about repository in the library - [documentation](https://patchlevel.github.io/event-sourcing-docs/latest/repository/). - This documentation is limited to bundle integration. - -## Use repositories - -You can access the specific repositories using the `RepositoryManager`. - -```php -use Patchlevel\EventSourcing\Repository\RepositoryManager; - -final class DoStuffAction -{ - public function __invoke(RepositoryManager $repositoryManager, HotelId $hotelId): Response - { - $hotelRepository = $repositoryManager->get(Hotel::class); - $hotel = $hotelRepository->load($hotelId); - - $hotel->doStuff(); - - $hotelRepository->save($hotel); - - // ... - } -} -``` -## Custom Repositories - -In clean code you want to have explicit type hints for the repositories -so that you don't accidentally use the wrong repository. -It would also help in frameworks with a dependency injection container, -as this allows the services to be autowired. -However, you cannot inherit from our repository implementations. -Instead, you just have to wrap these repositories. -This also gives you more type security. - -```php -use Patchlevel\EventSourcing\Repository\Repository; -use Patchlevel\EventSourcing\Repository\RepositoryManager; - -class HotelRepository -{ - /** @var Repository */ - private Repository $repository; - - public function __construct(RepositoryManager $repositoryManager) - { - $this->repository = $repositoryManager->get(Hotel::class); - } - - public function load(HotelId $id): Hotel - { - return $this->repository->load($id); - } - - public function save(Hotel $hotel): void - { - $this->repository->save($hotel); - } - - public function has(HotelId $id): bool - { - return $this->repository->has($id); - } -} -``` \ No newline at end of file diff --git a/docs/pages/snapshots.md b/docs/pages/snapshots.md deleted file mode 100644 index edffd785..00000000 --- a/docs/pages/snapshots.md +++ /dev/null @@ -1,83 +0,0 @@ -# Snapshots - -Some aggregates can have a large number of events. -This is not a problem if there are a few hundred. -But if the number gets bigger at some point, then loading and rebuilding can become slow. -The `snapshot` system can be used to control this. - -Normally, the events are all executed again on the aggregate in order to rebuild the current state. -With a `snapshot`, we can shorten the way in which we temporarily save the current state of the aggregate. -When loading it is checked whether the snapshot exists. -If a hit exists, the aggregate is built up with the help of the snapshot. -A check is then made to see whether further events have existed since the snapshot -and these are then also executed on the aggregate. -Here, however, only the last events are loaded from the database and not all. - -!!! info - - You can find out more about snapshots in the library - [documentation](https://patchlevel.github.io/event-sourcing-docs/latest/snapshots/). - This documentation is limited to bundle integration. - -## Using Symfony Cache - -You can use symfony cache to define the target of the snapshot store. - -```yaml -framework: - cache: - default_redis_provider: 'redis://localhost' - pools: - event_sourcing.cache: - adapter: cache.adapter.redis -``` -After this, you need define the snapshot store. -Symfony cache implement the psr6 interface, so we need choose this type -and enter the id from the cache service. - -```yaml -patchlevel_event_sourcing: - snapshot_stores: - default: - service: event_sourcing.cache -``` -Finally, you have to tell the aggregate that it should use this snapshot store. - -```php -namespace App\Domain\Profile; - -use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; -use Patchlevel\EventSourcing\Attribute\Aggregate; -use Patchlevel\EventSourcing\Attribute\Snapshot; - -#[Aggregate(name: 'profile')] -#[Snapshot('default')] -final class Profile extends BasicAggregateRoot -{ - // ... -} -``` -!!! book - - You can find out more about the attributes [here](aggregate.md). - -## Batch - -So that not every write process also writes to the cache at the same time, -you can also say from how many events should be written to the snapshot store first. -This minimizes the write operations to the cache, which improves performance. - -```php -namespace App\Domain\Profile; - -use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; -use Patchlevel\EventSourcing\Attribute\Aggregate; -use Patchlevel\EventSourcing\Attribute\Snapshot; - -#[Aggregate(name: 'profile')] -#[Snapshot('default', batch: 1000)] -final class Profile extends BasicAggregateRoot -{ - // ... -} -``` \ No newline at end of file diff --git a/docs/pages/store.md b/docs/pages/store.md deleted file mode 100644 index 5dd1842b..00000000 --- a/docs/pages/store.md +++ /dev/null @@ -1,90 +0,0 @@ -# Store - -In the end, the events have to be saved somewhere. -The library is based on [doctrine dbal](https://www.doctrine-project.org/projects/dbal.html) -and offers two different store strategies. - -!!! info - - You can find out more about stores in the library - [documentation](https://patchlevel.github.io/event-sourcing-docs/latest/store/). - This documentation is limited to bundle integration. - -## Manage database and schema - -The bundle provides you with a few `commands` with which you can create and delete the `database`. -You can also use it to create, edit and delete the `schema`. - -### Create and drop database - -```bash -bin/console event-sourcing:database:create -bin/console event-sourcing:database:drop -``` -### Create, update and drop schema - -```bash -bin/console event-sourcing:schema:create -bin/console event-sourcing:schema:udapte -bin/console event-sourcing:schema:drop -``` -## Use doctrine connection - -If you have installed the [doctrine bundle](https://github.com/doctrine/DoctrineBundle), -you can also define the connection via doctrine and then use it in the store. - -```yaml -doctrine: - dbal: - connections: - eventstore: - url: '%env(EVENTSTORE_URL)%' - -patchlevel_event_sourcing: - connection: - service: doctrine.dbal.eventstore_connection -``` -!!! warning - - If you want to use the same connection as doctrine orm, then you have to set the flag `merge_orm_schema`. - Otherwise you should avoid using the same connection as other tools. - -!!! note - - You can find out more about the dbal configuration - [here](https://symfony.com/bundles/DoctrineBundle/current/configuration.html). - -## Migration - -You can also manage your schema with doctrine migrations. - -```bash -bin/console event-sourcing:migration:diff -bin/console event-sourcing:migration:migrate -``` -You can also change the namespace and the folder in the configuration. - -```yaml -patchlevel_event_sourcing: - migration: - namespace: EventSourcingMigrations - path: "%kernel.project_dir%/migrations" -``` -## Merge ORM Schema - -You can also merge the schema with doctrine orm. You have to set the following flag for this: - -```yaml -patchlevel_event_sourcing: - store: - merge_orm_schema: true -``` -!!! note - - All schema relevant commands are removed if you activate this option. You should use the doctrine commands then. - -!!! warning - - If you want to merge the schema, then the same doctrine connection must be used as with the doctrine orm. - Otherwise errors may occur! - \ No newline at end of file diff --git a/docs/pages/subscription.md b/docs/pages/subscription.md deleted file mode 100644 index 350f1293..00000000 --- a/docs/pages/subscription.md +++ /dev/null @@ -1,126 +0,0 @@ -# Subscription - -With `projections` you can create your data optimized for reading. -projections can be adjusted, deleted or rebuilt at any time. -This is possible because the source of truth remains untouched -and everything can always be reproduced from the events. - -The target of a projection can be anything. -Either a file, a relational database, a no-sql database like mongodb or an elasticsearch. - -!!! info - - You can find out more about projection in the library - [documentation](https://patchlevel.github.io/event-sourcing-docs/latest/projection/). - This documentation is limited to bundle integration. - -## Define Projection - -In this example we are simply mapping hotel statistics: - -```php -namespace App\Projection; - -use App\Domain\Hotel\Event\GuestIsCheckedIn; -use App\Domain\Hotel\Event\GuestIsCheckedOut; -use App\Domain\Hotel\Event\HotelCreated; -use Doctrine\DBAL\Connection; -use Patchlevel\EventSourcing\Attribute\Create; -use Patchlevel\EventSourcing\Attribute\Drop; -use Patchlevel\EventSourcing\Attribute\Handle; -use Patchlevel\EventSourcing\EventBus\Message; -use Patchlevel\EventSourcing\Projection\Projector\Projector; -use Patchlevel\EventSourcing\Projection\Projector\ProjectorUtil; - -#[Projector(name: 'hotel')] -final class HotelProjection -{ - use ProjectorUtil; - - public function __construct(private Connection $db) - { - } - - /** @return list */ - public function getHotels(): array - { - return $this->db->fetchAllAssociative("SELECT id, name, guests FROM {$this->table()};"); - } - - #[Handle(HotelCreated::class)] - public function handleHotelCreated(Message $message): void - { - $event = $message->event(); - - $this->db->insert( - $this->table(), - [ - 'id' => $event->aggregateId(), - 'name' => $event->hotelName(), - 'guests' => 0, - ], - ); - } - - #[Handle(GuestIsCheckedIn::class)] - public function handleGuestIsCheckedIn(Message $message): void - { - $event = $message->event(); - - $this->db->executeStatement( - "UPDATE {$this->table()} SET guests = guests + 1 WHERE id = ?;", - [$event->aggregateId()], - ); - } - - #[Handle(GuestIsCheckedOut::class)] - public function handleGuestIsCheckedOut(Message $message): void - { - $event = $message->event(); - - $this->db->executeStatement( - "UPDATE {$this->table()} SET guests = guests - 1 WHERE id = ?;", - [$event->aggregateId()], - ); - } - - #[Create] - public function create(): void - { - $this->db->executeStatement("CREATE TABLE IF NOT EXISTS {$this->table()} (id VARCHAR PRIMARY KEY, name VARCHAR, guests INTEGER);"); - } - - #[Drop] - public function drop(): void - { - $this->db->executeStatement("DROP TABLE IF EXISTS {$this->table()};"); - } - - private function table(): string - { - return 'hotel_' . $this->projectorId(); - } -} -``` -If you have the symfony default service setting with `autowire`and `autoconfigure` enabled, -the projection is automatically recognized and registered at the `Projector` interface. -Otherwise you have to define the projection in the symfony service file: - -```yaml -services: - App\Projection\HotelProjection: - tags: - - event_sourcing.projector -``` -## Projection commands - -The bundle also provides a few commands to create, delete or rebuild projections: - -```bash -bin/console event-sourcing:projection:boot -bin/console event-sourcing:projection:run -bin/console event-sourcing:projection:teardown -bin/console event-sourcing:projection:remove -bin/console event-sourcing:projection:status -bin/console event-sourcing:projection:rebuild -``` \ No newline at end of file diff --git a/docs/pages/upcasting.md b/docs/pages/upcasting.md deleted file mode 100644 index 609fb01a..00000000 --- a/docs/pages/upcasting.md +++ /dev/null @@ -1,42 +0,0 @@ -# Upcasting - -There are cases where the already have events in our stream but there is data missing or not in the right format for our -new usecase. Normally you would need to create versioned events for this. This can lead to many versions of the same -event which could lead to some chaos. To prevent this we offer `Upcaster`, which can operate on the payload before -denormalizing to an event object. There you can change the event name and adjust the payload of the event. - -!!! info - - You can find out more about upcasting in the library - [documentation](https://patchlevel.github.io/event-sourcing-docs/latest/upcasting/). - This documentation is limited to bundle integration. - -## Usage - -```php -use Patchlevel\EventSourcing\Serializer\Upcast\Upcast; -use Patchlevel\EventSourcing\Serializer\Upcast\Upcaster; - -final class ProfileCreatedEmailLowerCastUpcaster implements Upcaster -{ - public function __invoke(Upcast $upcast): Upcast - { - // ignore if other event is processed - if ($upcast->eventName !== 'profile_created') { - return $upcast; - } - - return $upcast->replacePayloadByKey('email', strtolower($upcast->payload['email'])); - } -} -``` -If you have the symfony default service setting with `autowire`and `autoconfigure` enabled, -the upcaster is automatically recognized and registered at the `Upcaster` interface. -Otherwise you have to define the upcaster in the symfony service file: - -```yaml -services: - App\Upcaster\ProfileCreatedEmailLowerCastUpcaster: - tags: - - event_sourcing.upcaster -``` \ No newline at end of file diff --git a/docs/pages/usage.md b/docs/pages/usage.md new file mode 100644 index 00000000..1b709f37 --- /dev/null +++ b/docs/pages/usage.md @@ -0,0 +1,222 @@ +# Usage + +!!! info + + You can find out more about event sourcing in the library + [documentation](https://patchlevel.github.io/event-sourcing-docs/latest/). + This documentation is limited to bundle integration and configuration. + +## Repository + +You can access the specific repositories using the `RepositoryManager`. + +```php +use Patchlevel\EventSourcing\Repository\RepositoryManager; + +final class DoStuffAction +{ + public function __invoke(RepositoryManager $repositoryManager, HotelId $hotelId): Response + { + $hotelRepository = $repositoryManager->get(Hotel::class); + $hotel = $hotelRepository->load($hotelId); + + $hotel->doStuff(); + + $hotelRepository->save($hotel); + + // ... + } +} +``` + + +# Processor + +The `processor` is a kind of [event bus](./event_bus.md) listener that can execute actions on certain events. + +!!! info + + You can find out more about processor in the library + [documentation](https://patchlevel.github.io/event-sourcing-docs/latest/processor/). + This documentation is limited to bundle integration. + +!!! warning + + The following configuration option is only available with the default event bus. + If you use a different event bus, you will need to configure the listeners with that system. + You can find out more about this in the [event bus](./event_bus.md) documentation. + +## Usage + +A process can be for example used to send an email when a guest is checked in: + +```php +namespace App\Domain\Hotel\Listener; + +use App\Domain\Hotel\Event\GuestIsCheckedIn; +use Patchlevel\EventSourcing\Attribute\Subscribe; +use Patchlevel\EventSourcing\EventBus\Message; +use Patchlevel\EventSourcingBundle\Attribute\AsListener; +use Symfony\Component\Mailer\MailerInterface; +use Symfony\Component\Mime\Email; + +use function sprintf; + +#[AsListener] +final class SendCheckInEmailListener +{ + private function __construct(private MailerInterface $mailer) + { + } + + #[Subscribe(GuestIsCheckedIn::class)] + public function __invoke(Message $message): void + { + $event = $message->event(); + + $email = (new Email()) + ->from('noreply@patchlevel.de') + ->to('hq@patchlevel.de') + ->subject('Guest is checked in') + ->text(sprintf('A new guest named "%s" is checked in', $event->guestName())); + + $this->mailer->send($email); + } +} +``` +If you have the symfony default service setting with `autowire`and `autoconfiger` enabled, +the processor is automatically recognized and registered at the `AsProcessor` attribute. +Otherwise you have to define the processor in the symfony service file: + +```yaml +services: + App\Domain\Hotel\Listener\SendCheckInEmailListener: + tags: + - event_sourcing.processor +``` +## Priority + +You can also determine the `priority` in which the processors are executed. +The higher the priority, the earlier the processor is executed. +You have to add the tag manually and specify the priority. + +```php +namespace App\Domain\Hotel\Listener; + +#[AsProcessor(priority: 16)] +final class SendCheckInEmailListener +{ + // ... +} +``` +```yaml +services: + App\Domain\Hotel\Listener\SendCheckInEmailListener: + autoconfigure: false + tags: + - name: event_sourcing.processor + priority: 16 +``` +!!! warning + + You have to deactivate the `autoconfigure` for this service, + otherwise the service will be added twice. + +## Normalizer + +This bundle adds more Symfony specific normalizers in addition to the existing built-in normalizers. + +!!! note + + You can find the other build-in normalizers [here](https://patchlevel.github.io/event-sourcing-docs/latest/normalizer/#built-in-normalizer) + +### Uuid + +With the `Uuid` Normalizer, as the name suggests, you can convert Symfony Uuid objects to a string and back again. + +```php +use Patchlevel\EventSourcingBundle\Normalizer\SymfonyUuidNormalizer; +use Symfony\Component\Uid\Uuid; + +final class DTO +{ + #[SymfonyUuidNormalizer] + public Uuid $id; +} +``` +!!! warning + + The symfony uuid don't implement the `AggregateId` interface, so it can be used as aggregate id. + +!!! tip + + Use the `Uuid` implementation and `IdNormalizer` from the library to use it as an aggregate id. + +## Upcasting + +```php +use Patchlevel\EventSourcing\Serializer\Upcast\Upcast; +use Patchlevel\EventSourcing\Serializer\Upcast\Upcaster; + +final class ProfileCreatedEmailLowerCastUpcaster implements Upcaster +{ + public function __invoke(Upcast $upcast): Upcast + { + // ignore if other event is processed + if ($upcast->eventName !== 'profile_created') { + return $upcast; + } + + return $upcast->replacePayloadByKey('email', strtolower($upcast->payload['email'])); + } +} +``` +If you have the symfony default service setting with `autowire`and `autoconfigure` enabled, +the upcaster is automatically recognized and registered at the `Upcaster` interface. +Otherwise you have to define the upcaster in the symfony service file: + +```yaml +services: + App\Upcaster\ProfileCreatedEmailLowerCastUpcaster: + tags: + - event_sourcing.upcaster +``` + +## Message Decorator + +We want to add the header information which user was logged in when this event was generated. + +```php +use Patchlevel\EventSourcing\EventBus\Message; +use Patchlevel\EventSourcing\Repository\MessageDecorator\MessageDecorator; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; + +final class LoggedUserDecorator implements MessageDecorator +{ + public function __construct( + private readonly TokenStorageInterface $tokenStorage, + ) { + } + + public function __invoke(Message $message): Message + { + $token = $this->tokenStorage->getToken(); + + if (!$token) { + return $message; + } + + return $message->withCustomHeader('user', $token->getUserIdentifier()); + } +} +``` +If you have the symfony default service setting with `autowire`and `autoconfigure` enabled, +the message decorator is automatically recognized and registered at the `MessageDecorator` interface. +Otherwise you have to define the message decorator in the symfony service file: + +```yaml +services: + App\Message\Decorator\LoggedUserDecorator: + tags: + - event_sourcing.message_decorator +``` \ No newline at end of file