diff --git a/doc/bookdown.json b/doc/bookdown.json index d570198..1a2f5e9 100644 --- a/doc/bookdown.json +++ b/doc/bookdown.json @@ -1,12 +1,15 @@ { "title": "prooph event-store Symfony bundle", "content": [ - {"getting-started": "book/getting-started/bookdown.json"}, - {"reference": "book/reference/bookdown.json"}, - {"contribute": "book/contribute/bookdown.json"} + {"intro": "../README.md"}, + {"getting_started": "getting_started.md"}, + {"event_store": "event_store.md"}, + {"projection_manager": "projection_manager.md"}, + {"event_store_bus_bridge": "event_store_bus_bridge.md"}, + {"configuration_reference": "configuration_reference.md"} ], "target": "./html", "tocDepth": 2, - "template": "../vendor/tobiju/bookdown-bootswatch-templates/templates/main.php", + "template": "../vendor/prooph/bookdown-template/templates/main.php", "copyright": "Copyright (c) 2016 - 2019 prooph maintainers
Powered by Bookdown Bootswatch Templates" } diff --git a/doc/configuration_reference.md b/doc/configuration_reference.md new file mode 100644 index 0000000..2579d21 --- /dev/null +++ b/doc/configuration_reference.md @@ -0,0 +1,131 @@ +# Configuration Reference + +```yaml +prooph_event_store: + stores: + acme_store: + event_emitter: Prooph\Common\Event\ProophActionEventEmitter + wrap_action_event_emitter: true + event_store: Prooph\EventStore\Pdo\MysqlEventStore + repositories: + todo_list: + repository_class: Prooph\ProophessorDo\Infrastructure\Repository\EventStoreUserCollection + aggregate_type: Prooph\ProophessorDo\Model\User\User + aggregate_translator: prooph_event_sourcing.aggregate_translator + snapshot_store: ~ + stream_name: ~ + one_stream_per_aggregate: false + projection_managers: + main_manager: + event_store: Prooph\EventStore\Pdo\MysqlEventStore + connection: 'doctrine.pdo.connection' + event_streams_table: 'event_streams' + projection_table: 'projections' + projections: + user_projection: + read_model: Prooph\ProophessorDo\Projection\User\UserReadModel + projection: Prooph\ProophessorDo\Projection\User\UserProjection +``` + +## stores + +*Optional* + +This section contains the configuration of your event stores. +Please have a look at [the event store section](./event_store.md) of this documentation for further details. +The name of the event store will be part of its service id: `prooph_event_store.`. +For the `acme_store` in our example above it will be `prooph_event_store.acme_store`. + +### event_emitter + +*Optional* + +The event emitter that is used by the ActionEventEmitterEventStore. +It must be a class that implements `Prooph\Common\Event\ActionEventEmitter`. +The default value should be fine for most use cases. + + +### wrap_action_event_emitter + +*Optional* + +Should the given event store be decorated by an ActionEventEmitterEventStore? +In most cases you should keep this with the default value `true`. + + +### event_store + +*Required* + +The id of a service whose class implements `Prooph\EventStore\EventStore`. +Please have a look at [the event store section](./event_store.md) of this documentation for further details. + + +### repositories + +*Optional* + +Defines the repository that you can use to load and store your aggregates. +For further details please have a look at [the event store section](./event_store.md) of this documentation. + + +#### repository_class + +*Required* + +The FQCN of the repository. +In most cases it will be a subclass of `Prooph\EventSourcing\Aggregate\AggregateRepository` but it most not be, +it must just accept the same arguments in the constructor. + + +#### aggregate_type + +*Required* + +The FQCN of the aggregate that is loaded and stored by the repository. + + +## aggregate_translator + +*Required* + +The service id of the aggregate translator that is used by the repository. +Its class must implement `Prooph\EventSourcing\Aggregate\AggregateTranslator`. + +#### snapshot_store + +*Optional* + +The service id of a snap shot store. +Its class must implement `Prooph\SnapshotStore\SnapshotStore`. + +#### stream_name + +*Optional* + +You can pass a string as custom stream name if you want. + + +#### one_stream_per_aggregate + +*Optional* + +Should the repository create an own single stream for each aggregate? +See section *Using different Stream Strategies* for of [the event store section](./event_store.md) of this documentation for further details. + + +## projection_managers + +### event_store + +### connection + +### event_streams_table + +### projection_table + +### projections + +#### read_model + +#### projection diff --git a/doc/event_store.md b/doc/event_store.md new file mode 100644 index 0000000..292293b --- /dev/null +++ b/doc/event_store.md @@ -0,0 +1,284 @@ +# Event Store + +This documentation covers just the configuration of Prooph Event Stores in Symfony. +To inform yourself about the Prooph Event Store, please have a look at its +[official documentation](http://docs.getprooph.org/event-store/). + +## Setting up a MySQL PDO Event Store + +To setup a MySQL PDO Event Store we need the `prooph/pdo-event-store` package. + +```bash +composer require prooph/pdo-event-store +``` + +> **Hint**: You can also follow this instruction if you want to setup a PDO Event Store for MariaDB or PostgreSQL. +> You just need to use other classes which are also part of the `prooph/pdo-event-store` package. +> For further details please have a look at the [prooph/pdo-event-store package](https://github.com/prooph/pdo-event-store). + +Before we setup our event store, we need to setup some services: + +```yaml +# app/config/services.yml or (flex) config/packages/prooph_event_store.yaml +services: + prooph_event_store.pdo_mysql_event_store: + class: Prooph\EventStore\Pdo\MySqlEventStore + arguments: + - '@prooph_event_store.message_factory' + - '@pdo.connection' + - '@prooph_event_store.mysql.single_stream_strategy' + + prooph_event_store.mysql.single_stream_strategy: + class: Prooph\EventStore\Pdo\PersistenceStrategy\MySqlSingleStreamStrategy + + pdo.connection: + class: PDO + arguments: ['%dsn%'] +``` + +> **Hint**: For reusing a PDO connection from Doctrine please see below. + +> **Hint**: You can also use other stream strategies. +> Have a look at the documentation of the [prooph/pdo-event-store package](https://github.com/prooph/pdo-event-store/blob/master/docs/variants.md) +> to learn about the different strategies. +> See below for further information within this bundle. + +Do not be confused about the fact that the we defined a service with a class called event store – we are not done yet. +But we are ready to configure the event store: + +```yaml +# app/config/config.yml or (flex) config/packages/prooph_event_store.yaml +prooph_event_store: + stores: + acme_store: + event_store: 'prooph_event_store.pdo_mysql_event_store' +``` + +> **Hint**: To get autocompletion in some IDEs you can prepend the service id +> with an `@` (`'@prooph_event_store.pdo_mysql_event_store'`). +> +> The bundle will recognize this and find your event store anyway. + +We configured our first event store. +To put data into the event store (and read them from it) we might want to add repositories. + +## Adding repositories to the event store + +If you are adding repositories to your event_store, you want to go for event sourcing. +Therefore we will explain how to add an event sourced repository. + +First you need to install another package, prooph/event-sourcing](http://docs.getprooph.org/event-sourcing/): + +```bash +composer require prooph/event-sourcing +``` + +Before we can start adding our repositories, we need to define another service: + +```yaml +# app/config/config.yml or (flex) config/packages/prooph_event_store.yaml +services: + prooph_event_sourcing.aggregate_translator: + class: Prooph\EventSourcing\EventStoreIntegration\AggregateTranslator +``` + +It will help our repository to translate the event stream into an aggregate and vice versa. + +We assume that there is + - a class `Acme\Prooph\Repository\EventStoreUserRepository` which extends `Prooph\EventSourcing\Aggregate\AggregateRepository` + - and a class `Acme\Model\User` which extends `Prooph\EventSourcing\AggregateRoot`. + +More information about Aggregate Roots and Aggregate Repositories can be found in the [official documentation](http://docs.getprooph.org/event-sourcing/). + +Now we can configure our repository: + +```yaml +# app/config/config.yml or (flex) config/packages/prooph_event_store.yaml +prooph_event_store: + stores: + acme_store: + event_store: 'prooph_event_store.pdo_mysql_event_store' + repositories: + Acme\Prooph\Repository\EventStoreUserRepository: + aggregate_type: Acme\Model\User + aggregate_translator: 'prooph_event_sourcing.aggregate_translator' +``` + +> **Hint**: To get autocompletion in some IDEs you can prepend the service id +> with an `@` (`'@prooph_event_sourcing.aggregate_translator'`). +> +> The bundle will recognize this and find your event store anyway. + +Now you can access the repository from the service container with the id `Acme\Prooph\Repository\EventStoreUserRepository`. + +> **Hint**: If you do not want your repositories to have their classes as service ids, +> you can configure them like this: +> ```yaml +> # app/config/config.yml or (flex) config/packages/prooph_event_store.yaml +> prooph_event_store: +> stores: +> acme_store: +> event_store: 'prooph_event_store.pdo_mysql_event_store' +> repositories: +> acme.repository.prooph.event_store_user_repository: +> repository_class: Acme\Prooph\Repository\EventStoreUserRepository +> aggregate_type: Acme\Model\User +> aggregate_translator: 'prooph_event_sourcing.aggregate_translator' +> ``` +> This way your repository will be accessible with the service id `acme.repository.prooph.event_store_user_repository`. + +## Reusing a PDO connection from Doctrine + +If you already have a PDO connection configured through doctrine +and you want to use the same connection for your event store, +there is a simple way to reuse it: + +```yaml +# app/config/services.yaml +services: + prooph_event_store.connection.doctrine_pdo_connection: + class: PDO + factory: ['@doctrine.dbal.default_connection', getWrappedConnection] +``` + +## Using different Stream Strategies + +To make yourself familiar with different stream strategies, +please have a look at the documentation of the [prooph/pdo-event-store package](https://github.com/prooph/pdo-event-store/blob/master/docs/variants.md). + +If you want to use one of the Single Stream Strategies, you just need to set up the Strategy as a service and pass it to the event store: + + +```yaml +# app/config/services.yml or (flex) config/packages/prooph_event_store.yaml +services: + prooph_event_store.pdo_mysql_event_store: + class: Prooph\EventStore\Pdo\MySqlEventStore + arguments: + - '@prooph_event_store.message_factory' + - '@pdo.connection' + - '@prooph_event_store.mysql.single_stream_strategy' + + prooph_event_store.mysql.single_stream_strategy: + class: Prooph\EventStore\Pdo\PersistenceStrategy\MySqlSingleStreamStrategy + + pdo.connection: + class: PDO + arguments: ['%dsn%'] +``` + +If you want to use one of the Aggregate Stream Strategies, +you also need to configure your Stream Strategie as a service like above. +But you also need to your repositories to follow this strategy using the `one_stream_per_aggregate` option: + +```yaml +# app/config/config.yml or (flex) config/packages/prooph_event_store.yaml +prooph_event_store: + stores: + acme_store: + event_store: 'prooph_event_store.pdo_mysql_event_store' + repositories: + Acme\Prooph\Repository\EventStoreUserRepository: + aggregate_type: Acme\Model\User + aggregate_translator: 'prooph_event_sourcing.aggregate_translator' + one_stream_per_aggregate: true +``` + +Because this option defaults to `false`, this is only necessary for Aggregate Stream Strategies. + +## Plugins + +A prooph Event Store can be expanded using plugins. +If you want to know more about Event Store Plugins, please have a look at the [official documentation](http://docs.getprooph.org/event-store/event_store_plugins.html). + +Adding plugins to an Event Store is really simple. +Let's assume that we already have a class implementing `Prooph\EventStore\Plugin\Plugin` +and that we have configured it as service: + +```yaml +# app/config/services.yml +services: + acme.prooph.plugins.example_plugin: + class: Acme\Prooph\Plugins\ExamplePlugin +``` + +To attaching the plugin to an Event Store, we just need to tag it with `prooph_event_store..plugin`. +So if our Event Store is named `acme_store`, it would look like this: + +```yaml +# app/config/services.yml +services: + acme.prooph.plugins.example_plugin: + class: Acme\Prooph\Plugins\ExamplePlugin + tags: + - { name: 'prooph_event_store.acme_store.plugin' } +``` + +If you cant to attach to plugin to multiple Event Stores, just tag it multiple times. +In the special case that you want to attach the plugin to **every** Event Store, +you can use the tag `prooph_event_store.plugin` instead. + +## Metadata enricher + +If you do not know what a metadata enricher is, please have a look at the official documentation of [Metadata enricher](http://docs.getprooph.org/event-store/event_store_plugins.html#3-3-4). + +Let's assume that we want to add the issuer of an event to the metadata. +Our Metadata enricher might look like this: + +```php +tokens = $tokens; + } + + public function enrich(Message $message): Message + { + if ($this->tokens->getToken()) { + $message = $message + ->withAddedMetadata('issuer_type', 'user') + ->withAddedMetadata('issuer_name', $this->tokens->getToken()->getUsername()); + } + return $message; + } +} +``` + +And our service definition like this: + +```yaml +# app/config/services.yml +services: + acme.prooph.metadata_enricher.issuer: + class: Acme\Prooph\MetadataEnricher\IssuerMetadataEnricher + arguments: ['@security.token_storage'] +``` + +To enable the enricher for every Event Store, we just need to tag the service: + +```yaml +# app/config/services.yml +services: + acme.prooph.metadata_enricher.issuer: + class: Acme\Prooph\MetadataEnricher\IssuerMetadataEnricher + arguments: ['@security.token_storage'] + tags: + - { name: 'prooph_event_store.metadata_enricher' } +``` + +But be careful, this tag would add the metadata enricher to **every** Event Store. +If you want to add it only to one store, you need to use the tag `prooph_event_store..metadata_enricher`. diff --git a/doc/event_store_bus_bridge.md b/doc/event_store_bus_bridge.md new file mode 100644 index 0000000..bd9ee57 --- /dev/null +++ b/doc/event_store_bus_bridge.md @@ -0,0 +1,64 @@ +# Event Store Bus Bridge + +While both the Prooph Event Store Symfony Bundle and the [Prooph Service Bus Symfony Bundle](https://github.com/prooph/service-bus-symfony-bundle/) +are useful on its own, usually you want to use both together. + +To combine them, you will need the [Prooph Event Store Bus Bridge](https://github.com/prooph/event-store-bus-bridge) +that can be required with composer: + +```bash +composer require prooph/event-store-bus-bridge +``` + +There are three locations where we can combine both bundles. + +## Transaction Manager + +The transaction manager starts a new event store transaction on every command dispatch and commits its afterwards. +To enable it for an Event Store, you need to add a service and tag it as a plugin for a service bus. + +Assuming that we have an Event Store named `acme_store` and a Service Bus named `acme_command_bus` +the configuration might look like this: + +```yaml +# app/config/services.yml +services: + prooph_event_store_bus_bridge.acme_transaction_manager: + class: Prooph\EventStoreBusBridge\TransactionManager + arguments: ['@prooph_event_store.acme_store'] + tags: + - { name: 'prooph_service_bus.acme_command_bus.plugin' } +``` + +## Event Publisher + +The Event Publisher is an Event Store Plugin which listens on the Event Store and publishes recorded message on the Event Bus. + +Assuming that we have an Event Store named `acme_store` and an Event Bus named `acme_event_bus` +the configuration might look like this: + +```yaml +# app/config/services.yml +services: + prooph_event_store_bus_bridge.acme_event_publisher: + class: Prooph\EventStoreBusBridge\EventPublisher + arguments: ['@prooph_service_bus.acme_event_bus'] + tags: + - { name: 'prooph_event_store.acme_store.plugin' } +``` + +## Causation Metadata Enricher + +The Causation Metadata Enricher will add causation metadata to each recorded event. + +To enable it for all command buses and all event stores you can use a configuration like this: + +```yaml +# app/config/services.yml +services: + prooph_event_store_bus_bridge.causation_metadata_enricher: + class: Prooph\EventStoreBusBridge\CausationMetadataEnricher + tags: + - { name: 'prooph_service_bus.command_bus.plugin' } + - { name: 'prooph_event_store.plugin' } +``` diff --git a/doc/getting_started.md b/doc/getting_started.md new file mode 100644 index 0000000..a2777c3 --- /dev/null +++ b/doc/getting_started.md @@ -0,0 +1,49 @@ +# Getting started + +This documentation covers just the configuration of the Prooph Event Store in Symfony. +To inform yourself about the Event Store please have a look at the +[official documentation](http://docs.getprooph.org/event-store/). + +## Download the Bundle + +Download the bundle using composer by running +```bash +composer require prooph/event-store-symfony-bundle +``` +at the root of your Symfony project. + +## Enable the Bundle + +To start using this bundle, register the bundle in your application's kernel class: +```php + ['all' => true], +]; +``` + +Now that you have installed the bundle you might want start with configuring an [Event Store](./event_store.html). diff --git a/doc/projection_manager.md b/doc/projection_manager.md new file mode 100644 index 0000000..a20cf45 --- /dev/null +++ b/doc/projection_manager.md @@ -0,0 +1,255 @@ +# Projection Manager + +Projection Managers will help you to create persistent projections from your event stream. +For further information please have a look at the [official documentation](http://docs.getprooph.org/event-store/projections.html). + +Before you can setup an Projection Manager, you need to setup at least one [Event Store](./event_store.html). + +Then you can add a Projection Manager: + +```yaml +# app/config/config.yml or (flex) config/packages/prooph_event_store.yaml +prooph_event_store: + projection_managers: + acme_projection_manager: + event_store: 'prooph_event_store.pdo_mysql_event_store' + connection: 'pdo.connection' +``` + +Currently the bundle is limited to support Projection Managers for Event Stores +that are either part of the [prooph/pdo-event-store package](https://github.com/prooph/pdo-event-store) +or an `Prooph\EventStore\InMemoryEventStore`. +In the latter case you can omit the `connection`. + +Because a projection manager is worthless without connections he can manage, let's configure some projections. + +## Configure a projection + +To configure a projection we need a Projection before. + +Projections might either implement `Prooph\Bundle\EventStore\Projection` +or implement `Prooph\Bundle\EventStore\Projection\ReadModelProjection`. + +Both interfaces have just one method to configure the projection as explained in the [Event Store documentation](http://docs.getprooph.org/event-store/projections.html). + +To give one example from [proophessor-do-symfony](https://github.com/prooph/proophessor-do-symfony) +here is a Read Model: + +```php +connection = $connection; + } + + public function init(): void + { + $sql = <<connection->executeQuery($sql); + } + + public function isInitialized(): bool + { + $statement = $this->connection->executeQuery('SHOW TABLES LIKE read_todo;'); + return $statement->fetch() !== false; + } + + public function reset(): void + { + $this->connection->executeQuery('TRUNCATE TABLE read_todo;'); + } + + public function delete(): void + { + $this->connection->executeQuery('DROP TABLE read_todo;'); + } + + protected function insert(array $data): void + { + $this->connection->insert('read_todo', $data); + } + + protected function update(array $data, array $identifier): void + { + $this->connection->update('read_todo', $data, $identifier); + } +} +``` + +and the projection that uses the ReadModel: + +```php +fromStream('event_stream') + ->when([ + TodoWasPosted::class => function ($state, TodoWasPosted $event) { + /** @var TodoReadModel $readModel */ + $readModel = $this->readModel(); + $readModel->stack('insert', [ + 'id' => $event->todoId()->toString(), + 'assignee_id' => $event->assigneeId()->toString(), + 'text' => $event->text()->toString(), + 'status' => $event->todoStatus()->toString(), + ]); + }, + TodoWasMarkedAsDone::class => function ($state, TodoWasMarkedAsDone $event) { + /** @var TodoReadModel $readModel */ + $readModel = $this->readModel(); + $readModel->stack( + 'update', + ['status' => $event->newStatus()->toString()], + ['id' => $event->todoId()->toString()] + ); + }, + TodoWasReopened::class => function ($state, TodoWasReopened $event) { + /** @var TodoReadModel $readModel */ + $readModel = $this->readModel(); + $readModel->stack( + 'update', + ['status' => $event->status()->toString()], + ['id' => $event->todoId()->toString()] + ); + }, + DeadlineWasAddedToTodo::class => function ($state, DeadlineWasAddedToTodo $event) { + /** @var TodoReadModel $readModel */ + $readModel = $this->readModel(); + $readModel->stack( + 'update', + ['deadline' => $event->deadline()->toString()], + ['id' => $event->todoId()->toString()] + ); + }, + ReminderWasAddedToTodo::class => function ($state, ReminderWasAddedToTodo $event) { + /** @var TodoReadModel $readModel */ + $readModel = $this->readModel(); + $readModel->stack( + 'update', + ['reminder' => $event->reminder()->toString()], + ['id' => $event->todoId()->toString()] + ); + }, + TodoWasMarkedAsExpired::class => function ($state, TodoWasMarkedAsExpired $event) { + /** @var TodoReadModel $readModel */ + $readModel = $this->readModel(); + $readModel->stack( + 'update', + ['status' => $event->newStatus()->toString()], + ['id' => $event->todoId()->toString()] + ); + }, + TodoWasUnmarkedAsExpired::class => function ($state, TodoWasUnmarkedAsExpired $event) { + /** @var TodoReadModel $readModel */ + $readModel = $this->readModel(); + $readModel->stack( + 'update', + ['status' => $event->newStatus()->toString()], + ['id' => $event->todoId()->toString()] + ); + }, + ]); + + return $projector; + } +} +``` + +A lot of code, but really simple one. Its configuration is shorter. +First we need to define a service definition for both: + +```yaml +# app/config/services.yml +services: + proophessor.projection.todo: + class: Prooph\ProophessorDo\Projection\Todo\TodoProjection + + proophessor.projection.read_model.todo: + class: Prooph\ProophessorDo\Projection\Todo\TodoReadModel +``` + +Now we have two possibilities to configure the projections. + +## Tags + +We can add a Tag to the projection: + +```yaml +# app/config/services.yml +services: + proophessor.projection.todo: + class: Prooph\ProophessorDo\Projection\Todo\TodoProjection + tags: + - { name: prooph_event_store.projection, projection_name: todo_projection, projection_manager: acme_projection_manager, read_model: 'proophessor.projection.read_model.todo' } + + proophessor.projection.read_model.todo: + class: Prooph\ProophessorDo\Projection\Todo\TodoReadModel +``` + +While the `projection_name` is freely selectable, the `projection_manager` must reference an existing Projection Manager +(like the one we configured above). +The `read_model` attribute is necessary only if the projection implements `Prooph\Bundle\EventStore\Projection\ReadModelProjection`. + +## Central + +If you do not like tags or want to configure your projections at a central place, +you can do this directly at the `projection_manager`: + +```yaml +# app/config/config.yml or (flex) config/packages/prooph_event_store.yaml +prooph_event_store: + projection_managers: + acme_projection_manager: + event_store: 'prooph_event_store.pdo_mysql_event_store' + connection: 'pdo.connection' + projections: + todo_projection: + read_model: 'proophessor.projection.read_model.todo' + projection: 'proophessor.projection.todo' +``` + +As with the tag the `read_model` is necessary only if the projection implements `Prooph\Bundle\EventStore\Projection\ReadModelProjection`. + +Since both ways will produce the same result, it is up to you which of them you choose. + +## Running projections